npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vention/machine-logic-ui-sdk

v0.46.1

Published

Readme

@vention/machine-logic-ui-sdk

Browser helpers for a MachineLogic application's custom HMI, the React app in the application's customui/ folder that the pendant and the control center load in an iframe. It gives the HMI live data from the machine's MQTT broker, the URLs of the execution engine and of the application's own backend processes, an on-screen keyboard for touch pendants, and the messages that close the HMI or move the control center to another page. Install it at the exact version the application pins in customui/package.json:

npm install @vention/machine-logic-ui-sdk@<pinned-version>

What the HMI needs alongside it:

  • React 17 or 19. The package's peerDependencies accepts react ^17.0.2 || ^19.3.0. The SDK is type-checked against both versions (the typecheck-react19 target); its unit tests run on React 17.
  • @mui/material and @emotion/react. The package entry imports tss-react/mui (used by VirtualKeyboard) at load time, so importing any export pulls it in.
  • A bundler that handles CSS imports, such as Vite. The entry imports react-simple-keyboard/build/css/index.css.

Inside the platform-products monorepo the same source is also reachable as @ventionco/machine-logic-ui-sdk through a TypeScript path alias. Applications install the published @vention/machine-logic-ui-sdk.

The parameter, prop and return types of every export are in the package's type declarations, which your editor and tsc read. Look there for signatures; this README covers only behaviour that the types can't show.

Live data over MQTT

Wrap the app once in MqttProvider. It opens one MQTT-over-websocket connection that every useMqttSubscription call below it shares, and reconnects on its own.

  • version defaults to "v2" and picks which broker address to derive, through getMqttWsUrl. On the edge both use an unencrypted websocket: "v2" connects to 192.168.5.2:9001 and "v3" connects to port 9001 on the host that serves the HMI. In the cloud iframe both use the same wss:// URL: wss://<the HMI's host><mqttPath> when the host page gives an mqttPath query parameter that starts with /, else a URL derived from the iframe URL.
  • url sets the broker websocket URL yourself. When set, version and the URL detection are skipped. Use it for standalone development, for example ws://localhost:9001.

From projects/machine-code/apps/ai-operator/frontend/src/main.tsx, trimmed:

import { StrictMode } from "react"
import ReactDOM from "react-dom"
import { MqttProvider } from "@vention/machine-logic-ui-sdk"

import App from "./app/app"

ReactDOM.render(
  <StrictMode>
    <MqttProvider version="v3">
      <App />
    </MqttProvider>
  </StrictMode>,
  document.getElementById("root")
)

For development outside the iframe, pass the broker yourself, for example with VITE_MQTT_WS_URL=ws://localhost:9001. Left unset, the provider falls back to detection:

<MqttProvider version="v3" url={import.meta.env.VITE_MQTT_WS_URL}>
  <App />
</MqttProvider>

Read a topic with useMqttSubscription. It returns the latest message on that topic, or undefined until the first one arrives. The broker subscription is made once per topic, so only the first component to read a topic gets its retained message; a component that mounts later for the same topic returns undefined until the next publish.

  • topic must be an exact topic. The hook compares each incoming topic to this string with ===, so a wildcard filter such as operation/bins/+ subscribes at the broker but never updates the returned value. Use MqttClient for wildcards.
  • unsubscribeOnUnmount, off by default, unsubscribes at the broker when the component unmounts. The broker subscription is shared per topic across the provider, so this also stops the topic for any other component still reading it. The cleanup also runs whenever the connection status changes, and the hook subscribes again once it's connected.
  • Every payload goes through JSON.parse. Publish JSON on topics the HMI reads; a payload that is not valid JSON throws in the message handler. The type argument is an assertion, not a runtime check.
  • Nothing subscribes until the provider's connectionStatus is "connected". After a reconnect the hooks subscribe again.

From projects/machine-code/apps/ai-operator/frontend/src/app/contexts/operation-context.tsx, trimmed:

import { useEffect, useState } from "react"
import { useMqttSubscription } from "@vention/machine-logic-ui-sdk"

interface BinStatus {
  cardState: "normal" | "missing"
  pickedCount?: number
  averageCycleTime?: number
}

export const OperationStatus = () => {
  const [isOperationRunning, setIsOperationRunning] = useState(false)

  const operationActive = useMqttSubscription<boolean>({
    topic: "operation/operationActive",
    unsubscribeOnUnmount: true,
  })

  const bin1Status = useMqttSubscription<BinStatus>({
    topic: "operation/bins/1",
    unsubscribeOnUnmount: true,
  })

  useEffect(
    function handleOperationActiveFromMqtt() {
      if (operationActive === undefined) return
      setIsOperationRunning(operationActive)
    },
    [operationActive]
  )

  return (
    <p>
      {isOperationRunning ? "Running" : "Stopped"}, bin 1 picked {bin1Status?.pickedCount ?? 0}
    </p>
  )
}

mqttContext is the React context the provider fills. Read it with useContext(mqttContext):

  • client: the provider's mqtt.js MqttClient (not this package's MqttClient class), or null before the first connection. Use it to publish.
  • connectionStatus: the provider's connection state.
  • subscribedTopics, addSubscribedTopic, removeSubscribedTopic: the provider's bookkeeping for useMqttSubscription. An application does not need them.
import { useContext } from "react"
import { mqttContext } from "@vention/machine-logic-ui-sdk"

export const StopButton = () => {
  const { client, connectionStatus } = useContext(mqttContext)

  return (
    <button disabled={connectionStatus !== "connected"} onClick={() => client?.publish("operation/command", JSON.stringify({ action: "stop" }))}>
      Stop
    </button>
  )
}

Typing topics

The MqttPayloadByTopic type looks up a topic's payload type in a map of topics. Declare the application's topics once as that map:

import { MqttPayloadByTopic, useMqttSubscription } from "@vention/machine-logic-ui-sdk"

type OperationTopics = {
  "operation/operationActive": boolean
  "operation/bins/1": { cardState: "normal" | "missing"; pickedCount?: number }
}

type BinStatus = MqttPayloadByTopic<OperationTopics, "operation/bins/1">

export function useOperationTopic<Topic extends keyof OperationTopics & string>(topic: Topic) {
  return useMqttSubscription<MqttPayloadByTopic<OperationTopics, Topic>>({ topic, unsubscribeOnUnmount: true })
}

// useOperationTopic("operation/operationActive") returns boolean | undefined

The raw client

MqttClient is a small wrapper over mqtt.js for code outside React, or for wildcard subscriptions.

  • The constructor connects immediately. Its optional callback runs on every successful connection, including after a reconnect.
  • connect is what the constructor calls. Calling it again opens a second connection; an application does not need to.
  • subscribe(topic, regex, callback) subscribes to topic at the broker (the MQTT wildcards + and # are allowed) and calls callback for each incoming message whose topic matches regex.
  • unsubscribe(topic, regex, callback) removes that callback, and unsubscribes topic at the broker once no remaining callback uses the same regex.
  • publish publishes a string, not retained unless you pass retain. Serialize objects with JSON.stringify yourself.
  • A Callback receives the topic and the raw payload as a string; the payload is not parsed.

Calls made before the first connection opens are queued and sent once it does. After a close, the client is disconnected for about a second before it reconnects; during that gap publish is dropped silently and subscribe never reaches the broker, and once reconnected the client does not re-send the broker subscriptions made on the old connection. For a long-lived React HMI, prefer MqttProvider and useMqttSubscription, which resubscribe after a reconnect.

import { Callback, MqttClient, getMqttWsUrl } from "@vention/machine-logic-ui-sdk"

const BIN_TOPIC_FILTER = "operation/bins/+"
const BIN_TOPIC_PATTERN = /^operation\/bins\/\d+$/

const onBinStatus: Callback = (topic, message) => {
  const status = JSON.parse(message)
  console.log(topic, status.pickedCount)
}

export async function watchBins(): Promise<MqttClient> {
  const client = new MqttClient(await getMqttWsUrl("v3"), () => console.log("MQTT connected"))
  client.subscribe(BIN_TOPIC_FILTER, BIN_TOPIC_PATTERN, onBinStatus)
  client.publish("operation/command", JSON.stringify({ action: "start" }))
  return client
}

// Later: client.unsubscribe(BIN_TOPIC_FILTER, BIN_TOPIC_PATTERN, onBinStatus)

URL helpers:

  • connectToMqttAndGetClient builds a client on await getMqttWsUrl(), which is the "v2" broker address. It takes no arguments; for "v3" construct new MqttClient(await getMqttWsUrl("v3")) instead.
  • getMqttWsUrl returns the broker websocket URL for version, which defaults to "v2". When isOnEdge() is true it returns an unencrypted websocket URL: 192.168.5.2:9001 for "v2" and port 9001 on the current hostname for "v3". Otherwise, when the HMI's URL had an mqttPath query parameter starting with / when the SDK loaded, set by the host page, it returns wss://<the HMI's host><mqttPath>. Otherwise it returns getMqttWsUrlGivenUiIframeUrl(window.location.href).
  • getMqttWsUrlGivenUiIframeUrl derives the cloud broker URL from a remote iframe URL. It keeps the URL up to and including the digital-twin path segment, switches https: to wss:, then appends the path segment that follows passthrough and /mqtt: wss://<host>/.../digital-twin/<token>/mqtt.

Talking to the application's backend

These return plain strings to pass to fetch or to an RPC transport such as createConnectTransport({ baseUrl }) from @connectrpc/connect-web.

  • getExecutionEngineHttpUrl returns the execution engine's base URL: http://<current hostname>:3100 on the edge, otherwise everything in window.location.href before /machineCodeUi (the whole URL if that segment is absent).
  • getMachineCodeProcessHttpUrl takes a process name and returns <execution engine URL>/v1/machineCode/services/<processName>, the execution engine's proxy to one of the application's processes.
  • getMachineCodeApiBaseUrl returns getMachineCodeProcessHttpUrl(processName) followed by /api.
import { getMachineCodeApiBaseUrl } from "@vention/machine-logic-ui-sdk"

export async function fetchStatus(): Promise<unknown> {
  const response = await fetch(`${getMachineCodeApiBaseUrl("my-backend")}/status`)
  return response.json()
}

"my-backend" stands for the name the application's backend process is registered under, and /status for a route that backend serves.

Touch screens and the control center

VirtualKeyboard is an on-screen keyboard component with no props. Render it once, near the root of the layout. It listens for focus on any input in the document and shows itself about 150 ms later; it hides when the user presses its submit key, or taps outside both the keyboard and any input. It renders nothing while hidden. While visible it is a position: sticky panel, 45% of the viewport tall, at the bottom of its parent, and it scrolls the focused input into view.

  • A text layout covers text, password, email, search, url, month, datetime-local, time, and <textarea>; a numeric layout covers tel, date, and week. Other input types get no keyboard.
  • type="number" is not supported: the keyboard logs a warning and changes the input to type="tel". Use type="tel" for numeric fields.
  • Values are written through the native value setter and followed by input events, so React onChange handlers fire. A change event is dispatched on blur if the value changed.

isTouchScreenDevice returns true when "ontouchstart" in window or navigator.maxTouchPoints > 0. Gate the keyboard on it:

import { ReactNode } from "react"
import { VirtualKeyboard, isTouchScreenDevice } from "@vention/machine-logic-ui-sdk"

export const AppLayout = ({ children }: { children: ReactNode }) => (
  <div style={{ display: "flex", flexDirection: "column", height: "100vh", overflow: "auto" }}>
    <main style={{ flexGrow: 1 }}>{children}</main>
    {isTouchScreenDevice() ? <VirtualKeyboard /> : null}
  </div>
)

isOnEdge returns true when window.location.hostname starts with 192.168 or localhost, meaning the HMI is served from the machine's network rather than through the cloud. The URL helpers above branch on it. A dev server on localhost also counts as the edge, which is why standalone development passes MqttProvider an explicit url.

The HMI runs in an iframe. These post a message to window.parent for the host page to act on:

  • closeCustomUi posts "closeCustomUi", which asks the host to close the HMI.
  • navigateControlCenter posts "navigateToRemoteSupport" for "remoteSupport" or "navigateToControlCenterHomepage" for "controlCenterHomepage", which moves the host to the remote support page or the control center homepage.
import { closeCustomUi, navigateControlCenter } from "@vention/machine-logic-ui-sdk"

export const ExitBar = () => (
  <nav>
    <button onClick={() => navigateControlCenter("remoteSupport")}>Get help</button>
    <button onClick={closeCustomUi}>Exit</button>
  </nav>
)

Outside an iframe, window.parent is the page itself, so these messages reach nothing unless the page listens for them.