← Back to interactive docs Use your browser's Print dialog and choose “Save as PDF” to produce a manual.
Flexs Q5 Pro

Flexs Q5 Pro

Scripting Language Manual

Proven site monitoring and control

Covers firmware V127 and later

Generated 2026-07-26

Contents

  1. Language & Built-in Types
    1. The mJS Language
    2. String methods
    3. Number methods
    4. Array methods
  2. System & Environment
    1. Global Functions
    2. Inputs and Outputs
  3. Data Logging & Storage
    1. sd
  4. Networking
    1. http
    2. udp
    3. mqtt
    4. ftp
  5. Industrial Protocols
    1. modbus
    2. eip
    3. serial
    4. can
  6. Binary Data & Encoding
    1. bin
    2. JSON
  7. Math & Utilities
    1. Math
  8. Alarms & Events
    1. alm
  9. Example scripts

Introduction

The Flexs Q5 Pro is configured through its on-board web interface using an editable script. Scripts are interpreted with mJS, a restricted JavaScript engine for embedded systems — most everyday JavaScript syntax works, but there is no dynamic eval, no closures over freed objects, and the standard library is the set of device functions documented here.

Script lifecycle

A script starts automatically at boot and restarts approximately 1 second after it exits or crashes. Continuous loops must be bound to the run() function so the device can stop the script during configuration updates:

while(run()){
  // your monitoring / control logic
  waitMS(100);
}

Warning — deployment safety. Bad scripts can cause unexpected behavior including device restarts, lockups and relay state changes. Always test thoroughly in a non-critical environment before deploying. Give the script enough runtime to confirm proper operation before committing configuration changes, so the device can boot back into a known-good configuration if a critical error occurs.

The 5-second shutdown rule

Scripts must not block for more than 5 seconds after run() returns false. If your script does not exit within that window it will not be updated during configuration changes, a device restart will be required, and unexpected behavior may result because objects referenced by the script can become undefined after the configuration changes.

Language & Built-in Types

The mJS scripting language, and the methods carried by strings, numbers and arrays.

The mJS Language

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:

  • Any valid mJS code is valid ES6 code.
  • Any valid ES6 code is not necessarily valid mJS code.

Restrictions

  • No standard library. No 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.
  • No closures, only lexical scoping (nested functions are allowed).
  • No exceptions.
  • No new. To create an object with a custom prototype use Object.create(), which is available.
  • Strict mode only.
  • No var, only let.
  • No for..of, arrow functions (=>), destructuring, generators, proxies or promises.
  • No getters, setters, valueOf, prototypes, classes or template strings.
  • No == or != — only === and !==.

Byte strings

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.

String methods

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.length

string.lenght -> number

Get the length of a string

Returns

number string length (bytes)

Example

let a = "Hello World";

print( a.length ); // 11

string.number()

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.

Returns

number Number value of string

Example

let text = "123.456";
let number = text.number();
print( number ); // 123.456

string.slice()

'some_string'.slice(start, end) → string

Returns a substring between two indices.

Arguments

NameTypeRequiredDefaultDescription
start number yes — Start index (inclusive).
end number yes — End index (exclusive).

Returns

string The substring.

Example

'abcdef'.slice(1,3) === 'bc';

string.at()

'abc'.at(index) → number

Returns the numeric byte value at a string index.

Arguments

NameTypeRequiredDefaultDescription
index number yes — Byte index into the string.

Returns

number The byte value at that index.

Example

'abc'.at(0) === 0x61;

string.indexOf()

'abc'.indexOf(substr, [fromIndex]) → number

Returns the index of the first occurrence of a substring, or −1 if not found.

Arguments

NameTypeRequiredDefaultDescription
substr string yes — Substring to search for.
fromIndex number optional 0 Index to start searching from.

Returns

number Index of the first occurrence, or −1 if not found.

Example

'abc'.indexOf('bc') === 1;

chr()

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.

Arguments

NameTypeRequiredDefaultDescription
n number yes — Byte value, 0–255.

Returns

string | null A 1-byte string, or null for invalid input.

Example

chr(0x61) === 'a';

Number methods

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"

number.map()

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.

Arguments

NameTypeRequiredDefaultDescription
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.

Returns

number The mapped (and optionally clamped) value.

Example

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

number.limit()

x.limit(min, max) → number

Clamps the number between a lower and upper bound.

Arguments

NameTypeRequiredDefaultDescription
min number yes — Lower bound.
max number yes — Upper bound.

Returns

number The value clamped to [min, max].

Example

let reservoirPercentage = 103;
let limited = reservoirPercentage.limit(0, 100);
// limited = 100

number.toFixed()

x.toFixed([decimals]) → string

Formats the number as a string with a fixed number of decimal places.

Arguments

NameTypeRequiredDefaultDescription
decimals number optional 0 Number of decimal places.

Returns

string The formatted value.

Example

let pressure = 123.34224322;
let stringValue = pressure.toFixed(2); // "123.34"

Array methods

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.)

array.splice()

a.splice(start, deleteCount, [item1, item2, …])

Changes the contents of an array by removing existing elements and/or inserting new ones.

Arguments

NameTypeRequiredDefaultDescription
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.

Example

let a = [1,2,3,4,5];
a.splice(1, 2, 100, 101, 102);
// a === [1,100,101,102,4,5]

System & Environment

Script lifecycle, timing, device status and runtime configuration.

Global Functions

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()

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.

Returns

boolean true while the script should continue; false when it must shut down.

Example

while(run()){
  // monitoring logic here
  waitMS(100);
}
// falls through here during a configuration update — exit promptly

waitMS()

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().

Arguments

NameTypeRequiredDefaultDescription
milliseconds number yes — How long to pause, in milliseconds.

Example

while(run()){
  // take measurements ...
  waitMS(5000); // repeat every 5 seconds
}

print()

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.

Arguments

NameTypeRequiredDefaultDescription
value… any yes — One or more values to log. Objects should be wrapped in string() to serialise them.

Example

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()

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
);

Arguments

NameTypeRequiredDefaultDescription
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).

Example

// Short form
log("Logic Started", alm.info); 

// Extended form
log("Pump Fault", "Discharge pressure out of range", 3, alm.warn, 23.43);

setEnv()

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.

Arguments

NameTypeRequiredDefaultDescription
key string yes — Environment variable name (see table above).
value string | number optional — New value. Some keys, like "log.clear", take no value.

Example

setEnv("led", "green");          // status LED
setEnv("sys.utc_offset", -8);    // Pacific time
setEnv("sys.meas_interval", 60); // measure every minute

now()

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

Returns

timestamp Uptime in seconds. Also provides .sAgo() → seconds elapsed since this timestamp was taken.

Example

let uptime = now();
  print('Uptime Seconds: ',uptime);

datetime()

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.

Returns

object e.g. {"valid":true,"uptime":1437.05,"epoch":1589247811,"year":2020,"month":5,"monthDay":12,"weekDay":2,"hour":1,"minute":43,"second":31}

Example

print(string(datetime()));
let ts = datetime().epoch;           // unix timestamp
let year = datetime().year;          // e.g. 2020

string()

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().

Arguments

NameTypeRequiredDefaultDescription
value any yes — Number, object or other value to serialise.

Returns

string String / JSON representation of the value.

Example

let payload = { batteryVoltage: 14.3, uptime: now() };
http.req("http://myserver.com/url", string(payload), 1000, false);

__status()

__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.

Returns

object e.g. {"vcc":23.81,"name":"Flexs Vibration Monitor","uid":2585356324,"hw_v":9001,"fw_v":10,"cfg_v":164,"mqueue":0}

Example

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}

Inputs and Outputs

Methods to read channel values and set relay states.

getFeed()

getFeed(id | name) → number

Get custom feed value

Custom feeds can be used to log or visualise computed values from logic script. They can be read and written from the onboard web interface and viewed from the onboard display.

Arguments

NameTypeRequiredDefaultDescription
id number | string yes — ID of the custom feed to query. Can also supply a case insensitive string

Returns

number | bool Current custom feed value

Example

let value = getFeed(0);

setFeed()

setFeed(id | name,value)

Set custom feed value

Set the value of a custom feed.

Arguments

NameTypeRequiredDefaultDescription
id | name number | string yes — Feed ID
value number | bool yes — Feed Value

Example

let watts = inputs("voltage").inst * inputs("amperage").inst; // calculate watts from our voltage and amperage

setFeed(0,watts); // set the custom feed value



setFeed(1,true); // Set a boolean type feed. Could also use 1 or 0 inplace of true or false

inputs()

inputs(ch | label) → object

Read the current value of an analog input channel

Return the current value of an analog input channel as an object in the form of {"ripple":0.001878,"state":false,"max":0.001997,"min":-0.000126,"inst":0.001106,"avg":0.000976}

The inst child contains the current realtime value. Aggregated metrics are computed based on the configured measurement interval.

Arguments

NameTypeRequiredDefaultDescription
channel | label number | string yes — Input channel number | input channel name

Returns

Current value

Example

let voltage = inputs(1).inst;

let current = inputs("current").avg; 

let state = inputs("switch").state; // for inputs configured as discrete

powermetrics()

powermetrics(id | label) → object

Read the current value of a power metric.

Returns the current value as an object.

Simplified Form Result:

{ "thd": 3.2, "pf": 0.95, "freq": 59.8, "voltage": 225.34, "current": 1.2, "realpower": 230.4 }

Advanced Form Result: (advanced arg = true) { "label": "Total Power Con", "thd": 2.833947, "pf": 0.999362, "freq": 60.208321, "voltage": { "inst": 214.299423, "avg": 213.813995, "max": 216.401474, "min": 212.065689 }, "current": { "inst": 24.0798, "avg": 23.954132, "max": 24.357168, "min": 23.739422 }, "realpower": { "inst": 5157.308594, "avg": 5118.462891, "max": 5264.208984, "min": 5046.496094 } }

Arguments

NameTypeRequiredDefaultDescription
id | label number | string yes — Power Metric ID or Label
advanced bool optional false

Returns

Current Measurement Values

Example

let watts = powermetrics(0).realpower; // simple usage, returns average values


let maxWatts = powermetrics(0,true).realpower.max; // advanced usage

tempsensors()

tempsensors(id) → object

Read the current value of an attached one wire temp sensor.

Sensors can be addressed by the index # (relating to the order defined on the sensors configuration page) or the UID (unique sensor id on sensor label)

Result Format: {"lastUpdated":3,"avg":14.900000,"inst":15.125000}

Arguments

NameTypeRequiredDefaultDescription
id | label number | string yes — Index, UID or Label (string)

Returns

object Return result

Example

let indoorTemp = tempsensors(0).inst;

relays()

relays(ch) → object

Get the current relay state

Returns an object with the current relay information in the form of an object {"lvd":false,"hvd":false,"fuse":false,"state":false}

Arguments

NameTypeRequiredDefaultDescription
channel | label number | string yes — Relay channel # or label

Returns

object {"lvd":false,"hvd":false,"fuse":false,"state":false}

Example

let loadSwitch = relays(1).state;

setRelay()

setRelay(ch,state)

Set the relay state

Arguments

NameTypeRequiredDefaultDescription
channel | label number | string yes — Channel # or Label
state bool yes — State, True or False. 1 or 0

Example

setRelay(1,true); // turn relay 1 on

Data Logging & Storage

Recording measurements to the data logger, the memory card and non-volatile memory.

sd

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()

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.

Arguments

NameTypeRequiredDefaultDescription
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.

Returns

number 0 = no error, >0 = error code

Example

// Timestamped log line: 1589247811:    ch3: 23.454
sd.append('log.txt', string(datetime().epoch) + ":    ch3: " + string(23.454) + "\n");

sd.read()

sd.read(file) -> object

Read file contents from SD Card

Read a file from the sd card and returns the result

Arguments

NameTypeRequiredDefaultDescription
filename string yes — e.g. "config.json"

Returns

object {error:0,data:"file contents"} 0 = No Error, data = file contents as a string

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
filename string yes — e.g. "config.json"
data string yes — Data to write to the file

Returns

number 0 = no error, >0 = error code

Example

sd.write("hello.txt","Hello World!");

Networking

HTTP, UDP and MQTT communication with servers, brokers and other devices.

http

HTTP client requests and an on-device JSON web server under /app/.

The http object provides both directions of HTTP:

  • Client — http.req() sends requests to external servers or to other devices (including other Q5 units running a script web server).
  • 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()

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).

Arguments

NameTypeRequiredDefaultDescription
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", …)).

Returns

object { err, code, payload } — err 0 and code 200 indicate success; payload is the response body text.

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
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", …)).

Example

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); }

udp

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()

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.

Arguments

NameTypeRequiredDefaultDescription
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

Returns

number 0 = success, >0 = error code

Example

// 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()

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.

Arguments

NameTypeRequiredDefaultDescription
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.

Example

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

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()

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.

Arguments

NameTypeRequiredDefaultDescription
options object yes — { url, client, subscribe: [...], callback } — see table.

Returns

number 0 = connected, 207 = already connected, anything else = error code.

Example

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()

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().

Arguments

NameTypeRequiredDefaultDescription
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().

Returns

number 0 on success, otherwise an error code.

Example

let ret = mqtt.publish("power_system/battery_voltage", string(14.34));
print("publish:", ret);

mqtt.disconnect()

mqtt.disconnect()

Disconnects from the MQTT broker; call before the script exits.

Make sure the client is disconnected before the script exits.

Example

mqtt.disconnect();

ftp

ftp.put()

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.

Testing

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.

Security Considerations

FTP is considered an insecure file transfer protocol, use only on a private or restricted network.

Arguments

NameTypeRequiredDefaultDescription
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

Returns

number 0 = success, >0 = error code

Example

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
}


Industrial Protocols

Modbus, EtherNet/IP, CAN bus and RS-485 serial connectivity to PLCs and instruments.

modbus

Modbus TCP/serial master and a script-managed slave register table.

The modbus object works in two directions:

  • Master — 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).
  • Slave data table — 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()

modbus.connect(host, port, unitId) → number

Opens a Modbus TCP connection to a remote device.

Arguments

NameTypeRequiredDefaultDescription
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.

Returns

number 0 on success, otherwise an error code.

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
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.

Returns

object { status, data } — status 0 = success; data is a bin buffer.

Example

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()

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).

Arguments

NameTypeRequiredDefaultDescription
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").

Returns

number 0 on success, otherwise an error code.

Example

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()

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().

Arguments

NameTypeRequiredDefaultDescription
buffer bin buffer yes — Buffer from bin.alloc(bytes) sized for your registers (4 bytes per 32-bit value).

Example

let mbData = bin.alloc(20*4);  // room for float registers
modbus.setData(mbData);        // expose as the slave data table

modbus.setLock()

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.

Arguments

NameTypeRequiredDefaultDescription
locked boolean yes — true to lock the table, false to unlock.

Returns

boolean true when the lock state was applied.

Example

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()

modbus.disconnect()

Closes the master connection to the remote Modbus device.

Example

modbus.disconnect();

eip

EtherNet/IP client for reading data files from Allen-Bradley style PLCs.

The eip object speaks EtherNet/IP in both directions:

  • Client — 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.
  • Server — 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()

eip.connect(ip) → number

Connects to a PLC over EtherNet/IP.

Arguments

NameTypeRequiredDefaultDescription
ip string yes — PLC IP address, e.g. "192.168.168.1".

Returns

number 0 = connected, otherwise an error code.

Example

if(eip.connect("192.168.168.1") === 0){
  // read files ...
  eip.disconnect();
} else {
  print("PLC Connection Failed");
}

eip.read()

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().

Arguments

NameTypeRequiredDefaultDescription
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.

Returns

object { status, data } — status 0 = success; data is a bin buffer.

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
handler function yes — Callback function(req){ … } — return a bin buffer to answer read requests.

Example

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()

eip.disconnect()

Ends the EtherNet/IP connection.

Example

eip.disconnect();

serial

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 Expansion Serial TTL Port
2 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()

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, 3: Activates direction signalling on the expansion IO port for use with RS485 transceivers

:Swap Swap the TX RX lines on the expansion port

:Invert Invert the RS485 high low signaling (Experimental)

Arguments

NameTypeRequiredDefaultDescription
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

Returns

number Status code — 0 on success.

Example

let port = 1;             // isolated RS-485
serial.init(port, 19200);

serial.listen()

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.

Arguments

NameTypeRequiredDefaultDescription
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.

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
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().

Example

serial.tx(port, "Hello World over serial!\r");

can

CAN bus interface: initialise the bus, receive frames with a callback, and transmit standard or extended frames.

The can object connects the device to a CAN bus. Initialise the bus with a data rate, register a receive handler, and transmit frames with can.tx() — including extended IDs, CAN FD and remote (RTR) frames.

can.init()

can.init(dataRate, canFD, autoRetransmit)

Initialises the CAN bus interface.

Arguments

NameTypeRequiredDefaultDescription
dataRate number (kbps) yes — Bus data rate in kbps, e.g. 125, 250, 500.
canFD boolean yes — Enable CAN FD framing.
autoRetransmit boolean yes — Enable automatic retransmission.

Example

can.init(125, false, false); // 125 kbps, classic CAN, no auto retransmission

can.listen()

can.listen(callback)

Registers a receive handler for incoming CAN frames.

The callback receives a message object with the frame id and data payload. Decode binary payloads with the bin functions.

Arguments

NameTypeRequiredDefaultDescription
callback function yes — Callback function(msg){ … } — msg = { id, data }.

Example

can.listen(function(msg){
  print(string(msg)); // {"id":164242,"data":"hello from canbus"}
});

can.tx()

can.tx(id, payload, extendedId, canFD, remoteFrame)

Transmits a CAN frame.

Arguments

NameTypeRequiredDefaultDescription
id number yes — Frame identifier.
payload string yes — Frame payload — pack binary data with the bin functions.
extendedId boolean yes — true for a 29-bit extended identifier.
canFD boolean yes — true to send as a CAN FD frame.
remoteFrame boolean yes — true to send a remote (RTR) frame.

Example

can.tx(
  12,             // id
  "Test Payload", // payload
  false,          // extended id
  false,          // canFD
  false           // remote frame (RTR)
);

Binary Data & Encoding

Packing, parsing and converting binary buffers, JSON and strings.

bin

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()

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.

Arguments

NameTypeRequiredDefaultDescription
bytes number yes — Buffer size in bytes — 4 bytes per 32-bit register/float.

Returns

string A buffer with seek/read/write methods, cursor positioned at byte 0.

Example

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() / .read() / .write()

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.

Arguments

NameTypeRequiredDefaultDescription
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.

Example

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()

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().

Arguments

NameTypeRequiredDefaultDescription
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.

Returns

buffer Packed binary data ready for modbus.write(), mqtt.publish() or hex conversion.

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
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.

Returns

array The unpacked values.

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
hex string yes — Hex text, e.g. "01fe".

Returns

binary The decoded binary data.

Example

let bytes = bin.fromHex("01fe");
print(bin.toHex(bytes)); // 01fe

bin.toHex()

bin.toHex(data) → string

Converts binary data to a printable hex string.

The inverse is bin.fromHex().

Arguments

NameTypeRequiredDefaultDescription
data buffer | string yes — Binary data, e.g. a sha256() digest or packed array.

Returns

string Hexadecimal representation.

Example

let aesKey = sha256("flexscadaFlexsQ5!");
print("KEY: ", bin.toHex(aesKey));

bin.enc()

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.

Arguments

NameTypeRequiredDefaultDescription
data string yes — Plaintext. Serialise objects first with string(obj).
key binary yes — 256-bit key, e.g. from bin.sha256(passphrase).

Returns

binary Encrypted data (slightly larger than the plaintext).

Example

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);

bin.dec()

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", …)).

Arguments

NameTypeRequiredDefaultDescription
data binary yes — Encrypted data from bin.enc() or a peer.
key binary yes — The 256-bit key used to encrypt.

Returns

object { valid, data } — valid is true when decryption succeeded and checksums match; data is the plaintext.

Example

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()

bin.crc16(data,len) → number

Modbus CRC16 checksum function

Generate a crc16 checksum from a chunk of data

Arguments

NameTypeRequiredDefaultDescription
data string yes —
len number optional data.length

Returns

number Checksum Value

Example

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()

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!)

Arguments

NameTypeRequiredDefaultDescription
data string yes — Data to hash, e.g. a passphrase.

Returns

binary 32-byte digest, usable directly as an AES-256 key. Print with bin.toHex().

Example

let aesKey = sha256("flexscadaFlexsQ5!");
print("KEY: ", bin.toHex(aesKey));

JSON

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()

JSON.parse(text) → object

Parses a JSON string into an object.

Arguments

NameTypeRequiredDefaultDescription
text string yes — JSON text, e.g. res.payload from http.req or event.message from MQTT.

Returns

object The parsed value.

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
value any yes — The value to serialise.

Returns

string JSON text.

Example

let payload = JSON.stringify({ v: 14.3 }); // '{"v":14.3}'

Math & Utilities

Standard math library plus embedded extensions like value scaling and hardware random numbers.

Math

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()

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.

Returns

number 32-bit hardware random number.

Example

let nonce = Math.random();

Alarms & Events

Alarm condition evaluation, grouping, acknowledgement and structured event logging.

alm

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()

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.

Arguments

NameTypeRequiredDefaultDescription
group string yes — Group name, e.g. "UV".

Example

alm.g("UV"); // conditions evaluated next belong to the "UV" group

alm.e()

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:

  • The condition must stay true for triggerDelay seconds before the alarm activates.
  • If 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().
  • The optional 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.

Arguments

NameTypeRequiredDefaultDescription
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.

Returns

number Current priority of the alarm while active, 0 when not alarming.

Example

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()

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.

Arguments

NameTypeRequiredDefaultDescription
group… string yes — One or more group names.

Returns

number Highest active priority in the group(s), 0 if none alarming.

Example

let groupValue = alm.v("UV");

alm.fV()

alm.fV(group, title) → number

Returns the value of one specific fault.

Arguments

NameTypeRequiredDefaultDescription
group string yes — Group name, e.g. "UV".
title string yes — Fault title within the group.

Returns

number The fault’s current value.

Example

let faultValue = alm.fV("UV", "Reactor A Run Failure");

alm.list()

alm.list() → object

Returns the status of every registered alarm, organised by group.

Returns

object e.g. {"UV":{"Reactor A Run Failure":0}} — 0 means not alarming; otherwise the active priority.

Example

let faults = alm.list();
print(string(faults)); // {"UV":{"Reactor A Run Failure":0}}

alm.max()

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");

Returns

number Highest active priority overall, 0 when nothing is alarming.

Example

let max = alm.max();

alm.rst()

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).

Arguments

NameTypeRequiredDefaultDescription
group string optional — Group of the fault to reset. Omit to reset everything.
title string optional — Title of the fault to reset.

Example

alm.rst();                              // reset all faults
alm.rst("UV", "Reactor A Run Failure"); // reset one fault

alm.ack()

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.

Arguments

NameTypeRequiredDefaultDescription
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.

Example

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

Example scripts

Uptime

Read the device uptime in seconds with now().

let uptime = now();
  print('Uptime Seconds: ',uptime);

System Status

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}

Memory Card & Date/Time

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

}

JSON Web Client

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);

}

JSON Web Server

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
}

JSON Send Measurements to Server

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);

}

UDP Packet Transmission, SYSLOG

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);
}

MQTT Publish / Subscribe

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 exiting

Variable Measurement Interval

Adapts 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();
}

AES256 Encryption / Decryption

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);
}

EtherNet/IP Read

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);

}

Modbus TCP Data Table From Logic

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.789

Modbus TCP Multiple Registers READ / WRITE

Connects 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);
}

Alarm Conditions

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 acknowledgement

Binary Packing, Bits & Hex

Round-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));
}

Structured Event Logging

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);

}

UDP Server / Listener

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);
}

EtherNet/IP Server (PLC Handler)

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
  }
});

CAN Bus Transmit & Receive

Initialises 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)
  );
}

Serial Port: Encrypted Device-to-Device

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);
}

Serial Port: Plain Text

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);
}

Serial Modbus (RS-485)

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);
}

SD Card Configuration File

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();

}