@vention / machine-logic-ui-sdk
用于MachineLogic应用程序的自定义HMI的浏览器助手,应用程序中的React应用程序 customui/ 挂件和控制中心在iframe中加载的文件夹。它为HMI提供了来自机器的MQTT代理的实时数据、执行引擎和应用程序自己的后端进程的url、用于触摸挂件的屏幕上键盘,以及关闭HMI或将控制中心移动到另一个页面的消息。将它安装在应用程序的确切版本上 customui/package.json:
npm install @vention/machine-logic-ui-sdk@<pinned-version>
人机界面需要什么?
- 反应17或19。包的
peerDependencies接受react^17.0.2 || ^19.3.0。SDK针对两个版本进行类型检查typecheck-react19目标);它的单元测试运行在React 17上。 @mui/material和@emotion/react。包条目导入tss-react/mui(用VirtualKeyboard),因此导入任何导出都会将其拉入。- 一个处理CSS导入的打包器,比如Vite。条目导入
react-simple-keyboard/build/css/index.css.
在平台-产品单线程中,也可以通过 @ventionco/machine-logic-ui-sdk 通过TypeScript路径别名。应用程序安装已发布的 @vention/machine-logic-ui-sdk.
每个导出的参数、prop和返回类型都在包的类型声明中,您的编辑器可以使用这些类型声明 tsc 阅读。在那里寻找签名;这个自述文件只涵盖了类型不能显示的行为。
MQTT上的实时数据
将应用程序包装一次 MqttProvider。它打开一个MQTT-over-websocket连接 useMqttSubscription 呼叫下面它共享,并重新连接自己。
version默认为"v2"并选择哪个代理地址派生,通过getMqttWsUrl。在边缘,两者都使用未加密的websocket:"v2"连接到192.168.5.2:9001和"v3"连接端口9001在服务HMI的主机上。在云框架中,两者使用相同的wss://URL:wss://<the HMI's host><mqttPath>当主机页面给出mqttPath查询以。开头的参数/,否则从iframe URL派生的URL。url自己设置代理websocket URL。如果设置了,version和URL检测被跳过。例如,将其用于独立开发ws://localhost:9001.
从 projects/machine-code/apps/ai-operator/frontend/src/main.tsx修剪:
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")
)
对于iframe之外的开发,您可以自己传递代理,例如使用 VITE_MQTT_WS_URL=ws://localhost:9001。如果不设置,提供者将返回到检测:
<MqttProvider version="v3" url={import.meta.env.VITE_MQTT_WS_URL}>
<App />
</MqttProvider>
阅读一个主题 useMqttSubscription。它返回关于该主题的最新消息,或者 undefined 直到第一个人到来。对每个主题进行一次代理订阅,因此只有第一个读取主题的组件才能获得保留的消息;稍后为同一主题挂载的组件将返回 undefined 直到下次出版。
topic必须是一个准确的主题。钩子将每个传入主题与此字符串进行比较===,所以通配符过滤器如operation/bins/+订阅代理,但从不更新返回值。使用MqttClient通配符。unsubscribeOnUnmount,默认关闭,在组件卸载时在代理上取消订阅。代理订阅是跨提供者为每个主题共享的,因此这也会为仍在读取该主题的任何其他组件停止该主题。每当连接状态改变时,清理也会运行,并且钩子在连接后再次订阅。- 每个有效载荷都通过
JSON.parse。在HMI读取的主题上发布JSON;无效JSON的有效负载会在消息处理程序中抛出。type参数是一个断言,而不是运行时检查。 - 没有订阅,直到提供者的
connectionStatus是"connected"。在重新连接之后,钩子再次订阅。
从 projects/machine-code/apps/ai-operator/frontend/src/app/contexts/operation-context.tsx修剪:
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 是提供程序填充的React上下文。阅读它 useContext(mqttContext):
client:供应商的 mqtt.jsMqttClient(不是这个包裹MqttClient类),或null在第一次连接之前。用它来发布。connectionStatus:提供者的连接状态。subscribedTopics,addSubscribedTopic,removeSubscribedTopic:供应商的记账useMqttSubscription。应用程序不需要它们。
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>
)
}
输入主题
这个 MqttPayloadByTopic Type在主题映射中查找主题的有效负载类型。将应用程序的主题声明为该映射:
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
原始客户端
MqttClient 是mqtt.js的一个小包装器,用于React之外的代码或通配符订阅。
- 构造函数立即连接。它的可选回调在每个成功的连接上运行,包括在重新连接之后。
connect是构造函数调用的。再次调用它会打开第二个连接;应用程序不需要这样做。subscribe(topic, regex, callback)订阅topic在代理(MQTT通配符)+和#是允许的)和呼叫callback对于主题匹配的每个传入消息regex.unsubscribe(topic, regex, callback)移除那个回调,并取消订阅topic在代理上,一旦没有剩余的回调使用相同的regex.publish发布字符串,除非传递,否则不保留retain。序列化对象JSON.stringify你自己。- 一、
Callback以字符串形式接收主题和原始有效负载;负载没有被解析。
在第一个连接打开之前进行的调用将排队,并在第一个连接打开后发送。关闭后,客户端在重新连接之前被断开约一秒钟;在这段时间里 publish 无声地落下 subscribe 永远不会到达代理,并且一旦重新连接,客户端不会重新发送在旧连接上进行的代理订阅。对于长期使用的React HMI,首选 MqttProvider 和 useMqttSubscription,重新连接后重新订阅。
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助手:
connectToMqttAndGetClient构建一个客户端await getMqttWsUrl(),也就是"v2"代理地址。它不需要争论;为"v3"构造new MqttClient(await getMqttWsUrl("v3"))代替。getMqttWsUrl返回代理websocket URLversion,默认为"v2"。当isOnEdge()为true,则返回一个未加密的websocket URL:192.168.5.2:9001用于"v2"和端口9001的当前主机名"v3"。否则,当HMI的URL有mqttPath以开头的查询参数/当SDK加载时,由主机页面设置,它返回wss://<the HMI's host><mqttPath>。否则它会返回getMqttWsUrlGivenUiIframeUrl(window.location.href).getMqttWsUrlGivenUiIframeUrl从远程iframe URL派生云代理URL。它将URL保持到并包括digital-twin路径段、开关https:来wss:,然后追加后面的路径段passthrough和/mqtt:wss://<host>/.../digital-twin/<token>/mqtt.
与应用程序的后端对话
它们返回要传递的普通字符串 fetch 或RPC传输,例如 createConnectTransport({ baseUrl }) 从 @connectrpc/connect-web.
getExecutionEngineHttpUrl返回执行引擎的基础URL:http://<current hostname>:3100在边上,否则一切都在window.location.href之前/machineCodeUi(如果该段不存在,则为整个URL)。getMachineCodeProcessHttpUrl接受进程名并返回<execution engine URL>/v1/machineCode/services/<processName>,执行引擎的一个应用程序进程的代理。getMachineCodeApiBaseUrl返回getMachineCodeProcessHttpUrl(processName)紧随其后的是/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" 表示注册应用程序后端进程的名称,和 /status 对于后端服务的路由。
触摸屏和控制中心
VirtualKeyboard 是一个没有道具的屏幕键盘组件。渲染一次,靠近布局的根。它监听文档中任何输入的焦点,并在150毫秒后显示出来;当用户按下提交键,或者在键盘和任何输入之外点击时,它隐藏起来。它在隐藏的时候什么也不呈现。可见时,它是 position: sticky 面板,视窗高度的45%,在其父面板的底部,它将聚焦的输入滚动到视图中。
- 文本布局包括
text,password,email,search,url,month,datetime-local,time,<textarea>; 数字布局包括tel,date,week。其他输入类型没有键盘。 type="number"不支持:键盘记录警告并将输入更改为type="tel"。使用type="tel"用于数字字段。- 值通过本机值设置器写入,后面跟着
input事件,所以ReactonChange处理程序。一个change如果值改变,则在模糊时分派事件。
isTouchScreenDevice 当 "ontouchstart" in window 或者 navigator.maxTouchPoints > 0。把键盘关在上面:
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 当 window.location.hostname 开始于 192.168 或者 localhost这意味着人机界面是通过机器的网络提供的,而不是通过云。上面的URL帮助器在它的基础上进行分支。一个开发服务器 localhost 这也是为什么独立开发会过时的原因 MqttProvider 一个显式的 url.
HMI在iframe中运行。这些帖子给 window.parent 主机页要执行的操作:
closeCustomUi的帖子"closeCustomUi",要求主机关闭HMI。navigateControlCenter的帖子"navigateToRemoteSupport"用于"remoteSupport"或者"navigateToControlCenterHomepage"用于"controlCenterHomepage",将主机移至远程支持页面或控制中心首页。
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>
)
在框架之外, window.parent 是页面本身,因此这些消息不会到达任何地方,除非页面侦听它们。