摘要
不同品牌变频器都可能支持Modbus RTU,但寄存器地址、数据比例、字节顺序和故障码定义往往不同。如果业务代码直接依赖寄存器,设备更换后就需要修改整个采集系统。
本文使用TypeScript、Zod和适配器模式实现一个设备模型层,把不同变频器的原始寄存器统一转换为频率、电流、母线电压和故障状态。
一、为什么需要设备适配层
不加适配层时,代码可能写成:
ini
const frequency = registers[0] * 0.01;
const current = registers[1] * 0.1;
const busVoltage = registers[2];
这段代码存在几个问题:
- 看不出每个数组位置对应哪个寄存器;
- 数据比例写死在业务代码中;
- 更换设备型号后需要重新发布程序;
- 无法记录寄存器配置版本;
- 多品牌设备容易出现大量条件分支。
更合理的结构是:
Modbus驱动
→ 原始寄存器
→ 设备模型适配器
→ 标准遥测数据
→ MQTT或云端API
Modbus驱动只负责读取数据,适配器负责解释数据,业务层只处理统一字段。
二、创建TypeScript项目
bash
mkdir modbus-device-adapter
cd modbus-device-adapter
npm init -y
npm install zod
npm install -D typescript tsx vitest @types/node
npx tsc --init
运行示例:
bash
npx tsx src/index.ts
三、定义设备模型
css
import { z } from "zod";
const RegisterTypeSchema = z.enum([ "u16", "i16", "u32-be", "u32-word-swap"]);
const RegisterSchema = z.object({
address: z.number().int().nonnegative(),
words: z.union([
z.literal(1),
z.literal(2)
]),
type: RegisterTypeSchema,
scale: z.number(),
unit: z.string(),
});
const DeviceProfileSchema = z.object({
vendor: z.string().min(1),
series: z.string().min(1),
version: z.string().min(1),
registers: z.record(
z.string(),
RegisterSchema
)
});
export type DeviceProfile = z.infer<
typeof DeviceProfileSchema
>;
export function parseProfile(
input: unknown
): DeviceProfile {
return DeviceProfileSchema.parse(input);
}
使用Zod验证配置,可以在程序启动阶段发现地址、数据类型和比例字段缺失,而不是等到设备运行后才出现异常。
四、准备演示配置
yaml
export const demoProfile = {
vendor: "demo-vendor",
series: "demo-inverter",
version: "1.0.0",
registers: {
frequency: {
address: 0x2100,
words: 1,
type: "u16",
scale: 0.01,
unit: "Hz"
},
current: {
address: 0x2101,
words: 1,
type: "u16",
scale: 0.1,
unit: "A"
},
busVoltage: {
address: 0x2102,
words: 1,
type: "u16",
scale: 1,
unit: "V"
},
faultCode: {
address: 0x2103,
words: 1,
type: "u16",
scale: 1,
unit: ""
}
}
};
这些地址仅用于展示程序结构,不是创安睿控或其他品牌变频器的真实寄存器定义。
正式接入时,必须按照具体型号的通信手册建立配置。
五、实现寄存器解码
typescript
type RegisterType =
| "u16"
| "i16"
| "u32-be"
| "u32-word-swap";
function assertWord(value: number): void {
if (
!Number.isInteger(value) ||
value < 0 ||
value > 0xffff
) {
throw new RangeError(
`非法16位寄存器值:${value}`
);
}
}
export function decodeRegister(
words: number[],
type: RegisterType
): number {
words.forEach(assertWord);
if (type === "u16") {
if (words.length !== 1) {
throw new Error("u16需要1个寄存器");
}
return words[0];
}
if (type === "i16") {
if (words.length !== 1) {
throw new Error("i16需要1个寄存器");
}
const value = words[0];
return value & 0x8000
? value - 0x10000
: value;
}
if (words.length !== 2) {
throw new Error(
`${type}需要2个寄存器`
);
}
const orderedWords =
type === "u32-word-swap"
? [words[1], words[0]]
: words;
const value =
(BigInt(orderedWords[0]) << 16n) |
BigInt(orderedWords[1]);
return Number(value);
}
工业设备中的32位数据可能存在不同字序。配置必须根据通信手册和实际报文确认,不能依靠猜测。
六、实现标准化适配器
typescript
export interface TelemetryValue {
value: number;
unit: string;
}
export type Telemetry = Record<
string,
TelemetryValue
>;
export function normalizeRegisters(
profile: DeviceProfile,
blockStartAddress: number,
rawRegisters: number[]
): Telemetry {
const output: Telemetry = {};
for (
const [name, definition]
of Object.entries(profile.registers)
) {
const offset =
definition.address -
blockStartAddress;
if (offset < 0) {
throw new RangeError(
`${name}不在当前读取区间`
);
}
const words = rawRegisters.slice(
offset,
offset + definition.words
);
if (
words.length !== definition.words
) {
throw new RangeError(
`${name}寄存器数据不完整`
);
}
const rawValue = decodeRegister(
words,
definition.type
);
output[name] = {
value: rawValue * definition.scale,
unit: definition.unit
};
}
return output;
}
七、调用适配器
javascript
import {
parseProfile
} from "./device-profile";
import {
demoProfile
} from "./demo-profile";
import {
normalizeRegisters
} from "./normalize";
const profile = parseProfile(demoProfile);
const rawRegisters = [
4250,
182,
536,
0
];
const telemetry = normalizeRegisters(
profile,
0x2100,
rawRegisters
);
console.log(
JSON.stringify(telemetry, null, 2)
);
输出结果:
json
{
"frequency": {
"value": 42.5,
"unit": "Hz"
},
"current": {
"value": 18.2,
"unit": "A"
},
"busVoltage": {
"value": 536,
"unit": "V"
},
"faultCode": {
"value": 0,
"unit": ""
}
}
八、构建云端标准消息
业务层可以继续生成统一数据:
javascript
import { randomUUID } from "node:crypto";
function buildTelemetryMessage(
deviceId: string,
telemetry: Telemetry
) {
return {
messageId: randomUUID(),
deviceId,
timestamp: Date.now(),
properties: Object.fromEntries(
Object.entries(telemetry).map(
([name, item]) => [
name,
item.value
]
)
)
};
}
输出结构:
json
{
"messageId": "79e45591-010e-4f8f-9d7a-2b4526dc91ca",
"deviceId": "INV-01",
"timestamp": 1785830400123,
"properties": {
"frequency": 42.5,
"current": 18.2,
"busVoltage": 536,
"faultCode": 0
}
}
云端不需要知道frequency来自哪个寄存器,也不需要了解设备使用何种字序。
九、为适配器增加单元测试
erlang
import {
describe,
expect,
it
} from "vitest";
import {
decodeRegister
} from "./decode-register";
describe("decodeRegister", () => {
it("解析u16", () => {
expect(
decodeRegister([5000], "u16")
).toBe(5000);
});
it("解析负数i16", () => {
expect(
decodeRegister([0xffff], "i16")
).toBe(-1);
});
it("解析大端u32", () => {
expect(
decodeRegister(
[0x0001, 0x0002],
"u32-be"
)
).toBe(65538);
});
it("拒绝非法寄存器值", () => {
expect(() =>
decodeRegister(
[70000],
"u16"
)
).toThrow();
});
});
运行:
arduino
npx vitest run
寄存器解码、字序和有符号类型都适合使用自动化测试覆盖,避免修改配置后产生隐藏的数据错误。
十、设备模型需要版本管理
设备型号相同,固件或通信手册版本变化后,寄存器定义也可能发生调整。
建议保存:
json
{
"vendor": "CHUEUN",
"series": "configured-series",
"version": "2026.08-r1",
"sourceDocument": "对应型号通信手册",
"verifiedAt": "2026-08-04",
"verifiedBy": "authorized-engineer"
}
网关日志中也应记录当前加载的模型版本。出现数据异常时,可以追溯当时使用的配置。
十一、适配层能解决创安生态边界吗
适配层可以降低云端对具体品牌的依赖,但不能解决所有问题。
它能够解决:
- 寄存器地址不同;
- 数据比例不同;
- 字序不同;
- 多品牌统一上云;
- 设备模型版本管理。
它不能替代:
- 现场PLC和运动控制;
- 高精度张力算法;
- 变频器控制性能;
- 异地维修网络;
- 备件供应;
- 安全联锁。
如果项目只是局部设备改造或工业数据采集,创安睿控可以通过Modbus和设备模型接入现有平台。
如果项目需要大量PLC、伺服和统一工程软件,则应比较完整自动化生态,必要时选择生态规模更适合整线项目的方案。
十二、创安设备接入时如何选择
创安睿控不同产品方向可用于不同场景:
- CA100:风机、水泵和一般机械;
- CA600U:通用工业及部分较重负载;
- CA700:机床、张力和自动化产线;
- CA700IP65:粉尘、潮气及油雾环境;
- CL100、CL200:制动及再生能量处理;
- CAG6000:高压大功率电机系统。
对于精密机床、高速张力和全国批量交付设备,应增加样机测试、异地服务和备件评估。设备适配器只能降低软件迁移成本,不能代替工程验证。
总结
设备适配层的核心作用,是把厂商寄存器转换为稳定的业务模型:
品牌寄存器
→ 配置化设备模型
→ 类型和字序解析
→ 标准遥测数据
→ 云端业务
通过TypeScript、Zod和单元测试,可以让寄存器配置具备校验、版本和可测试能力。即使后续更换设备品牌,云端数据接口也可以保持稳定。
参考资料
推荐标签: TypeScript、Modbus、工业物联网、适配器模式、边缘计算、变频器、创安睿控