Examples

Data Manipulation and Events

Example - Create the sum of analog1 and analog2 values and send the new value as cp1. Also monitor the sum against predefined thresholds. If a threshold is exceeded, dispatch an event. Note that it is also good practice to check the analog values exist (and are a ‘number’ type) before trying to use them, or the script will fail.

load('senquip.js');
let ALARM_THRESHOLD = 55;
let WARN_THRESHOLD = 20;

SQ.set_data_handler(function(data) {
  // The current measurement data is passed in as a string.
  // To easily access values, convert the string into a JSON object
  let obj = JSON.parse(data);

  // Check measurement values (analog1, analog2) exist in the data object before using them
  // or the script will throw an error and stop execution
  if ((typeof obj.analog1 === "number") && (typeof obj.analog2 === "number")) {

    let sum = obj.analog1 + obj.analog2;
    SQ.dispatch(1, sum);

    if (sum >= ALARM_THRESHOLD) {
      SQ.dispatch_event(1, SQ.ALARM, "Sum critical");
    } else if (sum >= WARN_THRESHOLD) {
      SQ.dispatch_event(1, SQ.WARNING, "Sum high");
    }
  } else {
    SQ.dispatch_event(1, SQ.INFO, "No Data");
  }
}, null);

Example - Scale and offset values current1 and current2, and send them as the new values cp1 and cp5 with varying precision.

load('senquip.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);
  SQ.dispatch_double(1, 42.1*obj.current1 + 1.51, 0); // No decimal
  SQ.dispatch_double(5, -3.1416*obj.current2 + 9, 3); // 3 decimal places
}, null);

Filtering & Persistent Variables

Example - Implementation an exponential moving average across consecutive measurement cycles. Highlights the use of global variables to store information between measurement cycles.

load('senquip.js');
let filtered_value = 0;

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);
  let alpha = 0.3;
  let new_sample = obj.current1;
  filtered_value = (alpha * new_sample) + (1 - alpha)*filtered_value;
  SQ.dispatch(1, filtered_value);
}, null);

Note

The above example using global variables will only work if the device is set to ‘Always On’. Information in global variables is lost when the device sleeps or resets.

Example - Use persistent variables which keep their value when the device sleeps or hibernates. This example filters measurements across sleep cycles and keeps track of a service interval.

load('senquip.js');

// Give each NVS variable an easy to read index
let NVS_FILTER_VALUE = 1;
let NVS_SERVICE_HRS = 2;
let NVS_VALID = 3;

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  let nvs_is_valid = true;
  if (SQ.nvs_load(NVS_VALID) === 0) {
    // NVS values are uninitialised or have been lost
    SQ.nvs_save(NVS_VALID, 1);
    nvs_is_valid = false;
 }

  let alpha = 0.3;
  let new_sample = obj.ambient;
  let filtered_value = SQ.nvs_load(NVS_FILTER_VALUE);
  if (nvs_is_valid) {
    filtered_value = (alpha * new_sample) + (1 - alpha)*filtered_value;
  } else {
    // Initialise filter to the current value if nvs is lost or un-initialised
    filtered_value = new_sample;
  }
  SQ.nvs_save(NVS_FILTER_VALUE, filtered_value);
  SQ.dispatch(1, filtered_value);

  let service_hours = SQ.nvs_load(NVS_SERVICE_HRS);
  SQ.dispatch(2, service_hours);

}, null);

SQ.set_trigger_handler(function(tp) {
  if (tp === 1) {
    // Add 50 to the service hours
    let service_hours = SQ.nvs_load(NVS_SERVICE_HRS);
    SQ.nvs_save(NVS_SERVICE_HRS, service_hours + 50);
  }
}, null);

Parsing CAN Data

Example - Look for a specific PGN, parse the first 2 bytes of CAN data from Hex format, convert to a 16-bit signed value, then apply a fixed scale and offset to the result.

load('senquip.js');

// Function to convert a 16-bit unsigned integer to 16-bit signed value
function int16(x) {
  if (x > 32767) {x = x - 65536;}
  return x;
}

// Extract the PGN from the CAN ID
function pgn(id)
{
  return((id >> 8) & 0x0003FFFF);
}

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  if (typeof obj.can1 !== "undefined") {
    for (let i = 0; i < obj.can1.length; i++) {

      // Look for the specific PGN:
      if (pgn(obj.can1[i].id) === 0xFEC1) {

        // Extract the first 4 Hex characters (2 bytes):
        let d = SQ.parse(obj.can1[i].data, 0, 4, 16);

        // Convert to a signed 16-bit value and scale/offset the result:
        SQ.dispatch_double(1, int16(d) * 0.125 + 4.5, 1);

        // Extract the next 4 Hex characters and reverse the byte order:
        let e = SQ.parse(obj.can1[i].data, 4, 8, -16);

        // Keep result as an unsigned number and scale:
        SQ.dispatch_double(2, e * 0.5, 1);
      }
    }
  } else {
    SQ.dispatch_event(1, SQ.WARNING, "No CAN data available");
  }
}, null);

J1939 Fault Codes

Example - Determine when a J1939 Fault code is active on CAN1 using some funky byte manipulation.

load('senquip.js');

function decodeDTC(data) {
  // Decode DTC according to Conversion Method 4
  let byte3 = SQ.parse(data, 4, 2, 16, SQ.U16);
  let byte4 = SQ.parse(data, 6, 2, 16, SQ.U16);
  let byte5 = SQ.parse(data, 8, 2, 16, SQ.U8);
  let byte6 = SQ.parse(data, 10, 2, 16, SQ.U8);
  let spn = byte3 | byte4<<8 | (byte5 & 0xE0)<<11;
  let fmi = byte5 & 0x1F;
  let oc = byte6 & 0x7F;
  return {spn: spn, fmi: fmi, oc: oc};
}

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  if (typeof obj.can1 !== "undefined") {
    for (let i = 0; i < obj.can1.length; i++) {
      // Look for the fault code PGN from any SA:
      if ((obj.can1[i].id>>8) === 0x18FECA) {
        let dtc = decodeDTC(obj.can1[i].data);
        if (dtc.spn !== 0) {
          let s = "DTC SPN: " + JSON.stringify(dtc.spn) + " FMI: " + JSON.stringify(dtc.fmi);
          SQ.dispatch_event(1, SQ.ALARM, s);
        }
      }
    }
  } else {
    SQ.dispatch_event(1, SQ.WARNING, "No CAN data available");
  }
}, null);

Transmit CAN Data

Example - Regularly send a CAN message, containing measurements from the device.

load('senquip.js');
load('api_timer.js');
load('api_math.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);
  if (typeof obj.analog1 === "number") {
    let value = Math.floor(obj.analog1*100);
    // Use the measured 'value' and convert it into 3 bytes of CAN data
    let can_msg = chr(value & 0xFF) + chr((value>>8) & 0xFF) + chr((value>>16) & 0xFF);
    // Transmit the 3 bytes in can_msg, repeat transmission every 100 milliseconds
    // This can be called every time the data handler runs to update the transmitted value
    CAN.tx(1, 0x18FB1055, can_msg, 3, CAN.EXT + CAN.TX_SLOT(0), 100);
  }
}, null);

// Alternatively, we can set up a repeating timer to send a single CAN transmit each time the timer triggers
// This method creates more processing load, so the above method using CAN.TX_SLOT is preferred
Timer.set(1000, Timer.REPEAT, function() {
  // Send an extended message every 1 second, length is always 3
  CAN.tx(1, 0x18FB1055, "\x01\x02\x03", 3, CAN.EXT);
}, null);

Use of Triggers

Example - Set up a trigger handler to respond to button presses from the Senquip Portal.

load('senquip.js');
load('api_serial.js');

SQ.set_trigger_handler(function(tp) {
  if (tp === 1) { SQ.set_output(1, SQ.ON, 5); }  // Turn on Output 1 for 5s
  if (tp === 2) { SQ.set_output(1, SQ.OFF, 0); } // Turn Output 1 off forever
  if (tp === 3) {
    let s = "HELLO WORLD!";
    SERIAL.write(1, s, s.length);  // Send string over serial port (RS232 or RS485)
  }
  if (tp === 4) { SERIAL.write(1, "\x48\x45\x58", 3); } // Send 3 bytes of HEX data
}, null);

Use of Timers

Example - Set up a trigger handler to immediately send one serial message, and then send a second message one second later.

load('senquip.js');
load('api_timer.js');
load('api_serial.js');

SQ.set_trigger_handler(function(tp) {
  if (tp === 1) {
    // When Portal button #1 is pressed, immediately send 1st serial string
    let s1 = "TURN ON";
    SERIAL.write(1, s1, s1.length);
    // Set a non-repeating timer for 1000 ms, which will call the inline function().
    Timer.set(1000, 0, function() {
      // After one second, send the 2nd serial string
      let s2 = "TURN OFF";
      SERIAL.write(1, s2, s2.length);
    }, null);
  }
}, null);

String Enumerations

Example - Send a string enumeration based on an analog voltage measurement. It also gracefully handles the case where ‘analog1’ does not exist in the data message, in which case ‘typeof obj.analog1’ would have the value ‘undefined’ (rather than ‘number’).

load('senquip.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);
  let s = "No Signal";
  if (typeof obj.analog1 === "number") {
    if (obj.analog1 >= 4.5) { s = "Error"; }
    else if (obj.analog1 >= 3.5) { s = "High"; }
    else if (obj.analog1 >= 2.5) { s = "Normal"; }
    else { s = "Low"; }
  }
  SQ.dispatch(1, s);
}, null);

Send Data with HTTP POST

Example - Send a custom HTTP POST message containing device data to an external endpoint.

load('senquip.js');
load('api_endpoint.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);
  let out = {
    id: obj.deviceid,
    time: obj.ts,
    flow_rate: obj.current1 * 1000
  };
  let result = HTTP.post(JSON.stringify(out), "Content-Type: application/json\r\n");
  if (result === 0) {
    // Success
  } else {
    // Error
  }
}, null);

Request Data from HTTP API

Example - Request weather data from an external HTTPS API. You will need your own API key (Replace <YOUR API KEY>) from visualcrossing.com to use this example.

load("senquip.js");
load("api_endpoint.js");
load("api_timer.js");

let tomorrow = "";
let windSpd = 0;
let windDir = 0;
let http_status = "Not Fetched";

function get_weather() {
  HTTP.query({
    url: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/-32.7168,152.1869?key=<YOUR API KEY>&include=current",
    success: function (body) {
      if (body !== "") {
        // Check if the body is a JSON string, by looking at the first character
        if (body.at(0) !== "{".at(0)) {
          // Not a JSON string, so it is probably a plain text error message from the API
          // Copy the body string using slice, as the body string will not persist after this function returns
          http_status = body.slice(0, -1);
        } else {
          // Parse the JSON string into an object and extract the important parameters
          let weather = JSON.parse(body);
          tomorrow = weather.days[0].description;
          windSpd = weather.days[0].windspeed;
          windDir = weather.days[0].winddir;
          http_status = "Fetched";
        }
      }
    },
    error: function (err) {
      http_status = err;
    },
  });
}

SQ.set_trigger_handler(function (tp) {
  if (tp === 1) {
    get_weather();
  }
}, null);

SQ.set_data_handler(function (data) {
  SQ.dispatch(1, tomorrow);
  SQ.dispatch(2, windSpd);
  SQ.dispatch(3, windDir);
  SQ.dispatch(4, http_status);
}, null);

Custom Settings

Example - Retrieval and use of custom settings from a script. The example also dispatches the settings to the Portal so a record of the current value is kept. This may be desirable for debugging, traceability or reporting purposes.

load('senquip.js');
load('api_config.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  // Get the settings:
  let num_threshold = Cfg.get('script.num1');
  let str_setting = Cfg.get('script.str1');

  // Compare measurement against the custom setting:
  if (obj.analog1 > num_threshold) {
    SQ.dispatch_event(1, SQ.ALARM, "Custom Alarm");
  }

  // Send the custom settings to the Portal for reporting/logging purposes:
  SQ.dispatch(1, num_threshold);
  SQ.dispatch(2, str_setting);
}, null);

Bluetooth

Note

For examples on how to send data to the Senquip Connect mobile app, refer to the Senquip Connect app section

Example - An example of how to transmit and receive GPS data from one device to another using Bluetooth advertisements.

load('senquip.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  // If we have a valid GPS position, broadcast this to other devices via BLE
  if (typeof obj.gps_lat !== "undefined") {
      let ble_data = SQ.encode(obj.gps_lat,SQ.FLOAT) + SQ.encode(obj.gps_lon,SQ.FLOAT);
      BLE.sq_adv(ble_data);
  }

  // Check for BLE data from nearby devices
  if (typeof obj.ble !== "undefined") {
    for (let i = 0; i < obj.ble.length; i++) {
      // Check the message contains the 'Senquip Manufacturer Specific' header
      let sl = obj.ble[i].data.slice(2, 8);
      if (sl === "ff710a") {
        // We have found a message from another device
        // Extract the lat and long of the other device:
        let lat = SQ.parse(obj.ble[i].data,8,8,16,SQ.FLOAT);
        let lng = SQ.parse(obj.ble[i].data,16,8,16,SQ.FLOAT);
        // Calculated the distance to us:
        let meters = SQ.distance(lat, lng);
        SQ.dispatch(1, meters);
      }
    }
  }
}, null);

Files

Example - Log ambient temperature to a CSV file. Roll over to a new file every hour.

load('senquip.js');
load('api_file.js');
load('api_timer.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  // Create a filename containing the date and hour,
  // therefore every hour the filename will change.
  let filename = Timer.fmt("data-%F-%H.csv", Timer.now());

  // Create the new line of data for the file, where the first column is the time
  // and second column is the ambient temperature
  let new_line = Timer.fmt("%T", Timer.now()) + "," + JSON.stringify(obj.ambient) + "\n";

  // Append the new line to the file
  let bytes_written = File.write(new_line, filename, "a");

  // Check for error
  if (bytes_written === 0) {
    // Failed to write file
  }

}, null);

Warning

This example will continue to write files until the filesystem is full. Once the filesystem is full, all further file writes will fail.

Example - Demonstration of reading and writing values to a file. The file remains on the filesystem and is re-loaded every time the device reboots or sleeps. Note: frequent file writes will cause the FLASH memory to wear.

load('senquip.js');
load('api_file.js');

let SETTINGS_FILENAME = "settings.cfg";
let settings = {};

function load_from_file() {
  let file_str = File.read(SETTINGS_FILENAME);
  if (file_str) {
    // Load file into settings structure
    settings = JSON.parse(file_str);
  } else {
    // Failed to read file, create defaults
    settings = {
      a: "String Value",
      b: 1234,
      c: [1, 2, 3, 4]
    };
  }
}

// Load from file the first time after booting
load_from_file();

SQ.set_data_handler(function(data) {
  SQ.dispatch(1, settings.a);
  SQ.dispatch(2, settings.b);
  SQ.dispatch(3, settings.c[3]);
}, null);

SQ.set_trigger_handler(function(tp) {
  if (tp === 1) {
    // Change a settings value
    settings.b++;

    // Write updated settings to file
    let bytes_written = File.write(JSON.stringify(settings), SETTINGS_FILENAME);

    // Check for error
    if (bytes_written === 0) {
      // Failed to write file
    }
  }
  if (tp === 2) {
    // Manually load settings from file
    load_from_file();
  }
}, null);

Time of Day

Example - Change script behaviour based on the time of day. In this example the output is turned on every cycle but only if the time is between 7am to 7pm.

load('senquip.js');
load('api_timer.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  // Create a current time object
  let utc_str = Timer.fmt("{hour:%H,minute:%M,second:%S}", Timer.now());
  let utc = JSON.parse(utc_str);

  // Apply a local timezone offset (this could also be a custom setting)
  let utc_offset = 10; // AEST
  let hour = utc.hour + utc_offset;
  if (hour >= 24) hour = hour - 24;

  // Turn output on for 120 seconds between certain hours of the day
  if ((hour >= 7) && (hour <= 19)) {
    SQ.set_output(1, SQ.ON, 120);
  }
}, null);

Modbus

Example - Send a custom Modbus packet via Serial.

load('senquip.js');
load('api_serial.js');

SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  // Create a Modbus command
  // \x02 = Slave address 2
  // \x06 = Modbus Function 6 (Write Holding Register)
  // \x00\x01 = Register Address 1
  // \x00\x08 = Value 8
  let cmd_str = "\x02\x06\x00\x01\x00\x08";

  // Calculate and encode CRC
  let crc = SQ.crc(cmd_str);
  let crc_str = SQ.encode(crc, -SQ.U16);

  let modbus_str = cmd_str + crc_str;
  SERIAL.write(1, modbus_str, 8);

}, null);

Example - Read a dynamic set of Modbus registers from a slave every 5 seconds, and dispatch the results. Note: Serial 1 must be set to ‘Modbus’ mode.

load('senquip.js');
load('api_timer.js');

let SLAVE_ADDR = 2;

// Add more entries to this array to read more registers
let registers = [
  { 'cp': 1, 'reg_addr': 4, 'value': NaN },
  { 'cp': 2, 'reg_addr': 5, 'value': NaN },
  { 'cp': 3, 'reg_addr': 6, 'value': NaN }
];

function modbus_read_cb(value, userdata) {
  let index = userdata;
  if (isNaN(value)) {
    // Modbus read failed, the value is NaN
  } else {
    // Save value to our register array:
    registers[index].value = value;
  }
  // Increment the index and loop back to 0 if we have reached the end of the array:
  index++;
  if (index >= registers.length) index = 0;
  // Start the next timed read:
  read_modbus_timed(index);
}

function read_register(index) {
  let modbus_settings = JSON.stringify(SLAVE_ADDR) + ",4," + JSON.stringify(registers[index].reg_addr) + ",1,1.5";
  // Modbus settings: slave_addr = 2, function = 4, reg_addr = 4/5/6, num_reg = 1, timeout_s = 1.5 s
  let success = SQ.modbus_read(1, modbus_settings, modbus_read_cb, index);
  if (success === 1) {
    // modbus_settings ok
  }
  else {
    // Error in modbus_settings
  }
}

function read_modbus_timed(index) {
  // Pass index to the timer function as the userdata parameter
  Timer.set(5000, 0, function(userdata) {
      read_register(userdata);
  }, index);
}

// Start chain of registers reads. The next register will be read every 5 seconds
read_modbus_timed(0);

SQ.set_data_handler(function(data) {
  for(let i = 0; i < registers.length; i++)
  {
    SQ.dispatch(registers[i].cp, registers[i].value);
  }
}, null);

MQTT Subscribe

Example - Subscribe to a topic on the MQTT Endpoint connection, and set device settings published to this topic.

load('senquip.js');
load('api_endpoint.js');
load('api_config.js');

// Subscribe to the topic: 'DEVICE_ID/cfg'
MQTT.sub(Cfg.get('device.id') + '/cfg', function(conn, topic, msg)
{
  // Assume the msg data will be a JSON string. Parse string into an JSON object:
  let cfg = JSON.parse(msg);

  // Change device settings using the JSON object:
  Cfg.set(cfg);

  // A reboot is required for settings to take effect,
  // and a short delay (3 seconds) is required for proper de-initialisation.
  Sys.reboot(3000);
}, null);

Here is a sample message which could be published to the cfg topic in the example above:

{accel:{interval:1,name:"MQTT Enabled Accelerometer"}}

Device to Device API

Example - This example shows how two Senquip devices could be used to monitor a reduction of flow down a pipe. A remote unit is positioned at the entrance to a pipe, monitoring the total volume entering using a flow meter connected to pulse1. The remote unit requires no special script, it only needs to report the pulse1 value to the Senquip Portal. Another device (the local unit) monitors the total volume exiting the pipe using pulse1. The following script runs on the local unit to compare the difference in the volume entering and exiting the pipe, and triggering a warning if the difference is too great.

load('senquip.js');
load('api_timer.js');
load('api_config.js');
load('api_math.js');

let remote_data = {};

let delta_warn = Cfg.get('script.num1');

// Request data from remote device every 30 seconds
Timer.set(30000, Timer.REPEAT, function() {
  SQ.req_remote_data("ABCDE1234");
}, null);


// Save data from the remote device to a gloabl variable
SQ.set_remote_handler(function(data) {
  let obj = JSON.parse(data);
  if (obj.deviceid === 'ABCDE1234') {
    remote_data = obj;
  }
}, null);


SQ.set_data_handler(function(data) {
  let obj = JSON.parse(data);

  // Compare remote flow to local flow and look for large differences
  // Set a default status value of 'No Remote Data'
  let status = "No Remote Data";
  if (remote_data.ts !== undefined) {
    let time_diff_seconds = obj.ts - remote_data.ts;

    if (remote_data.pulse1 !== undefined) {
    status = "OK";

      let delta = obj.pulse1 - remote_data.pulse1;

      if (Math.abs(time_diff_seconds) > 120) {
        // If the time between readings is too large, we cannot safely compare
        SQ.dispatch_event(1, SQ.ALERT, "Time Difference Mismatch");
        status = "Time Difference Mismatch";
      }
      else {
        if (delta > delta_warn) {
          // This condition may indicate the pulse counts are out of sync
          status = "Positive Delta Flow";
          SQ.dispatch_event(1, SQ.WARNING, "Check Flow Meters");
        }

        if (delta < -delta_warn) {
          status = "Delta Flow Exceeded";
          SQ.dispatch_event(1, SQ.WARNING, "Reduction in flow detected");
        }
      }

      SQ.dispatch(1, delta);
      SQ.dispatch(2, remote_data.pulse1);
      SQ.dispatch(3, time_diff_seconds);
    }
  }
  SQ.dispatch(4, status);
}, null);