@vention/machine-logic-ui-sdk

中文文档
0.47.6发布于 2026/11/03MITTypeScriptES Module
原始使用文档
文档目录5 节
  1. Live data over MQTT
  2. Typing topics
  3. The raw client
  4. Talking to the application's backend
  5. Touch screens and the control center

@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.

版本与发布记录

@vention/machine-logic-ui-sdk 共发布 230 个版本,首次发布于 2024/08/27;过去 12 个月发布了 186 个版本。

版本发布日期距今
0.47.6latest2026/11/03今天
0.47.52026/10/313 天前
0.47.42026/10/304 天前
0.47.32026/10/295 天前
0.47.22026/10/295 天前
0.47.12026/10/277 天前

其他发布通道:dev → 0.0.0-dev.4527.a3d343ca7。

关于 @vention/machine-logic-ui-sdk 的常见问题

如何安装 @vention/machine-logic-ui-sdk?

在项目目录运行 npm install @vention/machine-logic-ui-sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^17.0.2 || ^19.3.0)、@mui/material(^5.16.7)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@vention/machine-logic-ui-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@vention/machine-logic-ui-sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@vention/machine-logic-ui-sdk 可以商用吗?

@vention/machine-logic-ui-sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@vention/machine-logic-ui-sdk 还在维护吗?

最新版本 0.47.6 发布于 2026/11/03(今天);至今共发布 230 个版本,其中过去 12 个月发布了 186 个;首次发布于 2024/08/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。