@csllc/iot-utilities
v2.19.1
Published
NodeJS Utilities for managing IOT devices
Keywords
Readme
iot-utilities
Instructions and utilities for using the Control Solutions IOT features.
This package can be used to define Typescript types and utility functions in javascript-based applications, or when run as a NodeJS command line utility, to simulate cart(s) and/or an MQTT listener for testing purposes.
Command Line Utilities
Note: In order to use the command-line utilities, you will need the appropriate credentials for accessing the service. This package is primarily a development/test tool, so you probably already know this.
The package has been tested with the version of NodeJS contained in the .nvmrc file. The package is intended to work under similar versions of NodeJS and in other Javascript environment, but you should add validation tests if you need that.
Installation
With an appropriate NodeJS version active,
For local installation:
Clone the github repo, and navigate to the folder using the terminal/command prompt
npm i
Configuration
A file called sim.json must exist in the working folder. This file contains configuration variables, as well as paths to other configuration files. The contents of the file are documented by the SimConfig interface. In summary the file contains
- The connection information to the MQTT server under the 'client' key
- Information about all devices to be simulated under the 'devices' key
- A path to the config folder, which contains additional files needed for certificates, simulated device configuration, etc
A simple example of a sim.json file would be:
{
"CONFIG_DIR": "./config",
"devices": [
{
"id": "<the unique ID for the device",
"ee": "<path to motor controller configuration file",
"serial": "<specify the serial number of the simulated motor controller>",
"fence": "<path from CONFIG_DIR to a .geojson file specifying the geofencing boundary",
"cert": "<path from CONFIG_DIR to the certificate used by the device",
"key": "<path from CONFIG_DIR to the key used by the device"
"disable": false
}
],
"client": {
"hostName": "<MQTT endpoint",
"port": 8883,
"name": "Client",
"cert": "<path from CONFIG_DIR to the certificate used by the listener",
"key": "<path from CONFIG_DIR to the key used by the listener"
}
}
Commands
Simulation Listener
The listener acts as an MQTT client, and subscribes to the relevant device messages. The listener can either listen to ALL devices (which could be a lot of messages), or you can have it focus on a specific device (which is useful if you are working with a broker shared by several developers). When you manage only a single device, you have additional options such as sending commands to the device.
To listen to all clients:
npm run listen
To listen and manage client 'test-1':
npm run mange test-1
When managing a device, you can press keys to trigger certain actions. For example, 'h' sends a horn command to the unit. 'b' sends the boundary file for that unit.
Simulator
The Simulator pretends to be one or more remote devices, and sends MQTT messages to the server.
Each device to be simulated must be specified in the sim.json file and have a unique ID (eg corresponding to a Thing name)
To run the simulation, use the npm run sim command. The terminal will output the messages sent to the MQTT broker.
NPM Package
To use the utilities in your project:
npm install @csllc/iot-utilities
Messages
A set of messages are defined which are used to transfer information to and from the mobile device. For example, DeviceState represents a report of the device's current state.
The properties of each message are documented in the src/message/... file, and the message can be converted to its binary form using the toBytes() member function, and decoded from binary using the fromBytes() member function.
The binary form of the message is used to send or receive the message via MQTT.
Inbound Compatibility (Legacy + v13)
Inbound status decoding supports both legacy payloads and v13 payloads.
- Shared topics with dual decoding:
a/s/id/{thingId},a/s/state/{thingId},a/s/charge/{thingId},a/s/discharge/{thingId} - Legacy-only inbound topic:
a/s/diag/{thingId} - v13-only inbound topics:
a/s/iot/{thingId},a/s/signal/{thingId},a/s/geo/{thingId}
Use decodeInboundStatus(topic, payload) to route and decode inbound status messages with the correct parser automatically. Existing direct fromBytes() usage on legacy payloads remains supported.
For v13 traffic, state and location are intentionally split. DeviceState / a/s/state/{thingId} does not include embedded GEO data, while DeviceGeo / a/s/geo/{thingId} carries the current location and boundary fields. Legacy DeviceState payloads still include their embedded geo block.
The Magliner / GENERIC_BOARD STATE contract follows Drew Yaeger's
mqtt_msgs.h from October 14, 2026. It uses a 134-byte payload with version 2
at byte 2. Amigo's existing 86-byte STATE format remains supported, along
with the previously supported 67- and 89-byte variants and original legacy
protocol. Layout selection uses payload length and the version marker.
When constructing a new Magliner message, set v13Layout = 'magliner',
client, version = 2, and all four temperature readings. Encoding emits only
the current Magliner format; older 126-byte messages remain readable, with
temperatures absent. Set real temperature readings before re-encoding an older
Magliner message; missing values are rejected instead of written as zero.
DeviceState.chargeHistory includes totalForwardDistance1,
totalReverseDistance1, totalForwardDistance2, totalReverseDistance2,
totalMotorPowerUsedLifetime, and totalMotorPowerUsedMonthly. Distance
counters are feet traveled, tracked separately for motors 1 and 2. totalQuickStopTime and
totalInOutTime are time counters; totalInOutTime corresponds to the
firmware field totalIndoorTime.
The state also exposes power, powerLastHourOfUse, timeLastHourOfUse,
powerToday, and powerMaxToday directly, including in JSON output. Power
values are in watts and timeLastHourOfUse is in seconds, as confirmed for
the Magliner firmware. These values require no scaling. The serial number begins at byte 106, the power fields
occupy bytes 110–121. motor1Temperature, motor2Temperature,
motor1HeatsinkTemperature, and motor2HeatsinkTemperature are signed
little-endian int16 Celsius readings at offsets 122, 124, 126, and 128.
They are exposed in JSON without scaling. Bytes 130–133 are unused union storage
and are zero-filled on encoding. No undocumented sensor-error sentinel is inferred. These offsets follow the packed C field types; some comments in
the supplied header have stale offsets.
Drew confirmed S4006408 as the test unit. Live 126-byte STATE messages
from that unit and 86-byte Amigo messages from S5100002 are covered by
captured regression fixtures.
GEO calculated action and reserved bytes
Starting with utilities 2.18.4, DeviceGeo exposes the calculated boundary action
reported by firmware 9.5.2 before its tolerance filter. boundaryAction remains
the current action in effect. Both use BoundaryAction: None = 0, Warning = 1,
Stop = 2, and Alarm = 3. Utilities reports these values as received; firmware
applies the GEO configuration mask and tolerance.
The payload remains 22 bytes, including the two-byte client prefix. These are zero-based offsets in the MQTT payload:
| Offset | Property | Wire type |
| --- | --- | --- |
| 18 | boundaryAction | uint8 |
| 19 | calcBoundaryAction | uint8 |
| 20–21 | reserved | Two uint8 values, in wire order |
fromBytes() reads all three trailing bytes and toBytes() preserves their
values. They appear in decoded objects, JSON, and listener output. GEO CSV output
appends CalcBoundaryAction,Reserved0,Reserved1 after BoundaryAction; consumers
that require an exact column count must accept the three added columns.
For example, ff00018023d619f52605ce0a82000000000000010000 decodes to
boundaryAction: 0, calcBoundaryAction: 1, and reserved: [0, 0].
New instances default the added fields to zero. Older firmware payloads with
zero-filled reserved bytes still decode, but a zero calculated action alone
cannot distinguish an unavailable calculation from BoundaryAction.None.
The legacy DeviceState.geo format is unchanged.
Boundaries
The BoundaryConfig message class defines a geofencing boundary. A BoundaryConfig contains some configurable properties, as well as a set of polygons that define the array of Boundarys to be observed by the mobile device.
Each Boundary definition consists of some metadata and a set of vertices (corners). The metadata describes, for example, whether a boundary is a 'keep in' type or a 'keep out' type - essentially whether a violation occurs if the mobile device ventures OUT of a keep-in boundary, or INTO a keep-out boundary. The 'action' property defines what should happen if the boundary is violated. The available options are defined in the TypeScript - for example the action might be to simply flag a warning or stop completely.
For a BoundaryConfig message, the MAX_POLYGONS and MAX_POLYGON_CORNERS constants define the maximum number of Boundary(s) and maximum number of vertices (corners) for each Boundary. These limits must be respected in order to successfully encode and decode the message to the binary form.
GeoJSON Boundary Files
Utility functions are provided to convert BoundaryConfig messages to and from GeoJSON formatted files (https://geojson.org). Notice that the GeoJSON file uses custom properties to store the Boundary metadata and configurable properties.
The BoundaryFeature type describes the Features stored in the file (Boundary with metadata), and the BoundaryJson type defines the complete GeoJSON object including the config key which is a custom extension to store the geofencing configuration.
Development
File Converters
When installed in development mode, several file format converters are available. Examples are shown below; in the future these could be further integrated into the package to make them callable when used as a module.
Binary files can be viewed using an appropriate utility (eg hexdump on OSX).
geo2bin GeoJSON file to binary BoundaryConfig object the
-boption converts the output to Base64npx ts-node src/cli geo2bin -b Walmart_1863.geoJSON Walmart_2110.b64bin2geo Reverses geo2bin
npx ts-node src/cli bin2geo -b Walmart_1863.bin Walmart_1863.geoJSONiothex2bin IOT board dsPIC processor hex file to binary blocks The binary format is a series of 1540-byte blocks (a 4-byte little endian unsigned-32 starting address, followed by 1536 bytes of program FLASH data, equivalent to one writable page of FLASH memory). The
-bflag converts directly to Base64; omitting that flag converts to unencoded binary.npx ts-node src/cli iothex2bin -b CS2520_IOT.X.production.hex CS2520.b64plot2bin PLOT boundary file to binary BoundaryConfig object
npx ts-node src/cli plot2bin -b ./Downloads/Walmart_1863.plot Walmart_1863.b64
The legacy .plot format stores one keep-in boundary per section:
:BND1warning:BND2stop:BND3alarm (optional extension)
If you need keep-out boundaries or multiple polygons for the same action, use GeoJSON instead.
bin2b64 Binary file to Base64 (encoder)
npx ts-node src/cli bin2b64 CS2520.bin check.b64b642bin Base64 to binary file (decoder)
npx ts-node src/cli b642bin CS2520.b64 check.bin
Making changes
When modifying this package, the recommended editor is Visual Studio Code with TypeScript and ESLint support.
Debug logging is provided by the debug npm package. For instance, to enable debugging of the SimCart (simulator), you would set the DEBUG environment variable to iot:SimCart. On Mac or Linux this would be like export DEBUG=iot:SimCart.
Keep in mind setting this environment variable is platform-specific. You might also set it programmatically in ./src/cli.ts for convenience.
When coding wordart, eg:
// ┌─┐┌┬┐┬ ┬┌─┐┬─┐
// │ │ │ ├─┤├┤ ├┬┘
// └─┘ ┴ ┴ ┴└─┘┴└─
was done using the kit NPM package exclaim command.
npm install -g kit
then
`kit exclaim --font='Calvin S' "Hello World"
Testing
mocha-based tests are provided and must be updated to reflect any code changes.
To run all the tests, use the npm test command.
