Advanced Examples

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: SQ.parse(data, 0, 2, 0, -SQ.U16) / 100,
      wind_dir: SQ.parse(data, 2, 2, 0, -SQ.U16),
      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);