应用蓝图
本页介绍如何构建 IronFlock 应用,让它按照平台的设计初衷来使用平台。一句话概括其思路:您的容器运行在每台设备上,并把应用的全部状态保存在应用的表中;仪表板是应用的用户界面;两者从不直接通信。设备自身提供的 Web 界面经由远程访问隧道提供给用户,并显示在仪表板中。
各个组成部分在各自的页面中有详细介绍。本页说明它们如何组合在一起。
应用的组成部分
一个应用同时运行在许多设备上,这些设备通常是机器旁边的工业 PC。应用由以下部分组成:
| 组成部分 | 说明 | 定义位置 |
|---|---|---|
| 边缘代码 | 安装了该应用的每台设备上的一个容器或一个 Docker Compose 堆栈 | Dockerfile 或 docker-compose.yml |
| SDK | 您的代码与平台之间唯一的纽带:表、实时更新、设备函数、文件 | IronFlock SDK(Python、JavaScript、Go) |
| 表 | 应用的状态,存放在项目自己的数据库中 | data-template.yml |
| 仪表板 | 应用的核心用户界面,实时绑定到表,面向所有设备 | board-template.yml |
| 参数 | 单台设备或设备组的设置 | env-template.yml |
| 远程访问 | 通往容器所提供端口(例如 Web 界面)的隧道 | port-template.yml |
| AI 智能体 | 供 IronFlock 助手使用的智能体,以您的设备函数作为工具 | ai-template.yml |
当用户在项目中安装您的应用时,IronFlock 会在该项目的数据库中创建应用的表,安装应用的仪表板,并开启一个私有消息域,把应用在所有设备上的容器、应用的表和仪表板实时连接起来。SDK 使用设备代理注入的身份加入该消息域,因此您的代码既不需要地址,也不需要凭据。
规则
- 表就是应用的状态。 仪表板、其他设备、重启后的容器或 AI 智能体所需要的一切,都存储在表中。容器只保留能够从表中重建的内容,例如其设置在内存中的副本。
- 各部分通过表交流,而不是直接相互通信。 设备写入它测量到的数据和它所处的状态,仪表板将其显示出来。仪表板写入用户做出的决定,设备据此执行。双方都不调用对方,因此设备离线时仪表板照常工作,没有人打开仪表板时设备也照常工作。
- 每张表只有一个写入方。 每张表要么由设备写入(每台设备只写关于自己的数据),要么由仪表板写入,要么由某一个服务写入。请在表的
description中注明是哪一种。如果同一批行有两个写入方,它们会互相覆盖对方的更改。 - 期望状态,而非命令。 要改变设备的行为,就把目标值(限值、设定值、配方、时间表)写入表中,并让每台设备采用最新的一行。离线的设备会在恢复连接后采用它,而采用两次也不会产生任何变化。
- 变化时写入,实时跟踪,绝不轮询。 当实体的某个值发生变化时写入该实体的行,测量值则按其采样率写入。所有读取方都实时跟踪表:仪表板的绑定、设备上的表订阅、设备 Web 界面中的 WebSocket。用定时器反复读取表,永远是错误的设计。
- 仪表板就是用户界面;设备的 Web 界面是对它的补充。 只有仪表板无法展示的内容,才需要由设备提供网页:机器自带的 HMI、视频、高刷新率的实时视图、机器旁的操作员屏幕。仪表板会将其嵌入。
各类数据的存放位置
| 数据 | 存放位置 | 写入方 | 读取方 |
|---|---|---|---|
| 随时间变化的读数 | 带 tsp 的表 | publishToTable、publishRowsToTable | 带时间窗口和聚合的图表 |
| 设备、机器或订单的当前状态 | 实体表(maintainLatestFlagFor) | appendToTable:一行,包含键、tsp 和发生变化的列 | latest 过滤条件 |
| 用户设置的限值、设定值、配方 | 由仪表板表单写入的表 | 表单 | 每台设备,方式见下文 |
| 用户创建、编辑和删除的记录 | 带 deleted 列的实体表 | 表单,删除时设置该标记 | 仪表板上的列表和编辑页面 |
| 面向操作员的错误和通知 | error-logs 表 | 带 userMessage 的 reportError | 仪表板的提示框组件 |
| 图片、文档、录制文件 | 应用的文件存储 | files.put | 存放在列中的文件 URL,由组件显示 |
| 密码和 API 密钥 | secret: true 列 | 任意表写入 | revealSecrets、verifySecret,仅限在应用自己的容器中 |
| 单台设备的安装配置:机器地址、串口 | 应用参数 | 设备或设备组的应用设置 | 环境变量 |
| 应用各部分之间、事后无人需要的消息 | 您自己的主题 | publish | subscribe |
| 需要某台设备立即应答的请求 | 设备函数 | registerDeviceFunction | callDeviceFunction、组件操作、AI 工具 |
追加还是发布? appendToTable 会等待正在重启的数据后端,并在数据后端拒绝某一行时告知您。publishToTable 只等待连接,因此在数据后端重启期间发布的行会丢失。状态变化和记录请用追加,高频读数请用发布。
用参数还是用设置表? 参数属于某台设备或某个设备组:安装或配置设备的人在应用设置中设定参数,您的代码从环境中读取它们。参数适用于因安装而异的内容。用户在应用运行期间更改的内容——限值、配方、时间表,以及任何适用于多台设备的内容——应放在由仪表板写入的表中。仪表板是用户使用应用的地方,每台设备都会立即跟随更改,而表会保留谁在何时更改了什么的历史记录。
跟踪仪表板写入的内容
设备分三步采用仪表板写入的状态(例如在仪表板上设置的限值):
- 读取表之前先订阅它,这样读取与订阅之间就不会漏掉任何行。
- 启动时使用
latest过滤条件读取一次最新的一行,并采用它。 - 只采用比当前生效的行更新的行(按
tsp判断)。在程序启动的那一刻保存的行,可能既通过读取到达,又通过订阅到达。
Python
await ironflock.subscribe_to_table("settings", lambda row, *args, **kwargs: apply(row))
rows = await ironflock.getHistory("settings", {"limit": 1, "filterAnd": [{"latest": True}]})
for row in rows or []:
apply(row)仅此而已,对会离线的设备也是如此。连接由 SDK 负责,您的代码无需操心:它会重连,恢复您的订阅和设备函数,然后按从旧到新的顺序,向每个表订阅传递设备离线期间存储的行,之后才是实时到达的行。设备离线期间保存的限值,会在设备恢复连接后立即送达。补发依据 tsp 进行:它传递比您的处理函数所收到的最新一行更新的行,最多为最新的 10,000 行。补发的读取与其他历史读取一样,会在重连窗口内等待数据后端;读取失败时,这些行不会被传递,并会记录一条警告。补发功能自 SDK 1.10.0 版本起提供。
命令、设备函数与主题
优先使用状态。 用户所谓的“命令”,大多其实是期望状态:设定值、开或关、配方。请像上文那样将它们写入表中。
必须只执行一次的命令(例如打印标签或启动清洗周期)是仪表板所写表中带有 id 的行。设备会在它自己的一张表中记录已执行的 id,因此同一条命令即使收到两次,也只会执行一次。
设备函数是设备通过 registerDeviceFunction 注册的过程。当调用方需要设备立即给出应答时,请使用设备函数:
- AI 智能体:
ai-template.yml中的工具会在助手选定的设备上调用该函数。参见 AI 智能体与工具。 - 仪表板上的开关组件和 SCADA 组件会在设备在线时调用它们。
- 您自己的容器和设备通过
callDeviceFunction相互调用。
设备函数应返回可读的文本或小型 JSON;预期内的失败(例如机器已关机)应作为结果返回,而不是抛出错误。
主题用于在您自己的各部分之间传递事后无人需要的消息:在您自己命名空间内的名称(例如 myapp.events)上使用 publish 和 subscribe。仪表板无法订阅主题:它显示的一切都必须在表中。设备离线期间发布的消息不会被存储,因此凡是必须送达的内容,都请使用表。
设备上的容器
设备代理为您的容器提供什么,又对它有什么要求:
- 身份:设备代理会设置
DEVICE_KEY、DEVICE_NAME、DEVICE_SERIAL_NUMBER、SWARM_KEY、APP_KEY、APP_NAME、ENV(DEV或PROD)、TUNNEL_DOMAIN、DEVICE_LAN_IP和DEVICE_PORT_FOR_<port>,以及供 SDK 使用的应用自身凭据。SDK 会读取它需要的值;切勿硬编码其中任何一个值。 - 参数:生产安装会获得用户设置的值,其余参数取各自的
defaultValue。开发运行只获得默认值。请为参数取具体的名称(用PLC_HOST,而不是HOST)。 - 存储:单镜像应用会获得
/data(该应用私有,在重启和更新后仍会保留,随应用一起删除)和/shared(由设备上的所有应用共享)。即便如此,也请将应用的状态保存在表中,这些文件夹只用于存放属于该设备的文件,例如下载的模型或缓存。 - 硬件:单镜像容器以特权模式运行,并可访问设备的
/dev。 - 日志:程序写入 stdout 和 stderr 的所有内容,就是该应用在 IronFlock 中的日志。请以无缓冲方式输出(
python -u)。 - 启动:容器启动 30 秒后必须处于运行状态,否则本次启动视为失败。
- 崩溃:生产应用退出后,设备代理会再次启动它,且每次失败后等待的时间都会更长。开发运行不会被重启。请在程序中处理预期内的失败(例如传感器无应答),并在遇到无法恢复的错误时让程序退出。
- 停止:您的程序是容器的主进程(PID 1),而 PID 1 进程除非自行处理停止信号 SIGTERM,否则会忽略该信号。请在收到 SIGTERM 时退出,否则停止应用时会一直等到 Docker 强制终止它。请以 exec 形式启动程序(
CMD ["python", "-u", "main.py"]),而不要通过 shell 启动。 - 开发运行:从云端 IDE 运行应用即为开发运行(
ENV为DEV)。开发运行写入应用的开发表,获得参数的默认值,并且没有远程访问隧道。
Docker Compose
当应用运行多个进程,或在您自己的代码之外捆绑第三方服务(例如 MQTT 代理或数据库)时,请使用 docker-compose.yml。基础知识参见 Docker 与 Compose。与单镜像相比:
- 每个服务都会获得注入的环境。 设备代理会为每个服务提供一个包含上述变量的独立环境文件;您自己编写的
env_file:会被替换。Compose 文件中的${VAR}会根据注入的值进行解析。 - 没有
/data。 服务不会获得应用文件夹。请将文件保存在命名卷中:命名卷在停止和更新后仍会保留,并在应用从设备上移除时被删除。 - 发布的端口会被重新映射。 每个发布的端口都会在设备的所有网络接口上获得一个空闲的主机端口,
DEVICE_PORT_FOR_<port>会告诉您的代码实际的端口。在port-template.yml中,请使用 Compose 文件里所写的主机端口。只发布用户需要访问的端口,服务之间则通过 Compose 网络按服务名相互通信。写成${VAR}的主机端口不受管理:它既不会被重新映射,也没有隧道。 - 不会额外添加任何内容。 设备代理不会授予 Compose 服务任何特权,也不会为其映射任何硬件设备。请自行声明
privileged、devices或device_cgroup_rules。devices:条目指定的硬件必须存在,否则容器无法启动;而device_cgroup_rules配合/dev卷可以在所有设备上使用。 - 不支持
profiles:。 设置了 profiles 的服务会被排除在版本之外。 - 同样的启动规则。 30 秒内,每个容器都必须处于运行状态或已以退出码 0 退出,并且至少有一个容器必须在运行。请为长期运行的服务设置
restart: unless-stopped。
设备上的 Web 界面
容器中的 Web 服务器可以通过其端口的远程访问隧道从任何地方访问,并由仪表板显示:
- 在固定端口上提供服务(绑定到
0.0.0.0),并在port-template.yml中以protocol: http声明该端口。生产安装会开启隧道,并通过 HTTPS 提供服务,WebSocket 也包含在内。 - 将隧道的 URL 写入表中。 SDK 的
getRemoteAccessUrlForPort(port)会给出该 URL。请在启动时将其写入设备表中该设备的那一行。只有生产安装才有隧道,因此开发运行不会写入 URL。 - 将其嵌入仪表板。 在按设备过滤的设备页面上,将嵌入组件的 URL 绑定到该列。在 UI Builder 中,这与其他任何数据绑定并无二致。
关于页面本身:
- 谁可以打开它:http 隧道是私有的。只有登录 IronFlock 且有权访问该设备的用户才能打开它,除非有用户将该端口切换为公开。在设备所在的网络上,该页面对所有人开放,因此不要在页面上显示工厂网络不应看到的任何内容,并且凡是会改变机器状态的操作,都要求通过您自己的登录验证。
- 相对 URL:加载资源、调用您的 API 以及打开 WebSocket 时,都使用相对于页面的地址,这样页面无论是在设备网络中、通过隧道,还是在反向代理之后,都能正常工作。
- 推送,绝不轮询:通过 WebSocket 发送更新。
- 浏览器中不放平台凭据:页面与容器中它自己的服务器通信,由该服务器持有 SDK 连接。
如果要显示的不是您自己的页面,而是工厂网络中某台机器的 Web HMI,请从您的容器对其进行反向代理。参见嵌入外部 HMI。
多台设备
- 每一行都带有所属设备:IronFlock 会把写入某一行的设备的键存储在
device_key中。按设备划分的表请以device_key为键,用path: kwargs.DEVICE_NAME将设备名称存入一列,并按该名称拆分图表。 - 设备列表和单设备页面:用设备表构建侧边导航,再配一个适用于所有设备的页面,该页面按路由中的设备过滤其组件。参见构建仪表板。
- 按设备或按机器的设置:用一个指明目标的列作为设置表的键(仪表板写入的行本身不带设备信息),并在目标页面的表单中填写该列。每台设备采用其自身键对应的最新一行。
- 离开的设备会保留其行。如果用户应能将其从列表中移除,请为设备表添加
deleted列,并为仪表板添加一个设置该列的表单。
检查清单
- 所有状态都在表中,每张表的描述中都注明了唯一的写入方,并且代码写入的每一列都已在
data-template.yml中声明。 - 每一行都有
tsp,状态行在发生变化时写入,且只包含键和发生变化的列。 - 按上述三个步骤跟踪仪表板写入的状态,使用 SDK 1.10.0 或更高版本,且代码从不处理连接或其会话。
- 长时间运行的循环中的表写入,会处理平台持续一分钟无法访问时写入所引发的错误;预期内的失败则通过
reportError并附带userMessage进行报告。 - 程序在收到 SIGTERM 时退出,以 exec 形式启动,并以无缓冲方式输出日志。
- 没有任何轮询:仪表板靠绑定,设备靠订阅,Web 界面靠推送。
- 应用提供的每个端口都已在
port-template.yml中声明,其 URL 来自getRemoteAccessUrlForPort并写入表中,仪表板再从该表中将其嵌入。 - Compose 应用中没有
env_file:、没有profiles:,也没有${VAR}形式的主机端口,文件保存在命名卷中,并且只发布用户需要的端口。
从入门应用开始
Claude Code 的 IronFlock 插件包含一个遵循此蓝图的入门应用:一个在每台设备上读取机器数据的程序,它将每台设备的状态和读数保存在表中,采用用户在仪表板上设置的限值,并提供一个实时视图,由仪表板嵌入到每台设备的页面中。Claude Code 会以它为基础创建新应用,并根据您的用例进行调整。