Migrating a Modbus device: tables: → points:
An existing brownfieldDevices.modbus connection written the original way —
raw tables: polled, decoded per-variable by hand-written JSONata in
mapping: — keeps working; nothing forces a rewrite. Migrate a connection
when you want the benefits points: grammar gives for free: address-token
validation at load instead of a typo silently reading the wrong register,
a poll plan the gateway derives instead of one you maintain by hand, and the
ability to import a vendor mapping file at all (import: only ever produces
points:, never tables:/mapping:).
Before and after
The two configurations below read the same two values off the same device.
brownfieldDevices:
modbus:
- name: MB1
type: TCP
address: 127.0.0.1
port: 8502
unitId: 1
tables:
- { type: holdingRegisters, start: 3000, length: 10, pollingRate: 100 }
mapping:
- node: /di:DeviceSet/own:MyObject/own:MyInt32
value: $modbusGetInt32BE("MB1", 3000)
brownfieldDevices:
modbus:
- name: MB1
type: TCP
address: 127.0.0.1
port: 8502
pollingRate: 100
expose:
parent: /ua:Objects/di:DeviceSet
object: MyObject
points:
- name: MyInt32
at: holding:3000
type: int32
endianness: ABCD # BE ≡ ABCD — see the endianness table on the main Modbus page
Point by point, translating the old shape:
- Drop
tables:.points:derives its own poll plan from the address tokens andpollingRates it finds — nothing to hand-maintain, and nothing to keep from silently drifting out of sync withmapping:the way a hand-written table range could. - Give the connection an
expose:. This is new:tables:/mapping:named the destination node explicitly on everymapping:entry (node: /di:DeviceSet/own:MyObject/own:MyInt32);points:instead projects each point underexpose.parent/own:<object>/own:<pointName>by default.expose.objecthere (MyObject) reproduces the old path exactly; omit it to default to the connection name instead. - Turn each
mapping:entry into a point. Read the JSONata expression for what it decodes:modbusGetInt32BE("MB1", 3000)is addressholding:3000, typeint32, layoutABCD(BEmeansABCD— see the endianness table on the main page for the others). AmodbusGetFloat32(..., "CDAB")call becomestype: float32, endianness: CDAB; ascale/offsetformula ($mb.value / 10.0 + 3.14) becomesscale: 0.1, offset: 3.14directly on the point. - Anything that does not translate stays a
value:escape hatch. A formula reading more than one register/table, or doing somethingscale/sqrtRange/scaleRegistergenuinely cannot express, is still legal JSONata — just move it onto the individual point's ownvalue:field (same syntax, see The escape hatch on the main page) instead of a connection-widemapping:entry. Migrate what translates cleanly; leave the rest exactly as it was.
Doing it a point at a time
A connection does not have to move in one step. points: and tables:
coexist on the same connection (the loader keeps both working), and mapping:
entries for variables the migrated points now also publish simply become
redundant — remove them once their points: equivalent is confirmed working,
not before.
Verifying the result with dump-config
However a connection got its points — hand-written, migrated, imported,
overridden — dump-config prints what the gateway actually derived from the
configuration: the poll plan it built, the projected nodes, and the exact
generated binding expression for every point, with points:/import:
themselves stripped back out (what's left is what a tables:+mapping:
config would have had to spell by hand):
node_modules/.bin/opcua-omni-edge dump-config -c config.yaml
The output opens with a banner marking it a debug artifact — do not deploy
or edit it as a configuration (secrets and certificates are redacted, and
derived: true markers are stripped so a pasted-back copy reads as
hand-written, which it is not). Read it to answer exactly the two questions a
migration raises:
- "What poll plan did the gateway derive?" — check the connection's
tables:in the dump against what you expected from the address tokens and polling rates you wrote. - "What does this point actually evaluate to at runtime?" — check the
generated
mapping:entry for the point's binding expression; compare it, argument for argument, against the hand-written JSONata it replaced.
If the two don't match — a CDAB where you expected ABCD, a
scaleRegister firing you didn't set — the mismatch is almost always the
endianness table
being read backwards (BE/LE axis vs. word-swap axis are NOT the same
question) or expose.object differing from the old hand-written node:
path. dump-config exits non-zero with the diagnostics on stderr if the
configuration itself is invalid — fix those before comparing anything.
Two related commands, useful while migrating a larger fleet:
opcua-omni-edge migrate-config config.yaml— a DIFFERENT migration: the v3→v4 schema shape (nodesets:→namespaces:+imports:,instance:→instances:). Unrelated totables:/points:; run it first if the configuration predates v4 at all, before touching Modbus.opcua-omni-edge convert-mapping <file> --write config.yaml --device MB1— up-converts a vendor mapping FILE (not an existingtables:/mapping:connection) straight intopoints:; the CLI form of the editor's "Convert" button. See Link vs. Convert on the main page.