Advanced Examples

Latching Alarms

Example - This example shows how to create latching alarms that remain active until manually cleared. The alarms are stored in non-volatile storage (NVS) and will persist across device reboots. However the persistent alarms must be manually created from the script. Normally alarms created by the device’s settings are not persistent unless the conditions remain active.

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

// Global flag to disable creation of alarms
// Load the previous value (using NVS channel 50)
let disable_alarms = SQ.nvs_load(50) ? true : false;

// The alarm level could also be loaded from a custom number setting
let ALARM_LEVEL = SQ.ALARM;

// List of active alarms. Each entry is an index that corresponds to ALARM_TYPES
// For example: [ 2, 4 ] means 'Low Pressure' and 'Low Voltage' alarms are active
let active_alarms = [];

let ALARM_TYPES = {
  '1': { msg: 'High Pressure', cp: 3, level: ALARM_LEVEL },
  '2': { msg: 'Low Pressure', cp: 3, level: ALARM_LEVEL },
  '3': { msg: 'Sensor Failure', cp: 3, level: ALARM_LEVEL },
  '4': { msg: 'Low Voltage', cp: 4, level: ALARM_LEVEL },
};
let ALARM_TYPES_LEN = 4; // Update this to match ALARM_TYPES

function is_alarm_active(index) {
  for (let i = 0; i < active_alarms.length; i++) {
    if (active_alarms[i] === index) { return true; }
  }
  return false;
}

function create_latching_alarm(index) {
  // Check the global disable_alarms flag
  if (!disable_alarms) {
    // Do not create a duplicate alarm if already active
    if (!is_alarm_active(index)) {
      SQ.nvs_save(index, 1);
      active_alarms.push(index);
    }
  }
}

function clear_all_alarms() {
  active_alarms = [];
  for (let i = 1; i <= ALARM_TYPES_LEN; i++) {
    SQ.nvs_save(i, 0);
  }
}

function set_disable_alarms(disable) {
  SQ.nvs_save(50, disable ? 1 : 0);
  disable_alarms = disable;
}

SQ.set_data_handler(function() {
  SQ.dispatch(1, disable_alarms ? 'DISABLED' : 'ENABLED');

  // Iterate through all active alarms and generate Portal alarms
  for (let i = 0; i < active_alarms.length; i++) {
    let alarm_type = ALARM_TYPES[active_alarms[i]];
    SQ.dispatch_event(alarm_type.cp, alarm_type.level, alarm_type.msg);
  }

}, null);

SQ.set_trigger_handler(function(tp) {
  if (tp === 1) { // Enable alarms
    set_disable_alarms(false);
  }
  if (tp === 2) { // Disable alarms
    set_disable_alarms(true);
  }
  if (tp === 3) { // Clear alarms
    clear_all_alarms();
  }

  if (tp === 4) { // Create alarm
    create_latching_alarm(1);
  }
  if (tp === 5) { // Create alarm
    create_latching_alarm(2);
  }
  if (tp === 6) { // Create alarm
    create_latching_alarm(3);
  }
}, null);

// Load previously active alarms from NVS on first boot
for (let i = 1; i <= ALARM_TYPES_LEN; i++) {
  let active = SQ.nvs_load(i);
  if (active) { active_alarms.push(i); }
}

Connecting to Bluetooth Sensors

Example - The following example demonstrates how to connect to a Calypso Ultrasonic Wind instrument via Bluetooth. The process includes scanning for nearby sensors, discovering characteristics and reading data.

load("senquip.js");
load("api_events.js");
load("api_bt_gap.js");
load("api_bt_gattc.js");
load("api_timer.js");
load("api_math.js");
load("api_serial.js");
load("api_endpoint.js");

let current_state = "RESET";
let state_entry_time = 0;

let SCAN_TIME_MS = 4000;
let conn = null;
let sensors = [];
let sensor_index = 0;

function log(s) {
  UDP.send(s);
}

function change_state(new_state) {
  log("State change: " + current_state + " --> " + new_state);
  current_state = new_state;
  state_entry_time = Sys.uptime();
}

function time_in_state_s() {
  return Sys.uptime() - state_entry_time;
}

function process_state() {
  log("State: " + current_state + " Time: " + JSON.stringify(time_in_state_s()));
  let state_functions = {
    RESET: function () {
      if (time_in_state_s() > 4) {
        change_state("SCAN");
      }
    },
    SCAN: function () {
      sensor_index = 0;
      // We now scan for the bluetooth advertising data of nearby sensors
      // This will trigger the GAP.EV_SCAN_RESULT event for each advertisement found.
      // To limit the scan to only the sensors we are interested in, we MUST filter using
      // the name of the sensor (in this case, it starts with "ULTRA")
      // Without this filter, there can be many advertisements, and the device will not be able to handle them all.
      GAP.scan(SCAN_TIME_MS, false, "ULTRA");
      change_state("WAIT");
    },
    WAIT: function () {
      // Wait for an async event, and eventually timeout
      if (time_in_state_s() > 60) {
        change_state("RESET");
      }
    },
    CONNECT: function () {
      // Connect to the next sensor in the list
      if (sensors.length === 0) {
        change_state("SCAN");
        return;
      }
      if (sensor_index >= sensors.length) {
        sensor_index = 0; // Reset index for next cycle
      }
      log("CONNECT");
      // Connect to the sensor at the current index
      // We then wait for the GATTC.EV_CONNECT event to be triggered
      GATTC.connect(sensors[sensor_index].addr);
      // Wait for connection and discovery events
      change_state("WAIT");
    },
    CONNECTED: function () {
      // Connected to the sensor
    },
    DISCONNECT: function () {
      // Disconnect from the current sensor
      // This state is not used in this example, but is shown for completeness
      if (!conn) {
        return;
      }
      log("Disconnecting from", JSON.stringify(conn));
      GATTC.disconnect(conn.connId);
      change_state("WAIT");
    },
  };
  let state_function = state_functions[current_state];
  if (typeof state_function === "function") {
    state_function();
  }
}

Timer.set(1000, Timer.REPEAT, process_state, null);

function sensor_in_list(sensor_addr) {
  for (let i = 0; i < sensors.length; i++) {
    if (sensors[i].addr === sensor_addr) {
      return true;
    }
  }
  return false;
}

Event.on(
  GAP.EV_SCAN_RESULT,
  function (ev, evdata) {
    let sr = GAP.getScanResultArg(evdata);
    let name = GAP.parseName(sr.advData);
    log("EV_SCAN_RESULT" + JSON.stringify(sr));

    if (sensor_in_list(sr.addr) === false) {
      // Found a new sensor, add to our sensor list
      sensors.push({ name: name, addr: sr.addr });
      log("NEW SENSOR:" + JSON.stringify(name) + "addr " + JSON.stringify(sr.addr));
    }
  },
  null
);

Event.on(
  GAP.EV_SCAN_STOP,
  function () {
    log("SCAN_STOP");
    // If we found no sensors, keep scanning
    if (sensors.length === 0) {
      change_state("SCAN");
    } else {
      change_state("CONNECT");
    }
  },
  null
);

function discover() {
  if (!conn) {
    return;
  }
  // Discover characteristics on connected device
  // This will trigger the GATTC.EV_DISCOVERY_RESULT event for each characteristic found.
  if (!GATTC.discover(conn.connId)) {
    change_state("RESET");
  }
}

Event.on(
  GATTC.EV_CONNECT,
  function (ev, evdata) {
    log("EV_CONNECT " + sensors[sensor_index].name);
    change_state("CONNECTED");
    conn = GATTC.getConnectArg(evdata);
    discover();
  },
  null
);

Event.on(
  GATTC.EV_DISCONNECT,
  function () {
    log("EV_DISCONNECT");
    conn = null;

    // NOTE: EV_DISCONNECT may occur twice for each disconnect
    // please handle accordingly
    change_state("RESET");
  },
  null
);

Event.on(
  GATTC.EV_DISCOVERY_RESULT,
  function (ev, evdata) {
    let dr = GATTC.getDiscoveryResultArg(evdata);
    // log(JSON.stringify(dr));
    /* You will see something like this:
        {"conn":{"addr":"E46303091846","connId":0,"mtu":247},"svc":"1800","chr":"2a00","handle":3,"prop":10}
        {"conn":{"addr":"E46303091846","connId":0,"mtu":247},"svc":"fe59","chr":"8ec90003-f315-4f60-9fb8-838830daea50","handle":13,"prop":40}
        {"conn":{"addr":"E46303091846","connId":0,"mtu":247},"svc":"180d","chr":"2a39","handle":49,"prop":18}
    */
    if (dr.chr === "a001") {
      // Example of how to read a characteristic
      if (!GATTC.read(dr.conn.connId, dr.handle)) {
        log("Failed to read characteristic");
      }
    }
    if (dr.chr === "a003") {
      // Turn on the compass by writing to the characteristic
      let newValue = "\x01";
      if (!GATTC.write(dr.conn.connId, dr.handle, newValue)) {
        log("Failed to write characteristic");
      }
    }
    if (dr.chr === "2a39") {
      // Subscribe to the data measurements
      if (!GATTC.subscribe(dr.conn.connId, dr.handle)) {
        log("Failed to subscribe to characteristic");
      }
    }
  },
  null
);

Event.on(
  GATTC.EV_NOTIFY,
  function (ev, evdata) {
    // This event is triggered when a subscribed characteristic sends a notification (new data)
    let rd = GATTC.getNotifyArg(evdata);
    // log("getNotifyArg " + JSON.stringify(rd));
    let data = rd.data;
    // Parse the data and apply scaling or offset
    let result = {
      wind_speed: (data.at(0) + data.at(1) * 256) / 100,
      wind_dir: (data.at(2) + data.at(3) * 256),
      battery: data.at(4) * 10,
      time: Timer.now(),
    };
    sensors[sensor_index].data = result;
    log('[' + JSON.stringify(data) + '] ' + JSON.stringify(data.length));
    log(JSON.stringify(result));
  },
  null
);

Event.on(
  GATTC.EV_READ_RESULT,
  function (ev, evdata) {
    log("EV_READ_RESULT");
    let rd = GATTC.getReadResult(evdata);
    log(JSON.stringify(rd));
  },
  null
);

SQ.set_data_handler(function () {
  if (sensors.length > 0) {
    let i = 0;
    SQ.dispatch(1, JSON.stringify(sensors[i]));
    if (isdef(sensors[i].data) && (sensors[i].data.time + 10) > Timer.now()) {
      SQ.dispatch(2, sensors[i].data.wind_speed);
      SQ.dispatch(3, sensors[i].data.wind_dir);
      SQ.dispatch(4, sensors[i].data.battery);
    }
  }
}, null);

Time of Day Sending

Example - This example shows how to instruct a Senquip device to transmit at a particular time of day. The desired transmit hour and minute are stored in custom parameters and represent the UTC time at which the device should send data. In the script, the device wakes once per hour to re-align its internal clock in case of oscillator drift. If the current hour matches the target transmit hour, the device allows transmission; otherwise, it suppresses it. The script’s data handler returns either SQ.TRANSMIT or SQ.NO_TRANSMIT, which instructs the device to send or suppress the transmission accordingly.

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

// Retrieve user-defined transmit time from custom number variables
let sync_hour = Cfg.get('script.num1');     // Target hour for transmission (UTC)
let sync_minute = Cfg.get('script.num2');   // Target minute for transmission (UTC)

// Function to align the next measurement cycle with the specified minute of the hour
function sync_to_minute(utc) {
  let diff_sec = (sync_minute*60) - (utc.minute*60 + utc.second);  // Time until target minute in seconds
  if (diff_sec < 0) {diff_sec += 3600;}  // If the time has passed, schedule for next hour
  SQ.set_next_cycle(diff_sec);           // Schedule next measurement cycle accordingly
}

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

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

  // Align the next cycle with the target minute, correcting for oscillator drift
  sync_to_minute(utc);

  // Only allow transmission if the current hour matches the configured transmit hour
  if (sync_hour === utc.hour) {
    return SQ.TRANSMIT;       // Allow transmission
  } else {
    return SQ.NO_TRANSMIT;    // Suppress transmission
  }

}, null);