string.length
string.lenght -> number
Get the length of a string
Returns
number
string length (bytes)
Example
let a = "Hello World";
print( a.length ); // 11
Scripting Language Manual
Flexs Q5 with modular IO cards
Covers firmware V11 and later
Generated 2026-07-26
The mJS scripting language, and the methods carried by strings, numbers and arrays.
What the embedded mJS interpreter supports — a strict subset of ES6 designed for microcontrollers.
Scripts run on mJS, a JavaScript engine designed for microcontrollers with limited resources. Its main design goals are a small footprint and simple C/C++ interoperability — on 32-bit ARM the engine takes about 50 kB of flash and less than 1 kB of RAM. mJS implements a strict subset of ES6:
String, Number, RegExp, Date,
Function, etc. — but the byte-level string, number and array methods
documented in this category are built in, and JSON.parse() /
JSON.stringify() are available.new. To create an object with a custom prototype use
Object.create(), which is available.var, only let.for..of, arrow functions (=>), destructuring, generators, proxies
or promises.valueOf, prototypes, classes or template strings.== or != — only === and !==.mJS strings are byte strings, not Unicode strings:
'ы'.length === 2, 'ы'[0] === '\xd1', 'ы'[1] === '\x8b'. This means an
mJS string can represent any binary data chunk — which is exactly how the
bin buffer functions use them.
Byte-level methods carried by every string: slice, at, indexOf — plus the chr() global for building strings from byte values.
Every mJS string carries these built-in methods. Remember that mJS strings are byte strings (see The mJS Language): indices count bytes, not Unicode characters, and strings may hold arbitrary binary data.
string.lenght -> number
Get the length of a string
number
string length (bytes)
let a = "Hello World";
print( a.length ); // 11
string.number() -> number
Converts a string to a numeric value — the inverse of `number.toFixed()` and `string()`. (Global function.)
Converts a text string into a number type variable
Wrapper to the strtod c function.
number
Number value of string
let text = "123.456";
let number = text.number();
print( number ); // 123.456
'some_string'.slice(start, end) → string
Returns a substring between two indices.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| start | number | yes | — | Start index (inclusive). |
| end | number | yes | — | End index (exclusive). |
string
The substring.
'abcdef'.slice(1,3) === 'bc';
'abc'.at(index) → number
Returns the numeric byte value at a string index.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| index | number | yes | — | Byte index into the string. |
number
The byte value at that index.
'abc'.at(0) === 0x61;
'abc'.indexOf(substr, [fromIndex]) → number
Returns the index of the first occurrence of a substring, or −1 if not found.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| substr | string | yes | — | Substring to search for. |
| fromIndex | number | optional | 0 | Index to start searching from. |
number
Index of the first occurrence, or −1 if not found.
'abc'.indexOf('bc') === 1;
chr(n) → string | null
Returns a 1-byte string whose byte value is n — the inverse of string.at(). (Global function.)
Builds a single-byte string from a numeric value. If n is not numeric or is
outside the 0–255 range, null is returned.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| n | number | yes | — | Byte value, 0–255. |
string | null
A 1-byte string, or null for invalid input.
chr(0x61) === 'a';
Methods carried by every number: range mapping, limiting and fixed-precision formatting — plus the number() global for parsing strings.
Every mJS number carries these built-in methods — they chain naturally for sensor scaling pipelines:
let waterLevel = 9; // feet
let pct = waterLevel.map(0, 18, 0, 100, true) // scale 0–18 ft → 0–100 %
.toFixed(1); // "50.0"
x.map(in_min, in_max, out_min, out_max, [limit]) → number
Linearly scales the number from one range to another, with optional clamping to the output range.
The method form of Math.map() — maps the value from the input range to the
output range. The optional limit argument clamps the result to the output
range; if not specified the output is not limited.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| in_min | number | yes | — | Lower bound of the input range. |
| in_max | number | yes | — | Upper bound of the input range. |
| out_min | number | yes | — | Lower bound of the output range. |
| out_max | number | yes | — | Upper bound of the output range. |
| limit | boolean | optional | false | true to clamp the result to the output range. |
number
The mapped (and optionally clamped) value.
let waterLevel = 9; // feet
// Map the reservoir level (0–18 ft) to a 0–100 percentage,
// clamped to the output range by the optional true argument.
let reservoirPercentage = waterLevel.map(0, 18, 0, 100, true);
// reservoirPercentage = 50
x.limit(min, max) → number
Clamps the number between a lower and upper bound.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| min | number | yes | — | Lower bound. |
| max | number | yes | — | Upper bound. |
number
The value clamped to [min, max].
let reservoirPercentage = 103;
let limited = reservoirPercentage.limit(0, 100);
// limited = 100
x.toFixed([decimals]) → string
Formats the number as a string with a fixed number of decimal places.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| decimals | number | optional | 0 | Number of decimal places. |
string
The formatted value.
let pressure = 123.34224322;
let stringValue = pressure.toFixed(2); // "123.34"
Built-in array manipulation: splice for removing and inserting elements in place.
mJS arrays support in-place editing with splice(). (Iteration uses plain
for loops or for(let i in arr) — for..of is not part of mJS.)
a.splice(start, deleteCount, [item1, item2, …])
Changes the contents of an array by removing existing elements and/or inserting new ones.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| start | number | yes | — | Index at which to start changing the array. |
| deleteCount | number | yes | — | How many elements to remove from start. |
| items… | any | optional | — | Elements to insert at start. |
let a = [1,2,3,4,5];
a.splice(1, 2, 100, 101, 102);
// a === [1,100,101,102,4,5]
Script lifecycle, timing, device status and runtime configuration.
Core functions available everywhere in a script: lifecycle, timing, logging and device status.
These functions are available in every script without any prefix. They control the script lifecycle, timing, logging and access to device status.
The most important of these is run() — every continuous loop must poll it
so the device can stop the script during configuration updates. See the
product overview for the full lifecycle rules.
run() → boolean
Returns true while the script should keep running; the required condition for every main loop.
Polls the script scheduler. While the script should keep executing, run()
returns true; when the device needs the script to stop (for example during a
configuration update), it returns false.
Binding your main loop to run() allows the device to stop the script during
configuration updates and prevents unexpected behavior. Your script must exit
within 5 seconds of run() returning false, otherwise it will not be
updated during configuration changes, a device restart will be required, and
objects referenced by the script may become undefined.
Calling run() also syncs the IO variables with your local variables — see
syncIO() for mid-loop synchronisation.
boolean
true while the script should continue; false when it must shut down.
while(run()){
// monitoring logic here
waitMS(100);
}
// falls through here during a configuration update — exit promptly
waitMS(milliseconds)
Pauses the script for the given number of milliseconds.
Suspends script execution, yielding processor time to the rest of the device firmware. Use it inside your main loop to set the measurement cadence.
Avoid single waits longer than a few seconds — long blocking waits delay
script shutdown during configuration updates (the 5-second rule). For long
intervals, loop shorter waits bound to run().
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| milliseconds | number | yes | — | How long to pause, in milliseconds. |
while(run()){
// take measurements ...
waitMS(5000); // repeat every 5 seconds
}
print(value, value2, ...)
Writes values to the device system log / script console.
Accepts any number of arguments and writes them to the script console, which
is visible in the on-board web interface. Numbers and objects should be
converted with string() when concatenating into a message.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| value… | any | yes | — | One or more values to log. Objects should be wrapped in string() to serialise them. |
let speed = scope.getPeak(50, 70);
print("Peak: ", string(speed));
// Peak: {"bin":322,"freq":59.977173,"amp":0.037552}
print("RPM:", (speed.freq * 120 / 10).toFixed(), "rpm");
log(title, [description], [type], [priority], [value])
Writes a message to the persistent system event log — short form with a severity, or extended form with grouping, priority and an attached value.
Unlike print() (console output), log() writes to the internal device event log.
If cloud synchronisation is enabled, the event log is automatically uploaded to the cloud.
The log entries are stored in RAM and therefore have an unlimited write endurance but will also be lost if power to the device is not maintained.
Two forms are supported:
Short form — a message plus a severity constant:
log("Logic Started", alm.info);
Extended form — title, description, a numeric type for grouping, a priority, and a numeric value attached to the message. This structured form is particularly useful when the device reports to a Grafana / InfluxDB cloud backend, where the fields become queryable:
log(
"Title",
"Description / Message",
3, // type, 0–255 — used for grouping alarm/message types
2, // priority, 0–255 — user definable, or use alm.ok / alm.info / alm.warn / alm.crit
23.43 // alarm value — numeric value attached to the message
);
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| title | string | yes | — | Title of the log entry (or the whole message in short form). |
| description | string | optional | — | Longer description / message body. |
| type | number | optional | 0 | Message type, 0–255. Used for grouping alarm/message types. |
| priority | number | alm constant | yes | — | Priority 0–255 — user definable, or one of alm.ok, alm.info, alm.warn, alm.crit. |
| value | number | optional | NAN | Numeric value attached to the entry (e.g. the measurement that triggered it). |
// Short form
log("Logic Started", alm.info);
// Extended form
log("Pump Fault", "Discharge pressure out of range", 3, alm.warn, 23.43);
setEnv(key, value)
Sets a device environment / configuration variable at runtime.
Adjusts device configuration from within a script. Known keys include:
| Key | Value | Effect |
|---|---|---|
"led" |
"green", "red", … |
Sets the front-panel status LED color. |
"log.clear" |
(none) | Clears the system log. |
"sys.utc_offset" |
number (hours) | Sets the clock's UTC offset, e.g. -8. |
"sys.meas_interval" |
seconds | Measurement interval. |
"sys.sync_interval" |
seconds | Server upload / sync interval. |
"enc.key" |
string | End-to-end encryption passphrase used for communication when encryption is activated. If not set, the device password is used. |
A practical use is adapting the measurement interval to communication outages
— see the Variable Measurement Interval example, which stretches the
interval as the measurement queue (__status().mqueue) fills.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| key | string | yes | — | Environment variable name (see table above). |
| value | string | number | optional | — | New value. Some keys, like "log.clear", take no value. |
setEnv("led", "green"); // status LED
setEnv("sys.utc_offset", -8); // Pacific time
setEnv("sys.meas_interval", 60); // measure every minute
syncIO()
Manually syncs IO points with local variables between run() calls.
IO variables are synced with local variables whenever run() executes. If you
need IO point updates to take effect during the loop body — for example to
change a relay immediately — call syncIO() manually.
io.relay1.state = true;
syncIO(); // push the change now instead of waiting for run()
now() → timestamp
Returns the device uptime in seconds; the result carries a .sAgo() method for measuring elapsed time.
Returns seconds since boot as a floating-point number. Useful for measuring
elapsed time and for timestamping when a wall-clock time is not required (see
datetime() for calendar time).
The returned timestamp also carries a .sAgo() method giving the number
of seconds elapsed since it was taken — convenient for run-time counters and
for alarm conditions with alm.e():
let startTime = now();
// ... later ...
print("Run Time:", startTime.sAgo()); // seconds since startTime
timestamp
Uptime in seconds. Also provides .sAgo() → seconds elapsed since this timestamp was taken.
let uptime = now();
print('Uptime Seconds: ',uptime);
datetime() → object
Returns the current date and time as an object, including the unix epoch.
Returns the device real-time clock as an object. valid indicates whether the
clock has been set. Set the timezone with setEnv("sys.utc_offset", hours).
Available properties: valid, uptime, epoch, year, month, monthDay,
weekDay, hour, minute, second.
object
e.g. {"valid":true,"uptime":1437.05,"epoch":1589247811,"year":2020,"month":5,"monthDay":12,"weekDay":2,"hour":1,"minute":43,"second":31}
print(string(datetime()));
let ts = datetime().epoch; // unix timestamp
let year = datetime().year; // e.g. 2020
string(value) → string
Converts numbers and objects to strings for logging and transmission.
Serialises a value to a string. Objects are converted to their JSON
representation — this is the standard way to prepare a payload for http.req,
mqtt.publish, udp.send or sd.append, and to make objects printable.
The inverse for JSON text is JSON.parse().
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| value | any | yes | — | Number, object or other value to serialise. |
string
String / JSON representation of the value.
let payload = { batteryVoltage: 14.3, uptime: now() };
http.req("http://myserver.com/url", string(payload), 1000, false);
__status() → object
Returns device status: supply voltage, identity, firmware versions and measurement queue level.
Returns a snapshot of device status. Fields include:
| Field | Meaning |
|---|---|
vcc |
Supply voltage (V). |
name, desc, location, contact |
Device identity strings from configuration. |
uid |
Unique device ID. |
hw_v, fw_v, cfg_v |
Hardware, firmware and configuration versions. |
mqueue |
Measurement queue fill level (percent). Useful for adapting measurement intervals during communication outages. |
object
e.g. {"vcc":23.81,"name":"Flexs Vibration Monitor","uid":2585356324,"hw_v":9001,"fw_v":10,"cfg_v":164,"mqueue":0}
print(string(__status())); // {"vcc":23.814098,"contact":"","location":"","desc":"","name":"Flexs Vibration Monitor","uid":2585356324,"hw_v":9001,"fw_v":10,"cfg_v":164,"mqueue":0}
Recording measurements to the data logger, the memory card and non-volatile memory.
The datalogging object: everything assigned into an io group is recorded by the device.
io is the bridge between your script and the device datalogger: everything
placed into the io object is logged and synced whenever run() executes
(or immediately with syncIO()).
To log local variables, add them to one of the predefined io groups. This can be an existing group with physical channel values, or a group dedicated to your own logging variables.
Add custom measurements to a predefined group with plain assignment:
// dot syntax for simple names
io.totalPower = { watts: 3000, voltage: 480, current: 48 };
// bracket syntax for group names with spaces
io["Front Bearing"] = amplitudes;
A common pattern from the vibration analysis examples is building a result
object from scope.getAmplitudes(), appending extra values, then assigning
the whole object to a group:
amplitudes.speed = rpm; // add the measured speed
io["Front Bearing"] = amplitudes; // one assignment logs it all
io.groupName = object / io["Group Name"] = object
Assigning an object to an io group records all of its values in the datalogger.
Assignment into io is how measurements enter the datalogger. The value
should be an object of named numeric measurements. Updates are pushed on the
next run() call, or immediately after syncIO().
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| group | string | yes | — | Name of a predefined io group (physical channels or a group dedicated to script variables). |
| object | object | yes | — | Named numeric values to record, e.g. { watts: 3000, voltage: 480 }. |
To log local variables you need to add them to one of the predefined io groups.
This could be an existing group with physical channel values or a group dedicated to your own logging variables.
To add custom measurements / variables to a predefined group named 'io' use the following Syntax
io.totalPower = {watts: 3000, voltage: 480, current: 48};
Memory card access for custom log files.
The sd object reads and writes to the device memory card, letting you keep custom configurations or log
files alongside the built-in datalogger — useful for raw text logs, CSV
exports or audit trails.
SD cards must be formatted as Fat32. Cards up to 128GB.
To avoid long read and write delays we recommend keeping the directory structure small and avoiding large files.
sd.append(filename, data)
Appends a string to a file on the memory card, creating the file if needed.
Appends data to filename on the memory card. Convert numbers and objects
with string() first, and terminate lines with "\n" yourself.
Pair with datetime().epoch for timestamped log lines.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| filename | string | yes | — | File name on the memory card, e.g. "log.txt". |
| data | string | yes | — | Text to append. Use string() to convert numbers/objects. |
number
0 = no error, >0 = error code
// Timestamped log line: 1589247811: ch3: 23.454
sd.append('log.txt', string(datetime().epoch) + ": ch3: " + string(23.454) + "\n");
sd.read(file) -> object
Read file contents from SD Card
Read a file from the sd card and returns the result
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| filename | string | yes | — | e.g. "config.json" |
object
{error:0,data:"file contents"} 0 = No Error, data = file contents as a string
let res = sd.read("hello.txt");
if(!res.error){
print(res.data); // Prints the contents of hello.txt
} else {
print("Failed to load file err=", string(res));
}
sd.write(filename,data)
Write to a file on the SD Card
Easily write your string to a file on the SD card.
If the file does not exist it will be automatically created, if it already exists it will be overwritten.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| filename | string | yes | — | e.g. "config.json" |
| data | string | yes | — | Data to write to the file |
number
0 = no error, >0 = error code
sd.write("hello.txt","Hello World!");
Battery-backed non-volatile memory (4 kB) for setpoints, user tables and other state that must survive restarts.
The nv object stores strings in 4 kB of battery-backed non-volatile memory.
It is ideal for operator setpoints, calibration values or user tables that
must survive script restarts and power cycles.
The standard pattern is to keep state in an object with sensible defaults,
serialise it with string() on save, and merge it key-by-key on load — that
way, new setpoints added to the code keep their defaults when an older saved
configuration is loaded. See the Non-Volatile Setpoint Storage example.
Multiple objects can share the memory by using the optional byte offset —
e.g. setpoints at offset 0 and a users table at offset 2048, splitting
the 4 kB in half.
nv.get([offset]) → string | undefined
Loads a string from non-volatile memory; undefined when no valid data is stored.
Reads the string stored at offset (default 0). Returns undefined if no
valid string is detected — this can happen if the memory backup battery goes
dead, so always handle the undefined case by falling back to defaults.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| offset | number | optional | 0 | Byte offset within the 4 kB memory. |
string | undefined
The stored string, or undefined when nothing valid is stored.
nv.set("abcd");
nv.set("1234",2048);
let msg1 = nv.get();
let msg2 = nv.get(2048);
print(msg1); // msg1 = "abcd"
print(msg2); // msg2 = "1234"
nv.set(data, [offset])
Stores a string in non-volatile memory.
Writes data at offset (default 0). Serialise objects with string()
first. Take care that regions written at different offsets don't overlap.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| data | string | yes | — | The string to store — use string(obj) for objects. |
| offset | number | optional | 0 | Byte offset within the 4 kB memory. |
nv.set(string(sp)); // setpoints at offset 0
nv.set(string(users), 2048); // users table at offset 2048
HTTP, UDP and MQTT communication with servers, brokers and other devices.
HTTP client requests and an on-device JSON web server under /app/.
The http object provides both directions of HTTP:
http.req() sends requests to external servers or to other
devices (including other Q5 units running a script web server).http.handle() registers a handler for requests arriving at
http://<device IP>/app/<url>, letting the script expose its own JSON REST
API.Payloads can optionally be end-to-end encrypted with AES. Set the passphrase
with setEnv("http.aesKey", "myAesPassphrase") — if not set, the device
password is used.
http.req(url, payload, timeout, encrypt) → { err, code, payload }
Sends an HTTP request; a non-empty payload is sent as a POST body.
Performs an HTTP request and returns the response. Passing a payload string
sends a POST with that body; an empty string "" performs a plain GET-style
request.
Check res.err === 0 and res.code === 200 before trusting the response,
then parse JSON payloads with JSON.parse(res.payload).
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| url | string | yes | — | Target URL, e.g. "http://172.16.220.39/app/abc". |
| payload | string | yes | — | POST body. Build with string(obj). Use "" for no payload. |
| timeout | number (ms) | yes | — | Request timeout in milliseconds. |
| encrypt | boolean | yes | — | Enable AES end-to-end encryption of the payload (key from setEnv("http.aesKey", …)). |
object
{ err, code, payload } — err 0 and code 200 indicate success; payload is the response body text.
let request = { msg: "hello from client", epoch: datetime().epoch };
let res = http.req("http://172.16.220.39/app/abc",
string(request), // POST payload
1000, // timeout ms
false); // no encryption
if(res.err === 0 && res.code === 200){
let payload = JSON.parse(res.payload);
print("Remote Q5 Battery Voltage", payload.batteryVoltage);
} else {
print("Error reading data from remote Q5:", string(res));
}
http.handle(handler, encrypt)
Registers a request handler, turning the script into a JSON web server at /app/<url>.
Registers a callback that serves requests to http://<device IP>/app/<url>.
The handler receives a request object (with req.url set to the path after
/app/) and returns the response body string — build JSON responses with
string(obj).
Register the handler once, then keep the script alive with a run() loop.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| handler | function | yes | — | Callback function(req){ ... return responseString; }. req.url is the path after /app/. |
| encrypt | boolean | yes | — | Require AES end-to-end encryption (key from setEnv("http.aesKey", …)). |
function handleHttp(req){
if(req.url === 'abc'){
return string({ batteryVoltage: 14.8, io: io, uptime: now(), epoch: datetime().epoch });
}
return "Invalid Request URL, try /app/abc";
}
http.handle(handleHttp, false);
while(run()){ waitMS(1000); }
Connectionless UDP datagrams: fire-and-forget transmission and a single-port listener with optional AES-256 encryption.
udp works in both directions: udp.send() transmits fire-and-forget
datagrams (no delivery confirmation — ideal for high-rate telemetry and
syslog targets), and udp.handle() listens on a port and hands received
packets to a callback, optionally decrypting them with AES-256.
udp.send(sourcePort, dest, payload, encrypted)
Transmits a UDP packet to a host and port.
Please use the latest firmware, On earlier firmware versions the sourcePort was omitted.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| sourcePort | yes | — | Source port for UDP datagrams | |
| dest | string | yes | — | Destination in "host:port" form, e.g. "192.168.5.159:2115". |
| payload | string | object | yes | — | Datagram payload data |
| encrypted | bool | optional | false | Whether to use built in AES256 Encryption and integrity validation |
number
0 = success, >0 = error code
// UDP Stream Example
while(run()){
let payload = {
batteryVoltage: 15.3,
uptime: now()
};
udp.send(4000,"192.168.5.159:2115",string(payload));
waitMS(1000);
}
udp.handle(port, handler, encrypt)
Listens for UDP packets on a port and passes them to a callback — only one listener is supported.
Registers the device's single UDP listener. The handler receives a request
object whose req.data holds the packet payload — parse JSON payloads with
JSON.parse(). When encrypt is true, payloads are decrypted with AES-256 and the integrity validated with a SHA256 checksum before the handler runs.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| port | number | yes | — | UDP port to listen on, e.g. 9000. |
| handler | function | yes | — | Callback function(req){ … } — req.data is the received payload. |
| encrypt | boolean | yes | — | true to expect AES-256 encrypted payloads. |
udp.handle(9000, function(req){ // only a single udp.handle listener is supported
//req = {port:4000,host:"172.16.220.1",data:"{msg:\"Hello World!\"}"}
let data = JSON.parse(req.data); // parse the received payload as JSON
print(data.msg);
}, false); // false: do not use AES-256 encryption or checksums
MQTT publish/subscribe client for broker-based telemetry and control.
The mqtt object connects to an MQTT broker, publishes topics and subscribes
with a callback. Requires firmware V91 or greater.
A robust pattern is to call mqtt.connect() at the top of every loop
iteration: if the client is already connected it simply returns code 207
(Already Connected), and if the connection ever drops it will reconnect.
mqtt.connect(options) → number
Connects to an MQTT broker and registers topic subscriptions with a callback.
Options object fields:
| Field | Type | Meaning |
|---|---|---|
url |
string | Broker address, e.g. "mqtt://172.16.220.22:1883". |
client |
string | MQTT client ID. |
subscribe |
array | Topic filters to subscribe, wildcards allowed, e.g. ["power_system/relays/+"]. |
callback |
function | Called for each received message with an event object { retain, qos, message, topic }. |
The callback's event.message can be a JSON string (parse with
JSON.parse) or binary data (convert with the bin functions).
Returns 0 on success, 207 if already connected; any other value is an
error code.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| options | object | yes | — | { url, client, subscribe: [...], callback } — see table. |
number
0 = connected, 207 = already connected, anything else = error code.
function mqttSubscriberCallback(event){
print("MQTT EVENT: ", string(event));
// {"retain":false,"qos":0,"message":"Hello World","topic":"power_system/relays/1"}
}
let ret = mqtt.connect({
url: "mqtt://172.16.220.22:1883",
client: "Nikiah Repeater",
subscribe: ["power_system/relays/+"],
callback: mqttSubscriberCallback
});
print("connect:", ret); // 0 ok, 207 already connected
mqtt.publish(topic, message) → number
Publishes a message to an MQTT topic.
Publishes message to topic. Message data can be text or binary: convert
objects with string(obj) first, and pack numeric arrays to binary with
bin.fromArray().
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| topic | string | yes | — | Topic to publish to, e.g. "power_system/battery_voltage". |
| message | string | binary | yes | — | Payload. Objects: string(obj). Numeric arrays: bin.fromArray(). |
number
0 on success, otherwise an error code.
let ret = mqtt.publish("power_system/battery_voltage", string(14.34));
print("publish:", ret);
mqtt.disconnect()
Disconnects from the MQTT broker; call before the script exits.
Make sure the client is disconnected before the script exits.
mqtt.disconnect();
ftp.put(addr,user,password,data,mode) → number
FTP File Write / Append
Write to a file on a ftp server.
Mode argument determines whether the file is overwritten or appended to.
Useful for uploading test results, etc to a remote server.
Run python3 -m pyftpdlib -w -p2121 -u logger -P secret on a local linux workstation to spin up a FTP server instance for testing
Requires installing pyftpdlib with pip install pyftpdlib or sudo apt-get install python3-pyftpdlib etc.
FTP is considered an insecure file transfer protocol, use only on a private or restricted network.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| address | string | yes | — | "10.0.0.1:21/log.txt" |
| user | string | optional | — | anonymous |
| password | string | optional | — | default = blank |
| data | string | yes | — | payload |
| mode | number | optional | 0 | 0 = write, 1 = append |
number
0 = success, >0 = error code
while(run()){
let voltage = 14.5;
let msg = datetime().epoch.toFixed() + "\t" + voltage.toFixed(4) + "V"; // create a string with the epoch timestamp followed by the measurement value
// msg = 141312433142 13.6043V
let res = ftp.put("172.16.220.220:2121/voltage.log","logger","secret",msg,1); // append to the voltage.log file
print(res);
waitMS(5000); // log every 5 seconds
}
Modbus, EtherNet/IP, CAN bus and RS-485 serial connectivity to PLCs and instruments.
Modbus TCP/serial master and a script-managed slave register table.
The modbus object works in two directions:
modbus.connect() / modbus.read() / modbus.write() poll and
command remote Modbus devices over TCP, or over the RS-485 serial ports
(Modbus RTU — initialise the port with serial.init() first; see the
Serial Modbus (RS-485) example).modbus.setData() / modbus.setLock() expose a
register table, backed by a bin buffer your script updates, that remote
Modbus TCP clients can read and write.Register data is packed binary — use the bin buffer read/write methods
with format strings like "F32BADC" or "U32DCBA" to convert between
registers and numbers. Test slave tables with the free
modpoll tool.
Function codes 1, 2 and 15 expect a bit array rather than register data.
modbus.connect(host, port, unitId) → number
Opens a Modbus TCP connection to a remote device.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| host | string | yes | — | IP address of the Modbus device, e.g. "172.16.220.111". |
| port | number | yes | — | TCP port, normally 502. |
| unitId | number | yes | — | Slave unit ID. |
number
0 on success, otherwise an error code.
let ret = modbus.connect("172.16.220.111", 502, 1);
if(ret === 0){
// read / write ...
modbus.disconnect();
} else {
print("Modbus Connect Failed, err=", ret);
}
modbus.read(port, unitId, fc, startReg, count) → { status, data }
Reads registers or coils from a Modbus slave.
Reads from a connected slave and returns { status, data } where status is
0 on success and data is a binary buffer. Parse the buffer with
data.seek(0) then repeated data.read(format) calls.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| port | number | yes | — | Communication port: 0 = TCP, >0 = Hardware Serial Port (serial ports need serial.init() first). |
| unitId | number | yes | — | Slave unit ID. |
| fc | number | yes | — | Modbus function code, e.g. 3/4 = read registers. FCs 1/2 return a bit array. |
| startReg | number | yes | — | Starting register address, e.g. 0x88B9. |
| count | number | yes | — | Number of registers or coils to read. |
object
{ status, data } — status 0 = success; data is a bin buffer.
let ret = modbus.read(0, // 0=tcp, >0 = physical serial port
1, // slave unitID
4, // function code
0x88B9, // start register
30); // register count
if(ret.status === 0){
ret.data.seek(0);
let data = {
meanWindSpeed: ret.data.read("U32DCBA")/10, // m/s
meanWindDirection: ret.data.read("U32")/10, // degrees
airTemp: ret.data.read("S32")/10, // °C
};
print(string(data));
} else {
print("Read Failed, err=", string(ret));
}
modbus.write(port, unitId, functionCode, startReg, count, data) → number
Writes registers or coils to a Modbus slave.
Writes packed binary data to the slave. Build the buffer from a numeric
array with bin.fromArray(array, format, byteOrder).
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| port | number | yes | — | Communication port: 0 = TCP, >0 = Hardware Serial Port (serial ports need serial.init() first). |
| unitId | number | yes | — | Slave unit ID. |
| functionCode | number | yes | — | Function code, e.g. 16 = write multiple registers. FC 15 expects a bit array. |
| startReg | number | yes | — | Starting register address. |
| count | number | yes | — | Number of registers or coils to write. |
| data | bin buffer | yes | — | Packed data, e.g. from bin.fromArray([1,2,3], "U16", "AB"). |
number
0 on success, otherwise an error code.
let arrayData = [1,2,3,4,5,6,7,8];
let data = bin.fromArray(arrayData, "U16", "AB");
let ret = modbus.write(0, 1, 16, 0, arrayData.length, data);
if(ret === 0){ print("Write Successful"); }
else { print("Modbus Write Failed, err=", ret); }
modbus.setData(buffer)
Backs the device Modbus slave register table with a script-managed bin buffer.
Points the device's Modbus TCP slave data table at a buffer allocated with
bin.alloc(). Remote clients then read and write your table; your script
updates it through the buffer's seek/read/write methods, guarded by
modbus.setLock().
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| buffer | bin buffer | yes | — | Buffer from bin.alloc(bytes) sized for your registers (4 bytes per 32-bit value). |
let mbData = bin.alloc(20*4); // room for float registers
modbus.setData(mbData); // expose as the slave data table
modbus.setLock(locked) → boolean
Locks/unlocks the slave data table so clients never read a half-updated table.
Lock the table before updating it from your script, then unlock so clients can
read and write again. modbus.setLock(true) returns true when the lock was
acquired.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| locked | boolean | yes | — | true to lock the table, false to unlock. |
boolean
true when the lock state was applied.
if(modbus.setLock(true)){
mbData.seek(0);
mbData.write("F32BADC", [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]);
mbData.write("U32BADC", 23.33);
modbus.setLock(false);
}
modbus.disconnect()
Closes the master connection to the remote Modbus device.
modbus.disconnect();
EtherNet/IP client for reading data files from Allen-Bradley style PLCs.
The eip object speaks EtherNet/IP in both directions:
eip.connect() / eip.read() read PLC data files. Files are
addressed by a type prefix and number — F, O, I, S, B, T, R,
N depending on type (e.g. "F103" for float file 103). Returned data is
a packed binary buffer parsed with the bin reader functions.eip.handle() answers incoming EtherNet/IP read and write
requests from a PLC, letting the device appear as a data file the PLC polls
and commands directly.eip.connect(ip) → number
Connects to a PLC over EtherNet/IP.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| ip | string | yes | — | PLC IP address, e.g. "192.168.168.1". |
number
0 = connected, otherwise an error code.
if(eip.connect("192.168.168.1") === 0){
// read files ...
eip.disconnect();
} else {
print("PLC Connection Failed");
}
eip.read(file, offset, bytes) → { status, data }
Reads bytes from a PLC data file.
Reads bytes starting at element offset of the addressed file. Parse the
returned buffer with string.seek() and string.read().
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| file | string | yes | — | File address with type prefix (F,O,I,S,B,T,R,N) + number, e.g. "F103". |
| offset | number | yes | — | Starting element offset within the file. |
| bytes | number | yes | — | Number of bytes to read, e.g. 4*4 for four 32-bit floats. |
object
{ status, data } — status 0 = success; data is a bin buffer.
let res = eip.read("F103", 1, 4*4); // four 4-byte floats
if(res.status === 0){
res.data.seek(0); // Position our cursor at the start of the data
let plc = {
flow: res.data.read("F32ABCD"),
pressure: res.data.read(), // No format specified, use the last specified format
pumpSpeed: res.data.read(),
temp: res.data.read(),
lastUpdated: now()
};
print(string(plc));
} else {
print("PLC Read Failed");
}
eip.handle(handler)
Answers incoming EtherNet/IP read and write requests from a PLC.
Registers a handler for EtherNet/IP requests arriving at the device. The request object provides:
| Field | Meaning |
|---|---|
req.file |
The requested file address (e.g. "F103"). |
req.bytes |
Read requests: the byte length of the requested data. |
req.data |
Write requests: a bin buffer with the written data. Absent on reads. |
Write requests (req.data present): parse the buffer with
req.data.seek() / req.data.read() and apply the values — e.g. unpack a
PLC status word into individual io flags with bit masks.
Read requests: build a response with bin.alloc() and the buffer write
methods (including the "BIT" format for packing status bits), then
return the buffer.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| handler | function | yes | — | Callback function(req){ … } — return a bin buffer to answer read requests. |
eip.handle(function(req){ // handle incoming EtherNet/IP requests
if(req.data){ // write request
req.data.seek(0);
let status = req.data.read("U16AB");
io.PALL = {
feedEnable: status & 1,
masterAlarm: (status & (1<<5)) > 0,
flowAvailable: req.data.read("U16AB") / 0.2777778
};
} else { // read request — build and return a reply
let buff = bin.alloc(100);
buff.write("BIT", [0, io.p304.running, io.p303.running, 0,0,0,0,0, 0,0,0,0,0,0,0,0]);
buff.write("U16AB", [151.2 * 0.2777778, io.clearwellLevel701.inst * 10]);
return buff;
}
});
eip.disconnect()
Ends the EtherNet/IP connection.
eip.disconnect();
RS-485 serial ports: initialise, transmit, and receive delimited messages with a callback.
The serial object drives the device's RS-485 serial ports:
| Port | Hardware |
|---|---|
1 |
Isolated RS-485 port |
2 |
Non-isolated RS-485 port |
Initialise the port with a baud rate, register a delimiter-based receive
callback, and transmit with serial.tx(). For device-to-device links over
untrusted wiring, combine with aes.enc/aes.dec and bin.toHex/
bin.fromHex to send encrypted, checksummed messages — see the Serial Port:
Encrypted Device-to-Device example.
The same ports can also speak Modbus RTU directly via modbus.read() /
modbus.write() with the matching port number — see the Serial Modbus
(RS-485) example.
serial.init(port, baud) → number
Initialises a serial port at the given baud rate.
Specify a parameter object for advanced usage
serial.init(port,{baud:9600,stopBits:1,parity:"odd",mode:"0,swap:false,invert:false});
:Mode 0: tx+rx, 1: rx only, 2: tx only,
:Invert Invert the RS485 high low signaling (Experimental)
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| port | number | yes | — | 1 = isolated RS-485, 2 = non-isolated RS-485. |
| baud | config | number | object | yes | — | Baud rate, e.g. 9600, 19200. for standard 8n1 settings otherwise supply a json object with parameters |
number
Status code — 0 on success.
let port = 1; // isolated RS-485
serial.init(port, 19200);
serial.listen(port, delimiter, callback)
Buffers received bytes and calls the callback whenever the delimiter character arrives.
Fills the receive buffer while watching for the delimiter character, then executes the callback with a message object:
| Field | Meaning |
|---|---|
msg.port |
Port the message arrived on. |
msg.ovf |
true if the receive buffer overflowed — discard the message. |
msg.data |
The received data. |
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| port | number | yes | — | 1 = isolated RS-485, 2 = non-isolated RS-485. |
| delimiter | string | yes | — | End-of-message character, e.g. "\r". |
| callback | function | yes | — | Callback function(msg){ … } — see the field table. |
function serialRxCallback(msg){
print("Received Serial Msg");
print("Port:", msg.port);
print("Overflowed:", msg.ovf);
print("Data:", msg.data);
}
serial.listen(port, "\r", serialRxCallback);
serial.tx(port, data)
Transmits data on a serial port.
Sends data out the port. Remember to append your protocol's end-of-message
delimiter (e.g. "\r") so the receiver's serial.listen() fires.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| port | number | yes | — | 1 = isolated RS-485, 2 = non-isolated RS-485. |
| data | string | yes | — | Data to send — hex-encode binary payloads with bin.toHex(). |
serial.tx(port, "Hello World over serial!\r");
Packing, parsing and converting binary buffers, JSON and strings.
Allocate, pack, and parse binary buffers used by Modbus, EtherNet/IP, MQTT and crypto.
The bin object converts between JavaScript numbers/arrays and the packed
binary buffers used across the protocol and crypto APIs.
Data types. Valid types for packing and unpacking:
| Type | Meaning |
|---|---|
F32 |
IEEE 754 single-precision float |
F64 |
IEEE 754 double-precision float |
BITS |
Single-bit array |
RBITS |
Reverse-bit-order single-bit array |
U8 / S8 |
Unsigned / signed 8-bit integer (0…255 / −128…127) |
U16 / S16 |
Unsigned / signed 16-bit integer (0…65,535 / −32,768…32,767) |
U32 / S32 |
Unsigned / signed 32-bit integer |
Byte order defaults to ABCD if not specified. It can be given as a
suffix on the format ("F32BADC", "U32DCBA", "U16AB") or as a separate
argument — AB (16-bit) and ABCD, BADC, DCBA, CDAB (32-bit) cover
the big-endian, little-endian and word-swapped layouts found across Modbus
devices. Bit types (BITS/RBITS) take no byte order.
Round trips. bin.fromArray ↔ bin.toArray convert between arrays and
packed binary; bin.toHex ↔ bin.fromHex convert between binary and hex
text — see the Binary Packing, Bits & Hex example.
Buffer objects returned by bin.alloc(), modbus.read().data and
eip.read().data carry their own cursor-based methods:
buffer.seek(byte) — set the cursor position.buffer.read(format, [count]) — read one value (or an array of count values) and advance.buffer.write(format, value | array) — write a value or array and advance.bin.alloc(bytes) → buffer
Allocates a empty buffer (string) that can be used to store and read binary values.
Creates an empty string bytes long. String objects in mJS are byte strings,
so they can hold arbitrary binary data — null characters do not terminate
them. After any call to bin.alloc() the binary read/write cursor is
automatically positioned at the start of the new buffer.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| bytes | number | yes | — | Buffer size in bytes — 4 bytes per 32-bit register/float. |
string
A buffer with seek/read/write methods, cursor positioned at byte 0.
let mbData = bin.alloc(20*4); // room for 20 float registers
modbus.setData(mbData); // Assign the data buffer for to modbus read and write requests.
while(run()){
mbData.write("F32ABCD",75); // Write a float32 (4 bytes) to our modbus data table
waitMS(1000);
}
buffer.seek(byte) · buffer.read(format, [count]) · buffer.write(format, value|array)
Tools to store and read binary data structures.
There is only one string type in mJS which is used to store either textual strings or raw byte buffers.
Payload data returned from RS485 reads, CANBus messages, Http requests, Modbus Queries etc can all be directly parsed into regular numbers easily with these methods.
seek(byte) sets the position (byte 0 = start). (Always call this before .read() or .write())
read(format) returns one value; read(format, count) returns an array of count values. The cursor advances.
write(format, value) writes one value; write(format, array) appends the whole array. The cursor advances. The "BIT" format packs elements of 1/0 (or boolean) values into bits — see eip.handle() for a worked status-word example.
seek(byte) sets the position (byte 0 = start).read(format) returns one value; read(format, count) returns an array of
count values. The cursor advances.write(format, value) writes one value. The cursor advances.write(format, array) appends the whole array. The cursor advances.The "BIT" format can be used to work with individual bits which could be provided as an array of 1/0(or boolean) or individual values — seeeip.handle()` for a worked status-word example.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| format | string | yes | — | Format string, e.g. "F32BADC", "U32DCBA". |
| count / value | number | array | optional | — | read: how many values to return. write: the value or array to pack. |
while(run()){
let data = bin.alloc(8); // allocate a 8 byte buffer
print(bin.toHex(data)); // print the hex for data "0000000000000000"
// Write some data to the buffer
data.seek(1); // Set our read/write pointer to start on the second byte
data.write("U8", 255); // Write a single unsigned 8 with value 255
data.write("U8", [0,1,2,3]); // Write an array of values as unsigned 8
print(bin.toHex(data)); // print the hex for data "00ff000102030000"
// Read some data back from the buffer
data.seek(1); // Set the read/write cursor position
print( data.read("U8") ); // "255"
print( string ( data.read("U8",4) ) ); // "[0,1,2,3]"
waitMS(1000);
}
bin.fromArray(array, format, [byteOrder]) → buffer
Packs an array of numbers or bits into a binary buffer.
Packs an array into binary data. For bit types ("BITS"/"RBITS") the array
holds 1/0 (or true/false) entries — 16 bits pack into two bytes.
The inverse is bin.toArray().
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| array | array | yes | — | Values to pack — numbers, or 1/0 / true/false for bit types. |
| format | string | yes | — | Element type: "F32", "F64", "BITS", "RBITS", "U8", "S8", "U16", "S16", "U32", "S32". |
| byteOrder | string | optional | "ABCD" | Byte order, e.g. "AB", "ABCD", "BADC", "DCBA", "CDAB". Not used by bit types. |
buffer
Packed binary data ready for modbus.write(), mqtt.publish() or hex conversion.
let data = bin.fromArray([1,2,3,4,5,6,7,8], "U16", "AB");
modbus.write(0, 1, 16, 0, 8, data);
// pack floats, word-swapped
let bytes = bin.fromArray([12345.6789, 100, 200], "F32", "BADC");
// pack a bit array (two bytes)
let flags = bin.fromArray([1,0,0,0,0,0,0,0, 0,1,1,1,1,1,1,1], "RBITS");
bin.toArray(data, format, [byteOrder]) → array
Unpacks binary data into an array of numbers or bits.
The inverse of bin.fromArray() — interprets packed binary data as an array
of the given type. Use it on data from bin.fromHex(), Modbus reads or MQTT
binary messages.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| data | buffer | binary | yes | — | The packed binary data to unpack. |
| format | string | yes | — | Element type — same set as bin.fromArray(), including "BITS"/"RBITS". |
| byteOrder | string | optional | "ABCD" | Byte order (see the class notes). Not used by bit types. |
array
The unpacked values.
let bytes = bin.fromHex("01fe");
let bitArray = bin.toArray(bytes, "RBITS");
print("bits:", string(bitArray));
let floats = bin.toArray(bin.fromHex("e6b74640000042c8"), "F32", "BADC");
bin.fromHex(hex) → binary
Converts a hex string into a binary object.
The inverse of bin.toHex(). Handy for pasting captured payloads back into a
script, or for decoding hex-encoded configuration values.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| hex | string | yes | — | Hex text, e.g. "01fe". |
binary
The decoded binary data.
let bytes = bin.fromHex("01fe");
print(bin.toHex(bytes)); // 01fe
bin.toHex(data) → string
Converts binary data to a printable hex string.
The inverse is bin.fromHex().
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| data | buffer | string | yes | — | Binary data, e.g. a sha256() digest or packed array. |
string
Hexadecimal representation.
let aesKey = sha256("flexscadaFlexsQ5!");
print("KEY: ", bin.toHex(aesKey));
bin.enc(data, key) → binary
Encrypts data with AES-256.
Encrypt payloads before sending them over untrusted networks or to insure data integrity,Keys are 256-bit values — derive one from a passphrase with bin.sha256(). Decrypted results include checksum validation.
The http and udp functions can also encrypt transparently (see setEnv("enc.key", …)).
Data is automatically padded and will be rounded up to the nearest block size.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| data | string | yes | — | Plaintext. Serialise objects first with string(obj). |
| key | binary | yes | — | 256-bit key, e.g. from bin.sha256(passphrase). |
binary
Encrypted data (slightly larger than the plaintext).
let aesKey = bin.sha256("password");
let secretMsg = string({ msg: "I LOVE FLEXSCADA", code: 12.454 });
let encrypted = bin.enc(secretMsg, aesKey);
print("Size (Raw, Encrypted)", secretMsg.length, encrypted.length);
aes.dec(data, key) → { valid, data }
Decrypts AES-256 data and validates its checksums.
Decrypt AES256 encrypted data received from peers. Keys are 256-bit values — derive one from a passphrase with bin.sha256(). Decrypted results include checksum validation via the valid flag.
The http and udp functions can also decrypt transparently (see setEnv("aes.key", …)).
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| data | binary | yes | — | Encrypted data from bin.enc() or a peer. |
| key | binary | yes | — | The 256-bit key used to encrypt. |
object
{ valid, data } — valid is true when decryption succeeded and checksums match; data is the plaintext.
let aesKey = bin.sha256("password");
let decrypted = bin.dec(encrypted, aesKey);
if(decrypted.valid){
let obj = JSON.parse(decrypted.data);
print(string(obj));
} else {
print("AES Decryption Failed.");
}
bin.crc16(data,len) → number
Modbus CRC16 checksum function
Generate a crc16 checksum from a chunk of data
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| data | string | yes | — | |
| len | number | optional | data.length |
number
Checksum Value
serial.init(2,9600);
let modbusFrame = bin.alloc(8);
modbusFrame.write("U8",3); // Slave Address
modbusFrame.write("U8",0x06); // 0x06 Function Code (Write Single Register)
modbusFrame.write("U16BA",0); // Register
modbusFrame.write("U16BA",1234); // Value
let crc = modbus.crc16(modbusFrame,6); // Generate the CRC from the first 6 bytes of our payload
modbusFrame.write("U16BA,crc); // write the CRC to the end of our message
serial.tx(2,modbusFrame); // Send the frame over the RS485 port.
bin.sha256(data) → binary
Computes a SHA-256 hash — the standard way to derive an AES-256 key from a passphrase.
Useful for ensuring data integrity or for generating encryption keys from passphrases (Don't forget to salt your passphrases!)
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| data | string | yes | — | Data to hash, e.g. a passphrase. |
binary
32-byte digest, usable directly as an AES-256 key. Print with bin.toHex().
let aesKey = sha256("flexscadaFlexsQ5!");
print("KEY: ", bin.toHex(aesKey));
Parsing JSON text into objects; use string() for the reverse.
JSON.parse() converts JSON text (from http responses, MQTT messages or
files) into script objects. The reverse — serialising an object to JSON text —
is done with the global string() function.
JSON.parse(text) → object
Parses a JSON string into an object.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| text | string | yes | — | JSON text, e.g. res.payload from http.req or event.message from MQTT. |
object
The parsed value.
let res = http.req(url, string(request), 1000, false);
if(res.err === 0 && res.code === 200){
let payload = JSON.parse(res.payload);
print("Remote Q5 Battery Voltage", payload.batteryVoltage);
}
JSON.stringify(value) → string
Serialises a value to JSON text — equivalent to the global string() for objects.
Available per the mJS standard. In the factory examples the global string()
function is normally used for the same purpose.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| value | any | yes | — | The value to serialise. |
string
JSON text.
let payload = JSON.stringify({ v: 14.3 }); // '{"v":14.3}'
Standard math library plus embedded extensions like value scaling and hardware random numbers.
Standard JavaScript Math library plus embedded extensions: Math.map() scaling and a hardware random number generator.
The standard JavaScript Math library is available, with two embedded
specialties documented below: Math.map() for scaling sensor values and
Math.random() backed by the hardware random number generator.
Constants: Math.E (≈2.718), Math.LN10 (≈2.303), Math.LN2 (≈0.693),
Math.LOG10E (≈0.434), Math.LOG2E (≈1.443), Math.PI (≈3.14159),
Math.SQRT1_2 (≈0.707), Math.SQRT2 (≈1.414).
Standard methods: abs, acos, acosh, asin, asinh, atan,
atan2, atanh, cbrt, ceil, cos, cosh, exp, floor, log,
max, min, pow, round, sin, sinh, sqrt, tan, tanh, trunc —
all behave as in standard JavaScript.
Math.random() → number
Returns a 32-bit random number from the hardware random number generator.
Unlike standard JavaScript, Math.random() on the Q5C is backed by the
hardware random number generator, making it suitable for nonces and keys.
number
32-bit hardware random number.
let nonce = Math.random();
Alarm condition evaluation, grouping, acknowledgement and structured event logging.
Register and evaluate alarm conditions with trigger/clear delays, grouping, acknowledgement and priority queries.
The alm object turns raw conditions into managed alarms: each fault has a
group, a title, a priority, a trigger delay (the condition must hold this long
before the alarm activates) and an optional automatic-clear behaviour. State
changes are automatically recorded in the event log, including the fault's
attached value.
Priority constants — used by alm.e() and the extended form of log():
| Constant | Meaning |
|---|---|
alm.ok |
Normal / no alarm |
alm.info |
Informational |
alm.warn |
Warning |
alm.crit |
Critical |
Custom numeric priorities (0–255) can be used as well.
Typical pattern — set the group, evaluate each condition every loop, then read the aggregate state:
while(run()){
alm.g("UV"); // current alarm group
alm.e(alm.crit, "Reactor A Run Failure", pressure > limit, 5, true, 0, pressure);
let worst = alm.max(); // highest active priority overall
waitMS(1000);
}
See the Alarm Conditions example for a complete script including resets and acknowledgement.
alm.g(group)
Sets the current alarm group; subsequent alm.e() evaluations register under it.
Alarms are organised into named groups (e.g. one per machine or subsystem).
Call alm.g() before evaluating conditions to select which group the
following alm.e() calls belong to.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| group | string | yes | — | Group name, e.g. "UV". |
alm.g("UV"); // conditions evaluated next belong to the "UV" group
alm.e(priority, title, condition, triggerDelay, clearable, clearDelay, [value]) → number
Evaluates an alarm condition with trigger/clear delays; returns the alarm’s current priority, or 0 when not alarming.
Registers and evaluates one fault under the current group (alm.g()). Call it
every loop iteration with the live condition — alm.e() handles the
debouncing, state tracking and event-log records for you:
true for triggerDelay seconds before the alarm
activates.clearable is true, the alarm clears automatically once the condition
has been false for clearDelay seconds; otherwise it stays latched until
reset with alm.rst().value (e.g. a reservoir level or pressure) is included in the
log messages automatically recorded when the fault changes state.Returns the alarm's current priority while active, or 0 when not alarming.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| priority | number | alm constant | yes | — | Alarm priority — alm.ok/alm.info/alm.warn/alm.crit or 0–255. |
| title | string | yes | — | Alarm title, unique within the group. |
| condition | boolean | yes | — | The live fault condition, evaluated every call. |
| triggerDelay | number (s) | yes | — | Condition must be true this many seconds before the alarm activates. |
| clearable | boolean | yes | — | Whether the alarm may clear automatically when the condition recovers. |
| clearDelay | number (s) | yes | — | Condition must be false (and the alarm clearable) this many seconds before it auto-clears. |
| value | number | optional | — | Current value attached to the fault (e.g. level, pressure) — recorded in state-change log entries. |
number
Current priority of the alarm while active, 0 when not alarming.
let startTime = now();
while(run()){
alm.g("UV"); // current alarm group
let value = alm.e(
alm.crit, // priority
"Reactor A Run Failure", // title
startTime.sAgo() > 5 && startTime.sAgo() < 12, // condition
5, // trigger delay (s)
true, // automatically clearable?
0, // clearing delay (s)
startTime // optional attached value, logged on state changes
);
print("Run Time:", startTime.sAgo(), " Fault Value:", value);
waitMS(1000);
}
alm.v(group, [group2, …]) → number
Returns the highest-priority active alarm within the given group(s).
If multiple faults exist in a group, returns the priority of the highest active one. Additional groups can be passed as extra arguments to aggregate across them.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| group… | string | yes | — | One or more group names. |
number
Highest active priority in the group(s), 0 if none alarming.
let groupValue = alm.v("UV");
alm.fV(group, title) → number
Returns the value of one specific fault.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| group | string | yes | — | Group name, e.g. "UV". |
| title | string | yes | — | Fault title within the group. |
number
The fault’s current value.
let faultValue = alm.fV("UV", "Reactor A Run Failure");
alm.list() → object
Returns the status of every registered alarm, organised by group.
object
e.g. {"UV":{"Reactor A Run Failure":0}} — 0 means not alarming; otherwise the active priority.
let faults = alm.list();
print(string(faults)); // {"UV":{"Reactor A Run Failure":0}}
alm.max() → number
Returns the highest active alarm priority across all groups.
A one-call health summary — useful for driving the status LED or a summary output:
setEnv("led", alm.max() >= alm.warn ? "red" : "green");
number
Highest active priority overall, 0 when nothing is alarming.
let max = alm.max();
alm.rst([group, title])
Resets all faults, or one specific fault.
With no arguments, resets every registered fault. Pass a group and title to reset a single fault (e.g. a latched, non-clearable alarm after the cause has been fixed).
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| group | string | optional | — | Group of the fault to reset. Omit to reset everything. |
| title | string | optional | — | Title of the fault to reset. |
alm.rst(); // reset all faults
alm.rst("UV", "Reactor A Run Failure"); // reset one fault
alm.ack([group, title, [cancel]])
Acknowledges all faults, or one specific fault; pass false to cancel an acknowledgement.
With no arguments, acknowledges every fault. Pass a group and title to
acknowledge a single fault. An optional false appended as the last argument
cancels the acknowledgement.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| group | string | optional | — | Group of the fault. Omit to acknowledge everything. |
| title | string | optional | — | Title of the fault. |
| cancel | boolean | optional | true | Pass false to cancel the acknowledgement. |
alm.ack(); // acknowledge all faults
alm.ack("UV", "Reactor A Run Failure"); // acknowledge one fault
alm.ack("UV", "Reactor A Run Failure", false); // cancel the acknowledgement
Read the device uptime in seconds with now().
let uptime = now();
print('Uptime Seconds: ',uptime);Read supply voltage, device identity, firmware versions and measurement queue level with __status().
print(string(__status())); // {"vcc":23.814098,"contact":"","location":"","desc":"","name":"Flexs Vibration Monitor","uid":2585356324,"hw_v":9001,"fw_v":10,"cfg_v":164,"mqueue":0}Sets the clock UTC offset, appends timestamped lines to a log file on the
memory card, and prints the full datetime() object.
while(run()){
setEnv('sys.utc_offset',-8); // Set UTC offset
// minute hour weekDay monthDay month year valid epoch are all valid properties of the dateTime object
// e.g. dateTime.year would return the current year
// Log the unix epoch timestamp followed by the ch3 values
//234323: ch3: {"ripple":0.001314,"state":false,"max":0.002665,"min":0.001114,"inst":0.001841,"avg":0.001937}
// string( is used to convert numbers to a string for logging.
sd.append('log.txt',string(datetime().epoch) + ": ch3: " + string(23.454) + "\n");
// Print the entire dateTime object to show all paramaters
print(string(datetime()));
// {"valid":true,"uptime":1437.058175,"epoch":1589247811,"year":2020,"month":5,"monthDay":12,"weekDay":2,"hour":1,"minute":43,"second":31}
waitMS(10000); // wait 10 second before repeating
}Reads JSON from external web servers and from another Q5 running the JSON Web
Server example — POSTing a payload, checking err/code, and parsing the
response.
// Example JSON web client showing reading from external web servers and from another Q5 running the JSON WEB SERVER example
setEnv("http.aesKey","myAesPassphrase"); // Set end to end encryption key (Only used if encryption is enabled), if not set the device password is used
function getDateFromRemoteQ5(url)
{
let request = {
msg: "hello from client",
epoch: datetime().epoch
};
let res = http.req(url,
string(request), // POST request payload
1000, // timeout
false // no encryption
);
if(res.err === 0 && res.code === 200) // Valid response, process it!
{
let payload = JSON.parse(res.payload); // Parse text payload into a JSON object
print("Remote Q5 Battery Voltage",payload.batteryVoltage);
print(string(payload)); // Print the entire payload to the log
} else {
print("Error reading data from remote Q5");
print(string(res));
}
print("");
}
while(run()){
// Request data from a remote Q5 running the json web server demo, change the URL to the appropriate URL of your device for testing
getDateFromRemoteQ5("http://172.16.220.39/app/abc");
let res = http.req("http://172.16.220.39/app/xyz",
"", // No Payload
1000, // 1s timeout
false // no aes encryption
);
print(string(res));
waitMS(5000);
}Turns the script into a JSON REST server. Access it in a browser at
http://<Q5 IP>/app/abc.
// Example JSON rest webserver
// Access the test webserver in your browser at http://<Q5 IP>/app/abc
setEnv("http.aesKey","myAesPassphrase"); // Set end to end encryption key (Only used if encryption is enabled), if not set the system password is used
function handleHttp(req){
print(string(req));
if(req.url === 'abc')
{
let reply = {
batteryVoltage: 14.8,
io: io,
uptime: now(),
epoch: datetime().epoch
};
return string(reply);
}
return "Invalid Request URL, try /app/abc";
}
http.handle(handleHttp, // request handler funciton
false); // no aes encryption
while(run()){
waitMS(1000); // wait 1 second
}Streams JSON measurements to a server by POSTing on an interval, with upload error handling. The server can reply with commands (e.g. relay changes).
// Example stream JSON measurements to server
function sendMeasurement(host){
let payload = {
batteryVoltage:14.3,
uptime: now()
};
let res = http.req(host, // url
string(payload), // payload
1000, // timeout
false); // aes encryption
if(res.err === 0 || res.code === 200){
print("Received from server",string(res));
// Could receive messages back from server to change relays etc... See web client example for more info
} else {
print("Upload Error, http status=",res.code);
}
}
while(run()){
sendMeasurement("http://myserver.com/url");
// server will receive a POST request with the json payload specified
waitMS(5000);
}Streams JSON packets to a UDP/syslog target once per second.
// UDP Stream Example
while(run()){
let payload = {
batteryVoltage: 15.3,
uptime: now()
};
udp.send("http://192.168.5.159:2115",string(payload));
waitMS(1000);
}Publishes telemetry and subscribes to command topics with a callback.
Reconnects automatically by calling mqtt.connect() each loop (code 207 means
already connected). Requires firmware V91 or greater.
// MQTT Publish and Subscribe Example, Requires Firmware V91 or greater
// Callback function which runs whenever a subscribed topic is received
// The 'event' object contains the topic info, the message can be binary
// data or a json string, json strings should be parsed, let data = json.parse(event.message);
// binary data can be converted into numerical arrays, see the binary data example
function mqttSubscriberCallback(event) {
print("MQTT EVENT: ", string(event));
// MQTT EVENT: {"retain":false,"qos":0,"message":"Hello World","topic":"power_system/relays/1"}
}
while (run()) {
// Connecting before each publish ensures that if the connection is ever disconnected it will reconnect,
// If the client is connected it will just return code 207 (Already Connected)
// return: Any response other than 0 indicates an error and the corresponding error code
let ret = mqtt.connect({
url: "mqtt://172.16.220.22:1883",
client: "Nikiah Repeater",
subscribe: ["power_system/relays/+"],
callback: mqttSubscriberCallback
});
print("connect:", ret);
let batteryVoltage = string(14.34);
// Publish an event to the MQTT broker, the message data can be binary or text, if you are pushing an object it should
// be converted to a string first, let msg = string( { object: "value" } );
// arrays of numbers can be converted and packed to binary first, see the binary data examples
ret = mqtt.publish("power_system/battery_voltage", batteryVoltage);
print("publish:", ret);
waitMS(5000);
}
mqtt.disconnect(); // Make sure the client is disconnected before exitingAdapts the measurement and upload intervals based on how full the measurement
queue is (__status().mqueue), allowing more measurements to be stored during
communication outages.
// This function adjusts the measurement interval dependong on how full the queue is, adjusts the measurement interval to allow for storing more measurements during comm outages
function checkMeasurementInterval(){
if(__status().mqueue > 75){ // 75% full
setEnv("sys.meas_interval",3600);// 1 hr meas interval
setEnv("sys.sync_interval",300);// 5 min upload
} else if(__status().mqueue > 10){ // 10% full
setEnv("sys.meas_interval",300);// 5 min interval
setEnv("sys.sync_interval",300); // 5 min upload
} else { // Default Measurement & Upload Interval
setEnv("sys.meas_interval",60);
setEnv("sys.sync_interval",60);
}
}
while(run()){
checkMeasurementInterval();
}Derives an AES-256 key from a passphrase with bin.sha256(), encrypts a JSON
payload, decrypts it and validates the checksum.
while(run()){
let aesKey = bin.sha256("flexscadaFlexsQ5!");
print("KEY: ", bin.toHex(aesKey));
let secretMsg = string( {msg: "I LOVE FLEXSCADA", code: 12.454 } );
let encrypted = bin.enc(secretMsg,aesKey);
let decrypted = bin.dec(encrypted,aesKey);
print("Size (Raw, Encrypted)",secretMsg.length,encrypted.length);
if(decrypted.valid){// decryption successful and checksums valid
let decryptedObj = JSON.parse(decrypted.data);
print(string(decryptedObj));
} else {
print("AES Decryption Failed.");
}
waitMS(1000);
}Connects to an Allen-Bradley style PLC over EtherNet/IP, reads four floats from
file F103, parses them with the binary reader (with unit conversion), and
tracks data freshness.
let plc = {lastUpdated:0};
while(run()){
// eip.disconnect();
if(eip.connect("192.168.168.1") === 0){ // 0 = no error
let res = eip.read("F103",1,4*4); // File: F103, Offset: 1, Bytes: 4*8 (4x 4 byte floats)
// Files can be prefixed with F,O,I,S,B,T,R,N depending on type
if(res.status === 0){ // 0 = no error
res.data.seek(0); // establish our binary reader cursor to parse the input data
plc = {
flow: res.data.read("F32ABCD"), // Parse the bytes into our object
pressure: res.data.read() * 0.145038,// kPA to PSI,
pumpSpeed: res.data.read(),no read format specified. Use the last defined read format
temp: res.data.read(),
lastUpdated:now()
};
print( string (plc) ); // Print our result data
} else {
print("PLC Read Failed");
}
} else {
print("PLC Connection Failed");
let updatedAgo = now() - plc.lastUpdated;
print("Last Updated ",updatedAgo, " S Ago");
}
eip.disconnect(); // End the connection
waitMS(1000);
}Creates a Modbus slave register table backed by a bin buffer that the script
updates and remote Modbus TCP clients can read and write. Includes modpoll
test commands.
// This example creates a modbus data table that can be updated from logic and be read and written remotely via modbusTCP
let mbData = bin.alloc(20*4); // Allocate a modbus buffer with room for 18 float registers
modbus.setData(mbData); // Set the modbus data table to use our variable
while(run()){
if(modbus.setLock(true)){ // Lock the table from reads while data is being updated
mbData.seek(0); // set the file position to the begining of the data table (byte 0)
// write some values to our register table
mbData.write("F32BADC",[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); // append an array of values as float32's
mbData.write("U32BADC",23.33); // append a single value as a unsigned 32 bit int with badc byte order
// read 2 values back from the end of our register table (these values could be written to remotely using modbus set the holding register functions)
let reg17_18 = mbData.read("F32BADC",2);
print(string(reg17_18)); // print the values;
modbus.setLock(false); // unlock the data table so that clients can read and write again
}
waitMS(1000);
}
// Test using the free modpoll tool https://www.modbusdriver.com/modpoll.html
// read our devices register table
// modpoll.exe -0 -m tcp -t 4:float -r 0 -c 18 -a 0 -1 192.168.1.20
// write to the last two floats in our register table
// modpoll.exe -0 -m tcp -t 4:float -r32 -u -a 0 -1 192.168.1.20 123.456 456.789Connects to a Modbus TCP slave, reads 30 registers from a weather station
(parsing mixed U32/S32 values with scaling), and writes multiple registers
packed with bin.fromArray().
// Please Note, Function Codes 1,2 and 15 expect a bit array
// === TRY CONNECT ===
let ret = modbus.connect("172.16.220.111",502,1); // Connect to the modbus master
if(ret === 0){
// === READ TEST ===
let ret = modbus.read(0, // port: 0=tcp, 1=ttl serial, 2=rs485
1, // slave unitID
4, // function, 3 = read multiple registers
0x88B9, // start register
30 //register / coil count
);
if(ret.status === 0){
ret.data.seek(0);
let data = {
meanWindSpeed: ret.data.read("U32DCBA")/10, // m/s
meanWindDirection: ret.data.read("U32")/10, // degress
airTemp: ret.data.read("S32")/10, // °C
internalTemp: ret.data.read("S32")/10, // °C
acousticTemp: ret.data.read("S32")/10, // °C
rawTemp: ret.data.read("S32")/10, // °C
relativeHumidity: ret.data.read("U32")/10, // %rh
dewpoint: ret.data.read("S32")/10, // °C
absAirPressure: ret.data.read("U32")/100, // hPa
relAirPressure: ret.data.read("U32")/100, // hPa
};
print(string(data));
} else {
print("Read Failed, err=",string(ret));
}
// === WRITE TEST ===
let arrayData = [1,2,3,4,5,6,7,8];
let data = bin.fromArray(arrayData,"U16","AB"); // Convert array into packed binary structure
ret = modbus.write(0, // port: 0=tcp, 1=serial,
1, // slave unitID
16, // function, 16 = write multiple registers
0, // start register
arrayData.length, // register / coil count
data // data to write
);
if(ret === 0){
print("Write Successful");
} else {
print("Modbus Write Failed, err=",ret);
}
modbus.disconnect();
} else {
print("Modbus Connect Failed, err=",ret);
}Registers a critical fault with a 5-second trigger delay using alm.e(),
prints its live value each loop, and shows the group/summary queries
(alm.v, alm.fV, alm.list, alm.max) plus resetting and acknowledging
faults with alm.rst/alm.ack.
let startTime = now();
while(run()){
// Set the current alarm group
alm.g("UV");
// Evaluate an alarm condition (also returns the current priority of the alarm (if active) or 0 if not alarming
let value = alm.e(
alm.crit, // alarm priority
"Reactor A Run Failure", // alarm title
startTime.sAgo() > 5 && startTime.sAgo() < 12, // alarm condition
5, // Alarm trigger delay (Condition must be true for this many seconds before the alarm activates)
true, // Alarm automatically clearable?
0, // Alarm clearing delay (Condition must be (false) AND alarm clearable (true) for this many seconds before the alarm automatically clears)
startTime // Optional argument, the current value e.g. reservoir level etc. Included in log messages which are automatically recorded when faults change states
);
print("Run Time: ", startTime.sAgo(), " Fault Value:", value);
let groupValue = alm.v("UV"); // If multiple faults exist per group this will return the highest priority active alarm. Multiple groups can be included as additional arguments
let faultValue = alm.fV("UV","Reactor A Run Failure"); // Get the value of a specific fault under the "UV" category
let faults = alm.list(); // Return an object with the status of all the registered alarms {"UV":{"Reactor A Run Failure":0}}
let max = alm.max(); // return the highest alarming priority across all groups
waitMS(1000);
}
// Resetting Faults
alm.rst(); // reset all the faults
alm.ack(); // acknowledge all the faults
alm.rst("UV","Reactor A Run Failure"); // Reset a specific fault
alm.ack("UV","Reactor A Run Failure"); // Acknowledge a specific fault.. An optional false argument can be appended to cancel the acknowledgementRound-trips data through the bin conversion functions: packing a bit array
to bytes with "RBITS", converting between binary and hex text with
bin.toHex/bin.fromHex, and packing/unpacking word-swapped 32-bit floats
("F32" + "BADC").
print("==== Convert an array of 16 bits to a binary string of two bytes ====");
{
let bitArray = [ 1,0,0,0,0,0,0,0, 0,1,1,1,1,1,1,1 ]; // true or false could be used
let bytes = bin.fromArray(bitArray,"RBITS");
let hex = bin.toHex(bytes);
print("raw bit array:", string(bitArray) );
print("packed to bytes: ", hex); // 01fe
}
print("");
print("==== Convert two bytes from a hex string into an array of bits ====");
{
let bytes = bin.fromHex("01fe");
let bitArray = bin.toArray(bytes,"RBITS");
print("raw hex: ", bin.toHex(bytes));
print("converted to array: ", string(bitArray));
}
print("");
print("==== Convert an array of numbers to a 32 bit floating point packed data structure ====");
{
let numberArray = [ 12345.6789,100,200,300,400 ];
let bytes = bin.fromArray(numberArray,"F32","BADC");
let hex = bin.toHex(bytes);
print("raw number array:", string(numberArray) );
print("packed to bytes: ", hex);
}
print("");
print("==== Convert packed 32 bit floating point numbers to an array ====");
{
let hex = "e6b74640000042c80000434800004396000043c8"; // Hex output from previous run
let bytes = bin.fromHex(hex);
let numberArray = bin.toArray(bytes,"F32","BADC");
print("raw hex: ", hex);
print("converted to array: ", string(numberArray));
}The extended five-argument form of log(): title, description, a numeric type
for grouping, a priority, and an attached value — particularly useful when
reporting to a Grafana / InfluxDB cloud backend.
// Log a message (useful when using Grafana / influxdb cloud backend)
log(
"Title",
"Description / Message",
3, // type, 0 to 255, used for grouping alarm types
2, // message priority (0..255) user definable or use builtin alm.ok alm.info alm.warn alm.crit
23.43 // alarm value, numeric value attached to alarm, user settable
);
while(run()){
waitMS(1000);
}Listens for AES-256 encrypted JSON packets on UDP port 9000 with
udp.handle() — only a single UDP listener is supported — and merges the
received data into local state with a last-updated timestamp.
udp.handle(9000, function(req){ // listen on port 9000; only a single udp.handle listener is supported
let data = JSON.parse(req.data); // parse the received payload data as JSON
for(let i in data){
if(i === "users"){
users = data[i];
} else {
rem[i] = data[i];
rem[i]._lu = now();
}
}
}, true); // true: use AES-256 encryption
while(run()){
waitMS(1000);
}Answers EtherNet/IP requests from a PLC with eip.handle(): write requests
unpack a status word into individual io flags using bit masks, and read
requests build a binary reply with bin.alloc(), the "BIT" bit-packing
format and "U16AB" registers.
eip.handle(function(req){ // handle incoming EtherNet/IP read and write requests
if(req.data){ // Is this a write request with write data?
print("EtherIP Write Request");
print("EIP File", req.file);
print("EIP Payload:", bin.toHex(req.data));
req.data.seek(0);
let status = req.data.read("U16AB");
io.PALL = {
_lu: now(),
skidInletValveD: (status & (1<<11)) > 0,
skidInletValveC: (status & (1<<10)) > 0,
skidInletValveB: (status & (1<<9)) > 0,
skidInletValveA: (status & (1<<8)) > 0,
chnMasterAlarm: (status & (1<<6)) > 0,
masterAlarm: (status & (1<<5)) > 0,
skidDAlarm: (status & (1<<4)) > 0,
skidCAlarm: (status & (1<<3)) > 0,
skidBAlarm: (status & (1<<2)) > 0,
skidAAlarm: (status & (1<<1)) > 0,
feedEnable: status & 1,
flowAvailable: req.data.read("U16AB") / 0.2777778,
skidAStatus: req.data.read(), // read using the same previously defined structure
skidBStatus: req.data.read(),
skidCStatus: req.data.read(),
};
req.data.seek(100);
io.PALL.inletTurbidity = req.data.read();
} else {
// Read request.
print("EtherIP Read Request");
print("EIP File", req.file);
print("EIP Bytes:", req.bytes); // the byte length of the requested data
let pallW = {
highClearwellAlarm: alm.v("Clearwell") >= alm.warn,
highPressAlarm: alm.fV("PALL Intake","Inlet Pressure High") > 0,
filterOutputPermission: true,
flowRequest: 151.1999 * pallProcess.pumpsRunningCount.limit(1,4) // m3/h
};
let buff = bin.alloc(100); // empty 100-byte buffer; the read/write cursor is positioned on it automatically
buff.write("BIT", [0, io.p304.running, io.p303.running, io.p302.running, io.p301.running,
pallW.highClearwellAlarm, pallW.highPressAlarm, pallW.filterOutputPermission,
0,0,0,0,0,0,0,0]); // write our statuses as bits
buff.write("U16AB", [pallW.flowRequest * 0.2777778,
io.clearwellLevel701.inst * 10]); // flow request + clearwell level as U16s
return buff; // return our binary data
}
});Persists operator setpoints and a users table in battery-backed memory with
nv.get()/nv.set() — merging saved values over code defaults so newly
added setpoints keep their defaults, and splitting the 4 kB between two
objects with a byte offset.
let sp = {
dialer: 0,
p101Hrs: 0,
p102Hrs: 0,
p301Hrs: 0,
p302Hrs: 0,
p101Mode: 2,
p102Mode: 2,
p301Mode: 2,
p302Mode: 2,
// booster process
millionGallonResPumpCall1: 85,
millionGallonResPumpCall2: 80,
millionGallonResFull: 95,
clearwellLow: 50,
clearwellResume: 60,
boosterPumpSpeed: 57,
boosterMinPumpFlow: 88 // per pump running 100% speed
};
let users = {}; // Blank users list.
let cfg = {
load: function(){
let ssp = nv.get(); // load from non-volatile memory; no offset argument = position 0
if(ssp){ // nv.get() returns undefined if no valid string is detected
// (this can happen if the memory backup battery goes dead)
let nsp = JSON.parse(ssp); // parse the saved setpoint string as a JSON object
for(let key in nsp){ sp[key] = nsp[key]; }
// Merging key-by-key (instead of sp = nsp) means setpoints added to the
// code later keep their defaults when an older saved config is loaded.
} else {
print("Loaded Default Setpoints");
cfg.save();
}
let u = nv.get(2048); // load the users object at a 2 kB offset —
// 4 kB total, so we split it in half
if(u){
users = JSON.parse(u); // parse the users string for our user authentication
}
},
save: function(){
nv.set(string(sp));
nv.set(string(users), 2048);
},
};
cfg.load(); // run at the beginning of the script, after setpoint and user declarations
cfg.save(); // run whenever setpoints or users are changedInitialises the CAN bus at 125 kbps, prints every received frame from the
can.listen() callback, and transmits a test frame once per second with
can.tx().
can.init(125, false, false); // data rate (kbps), canFD, auto retransmission
can.listen(function(msg){ // CAN rx handler; msg = {id:164242, data:"hello from canbus"}
print(string(msg));
});
while(run()){
waitMS(1000);
can.tx(
12, // id
"Test Payload", // payload
false, // extended id
false, // canFD
false // remote frame (RTR)
);
}Sends AES-256 encrypted, checksummed measurements between devices over
RS-485: objects are serialised with string(), encrypted with bin.enc(),
hex-encoded with bin.toHex() and framed with a carriage return; the receive
callback reverses each step and validates the checksum.
let port = 1; // Available ports vary depending on the device. Refer to the serial.init documentation
let aesKey = bin.sha256("password");
function serialRxCallback(msg){
print("Received Serial Msg");
print("Port:", msg.port); // port of incoming msg
print("Overflowed:", msg.ovf); // msg.ovf is boolean true / false
print("Data:", bin.toHex(msg.data));
if(msg.data.length && !msg.ovf){ // received a complete message
let payload = bin.fromHex(msg.data); // convert the hex message back to binary
let decrypted = bin.dec(payload, aesKey); // decrypt the encrypted message
if(decrypted.valid){ // decryption successful and checksums valid
// For binary payloads we could parse directly, e.g.:
// decrypted.data.seek(0); let value = decrypted.data.read("F32ABCD");
let decryptedObj = JSON.parse(decrypted.data);
print(string(decryptedObj));
} else {
print("AES Decryption Failed.");
}
}
}
function transmitMeasurements(){
let data = { message: "Hello World!", voltage: 13.45, ch1: 3.99 };
let payload = string(data); // convert the object to a string
let encrypted = bin.enc(payload, aesKey); // encrypted data includes a 256-bit checksum
let hex = bin.toHex(encrypted); // hex-encode for transmission over serial
serial.tx(port, hex + "\r"); // append a carriage return to the end
}
serial.init(port, 19200);
serial.listen(port, "\r", serialRxCallback); // buffer rx until a carriage return arrives
while(run()){
transmitMeasurements();
waitMS(1000);
}Minimal RS-485 usage: initialise the port, print every carriage-return delimited message that arrives, and transmit a text line once per second.
let port = 1; // Available ports vary depending on the device. Refer to the serial.init documentation
function serialRxCallback(msg){
print("Received Serial Msg");
print("Port:", msg.port); // port of incoming msg
print("Overflowed:", msg.ovf); // msg.ovf is boolean true / false
print("Data:", msg.data);
}
serial.init(port, 19200);
serial.listen(port, "\r", serialRxCallback); // buffer rx until a carriage return arrives
while(run()){
serial.tx(port, "Hello World over serial!\r");
waitMS(1000);
}Polls a Modbus RTU slave directly over the RS-485 port: initialise the port
with serial.init(), then use modbus.read() with the matching port number
and parse the returned registers as word-swapped floats.
let port = 1; // Available ports vary depending on the device. Refer to the serial.init documentation
let ret = serial.init(port, 9600);
while(run()){
let res = modbus.read(port,
1, // slave unitID
3, // function code
8, // start register
10 // register count
);
print(res.status);
if(res.status === 0){
res.data.seek(0);
let registers = res.data.read("F32BADC", 8);
print("voltage (reg0)=", registers[0]);
}
waitMS(1000);
}Demonstrates storing a JSON configuration file with setpoints etc to a sd card.
let sp = {
pumpMode:0,
resFull:99,
resCall:80,
resLow:50,
minFlow:150,
maxFlow:320,
pumpHours:244
};
let cfg = {
changed:false,
lastSaved:now(),
load: function(){
let res = sd.read("setpoints.json");
if(!res.error){
sp = JSON.parse(res.data);
} else {
print("Failed to load configuration: ", string(res));
}
},
save:function(){
if(this.changed || this.lastSaved.sAgo() > 86400){ // save the setpoints immediately if changed otherwise daily to write the pump hours
let res = sd.write("setpoints.json",string(sp));
// Save a backup at least once a month in a seperate year month labeled file
let date = datetime();
let dateString = '/LOG/sp-' + string(date.year) + '-' + string(date.month) + '.json';
sd.write(dateString,string(sp));
if(res){
log("Failed to save setpoints to SD Card",alm.warn);
return false;
}
return true;
}
}
};
cfg.load(); // Load the configuration initially
while(run()){
waitMS(1000);
cfg.save();
}