Boron LTE + Bluetooth LE?

image

I’m trying to figure out the best way to accomplish a goal and wanted to tap into the brains here to see what others think.

So here is the setup.

10 x (Borons or Argons) + Light & Distance Sensor to detect when a secure container is opened via the light change or the distance on the locked door changes when it’s opened vs closed.

The Boron’s or Argons will spend most of their time sleeping on battery power unless they are woken up by an interrupt from the Light / Distance sensor which only consumes 0.2 mA and will run 24/7/365.

I need a way to disable the Alarm function when the door is opened when an employee goes to do general maintenance on this secured enclosure.

I’m thinking maybe I could create a keyfob controller that would broadcast a string of data that the 10 Gen3 devices could wake up and listen for every 30-60 seconds to figure out if they should pause the Alarm mode for 15 mins before reactivating the Alarm again.

I need to do some testing to figure out how quickly the Gen 3 devices can wake up, listen for a BLE broadcast for 1-2 seconds to figure out if it should remain armed or disabled for 15 mins.

Anybody tried anything like this before?

Alternate:

Use a 1-Wire to I2C bridge device (https://datasheets.maximintegrated.com/en/ds/DS2484.pdf) to read a 1-Wire key-fob button via an external reader on the enclosure (the bridge consumes 3uA in sleep and it could be woken up frequently to check if a key has been presented). The key id can be updated remotely for easy key management and that way you have a physical security layer instead of the radio fob that could be compromised ?

1 Like

Would it make sense to have some type of secure element inside the box? Whereas a fob, etc can be plugged in once the box is open? The alarm could sound immediately but it wouldn’t be sent until 10 seconds later. (That’s if the “maintenance key” wasn’t inserted.) If the key is inserted, then the alarm shuts off.

You can take a look at how GameWell designed their fire call boxes. I happened to check out the inside of one and was thoroughly impressed by the well thought out design. You opened it up and there was a “key” that you could plug in to bypass or disable the alarm during testing.

Hopefully that makes sense.

Btw, 200µA is a ton of current to be running 24/7/365. I hope you have a very large battery. :slight_smile:

1 Like

I need a way that's not easily bypassed by a bad guy if this disable technique is learned about by the public.

I don't think your recommendation is a bad one but it increases the cost by needing an extra key and key mechanism for each enclosure.

I'm thinking that having the Gen3 Device wake up every 60 seconds to listen to a BLE broadcast that can tell all devices if they should disable for a short period or not.

The Boron only consumed 2.5mA while constantly broadcasting over BLE while the main status LED was turned down to zero brightness. If I only have the Boron listen to a BLE broadcast every 60 seconds for a couple of seconds then the power consumption should be manageable.

This will run off 3 AA Energizer or Energizer Lithium non-rechargeable batteries which provide about 3300 mAh of battery capacity.

The Light & Distance sensors only consume 0.02mA continuous, which is awesome compared to some other Time of Flight distance sensors that consume 20-50mA continuous.

0.02mA x 24 Hours x 30 days = 14.5 mAh / 1 Month of run time
0.02mA x 24 Hours x 180 days = 87 mAh / 6 Months of run time
0.02mA x 24 Hours x 360 days = 174 mAh / 1 Year of run time

The other BLE example that @rickkas7 has written was where the Gen3 devices could tell which BLE device was closest to it based on the signal strength that it was reading from the remote device it was seeing advertise.

This may be a way to just have a Maintnece fob that is worn by the maintenance crew when opening the box so the Gen3 device will disable if it picks up on the BLE broadcast with the right secret key + the signal strength is like 100% due to the maintenance may wearing it while right at the secured container.

I really like this approach because then when the device is opened and the Alarm is activated the Gen3 device would not need to wake up every 60 seconds but instead just check for a BLE broadcast from the maintenance FOB that would be broadcasting a secret key + have 100% signal strength since it would be right up on the enclosure and that would be the combination that would disable the alarm for 5-10-15 mins before auto rearming again.

Nice, I wasn't sure what type of batteries you were using. The lithium is a good choice especially for a long term deployment. Is the Boron plugged in or on batteries too?

It's not a bad idea. Scanning though is costly. On a sister chip (the nRF52810) i'm seeing at least 9mA while the receiver is active. If your maintenance fob is something like a Tile then it's advertising every 2 seconds. Obviously you can make it bigger, have it transmit more often, etc. And yea the RSSI idea is definitely a good idea. Depending on antenna placement and orientation you may get different readings.

A cheap and easy way to play with the idea is to get a Tile Mate from Amazon or similar. The hardware is crazy hackable. The hardware engineers added a 10 pin Tag Connect (among other niceties) which makes it easy to work with. You'd could use one of the beacon examples from Nordic's SDK quickly if you wanted to prove it out.

Alternatively, you can advertise with reduced transmit power. Then simply have a phone app that's scanning for it, authenticates and deactivates the alarm. You could do the same thing with the Tile Mate in this case as well. (have it scan instead of transmit) You'd have to have a hefty battery for it though. :dizzy_face:

I know broadcasting via BLE on a Boron with the status LED brightness on zero the power consumption is only 2.5mA which is really good.

I would only need to scan for the Disarm broadcast quickly once the device is woken up by the enclosure being opened. Once the Disarm broadcast picked up and the RSSI is high then the device would automatically disarm. So no need to wake up every min to scan for a disarm broadcast.

I figure I will just stick with a Gen3 device to make the Maintenance FOB with an ON/OFF switch so it only needs to be on when they are actually doing maintenance.

The enclosures will be at least 20 feet apart from each other so RSSI will not need to be a perfect 100%, but 90-100% would probably work, just need to do some testing.

Nice, I've never heard about these, but they sound nice.

Looks like I have some testing to prove out how this works in the real world.

@ScruffR
Here is what I’m trying to accomplish.

If a locked box is detected as open then the Boron wakes up, looks for a predefined Argon that is sending a broadcast 5-10 times a second over BLE and if it detects that predefined BLE Argon address + the signal stregnth is almost 100% because the employee is standing right next to the Boron with the Argon key fob that is constantly broadcasting then it will simply not sound the Alarm siren but will still log the event.

Kinda like the crude drawling I did on my phone that’s attached.

Does each Argon have a unique ID that can’t be changed that would be OK to be used as a security key?

What are your thoughts on this idea of using BLE as a wireless key?

I think what you are planning there is doable but, while each BLE device normally has a fixed address, there - of course - are also devices that can spoof addresses.
So I wouldn’t just rely on the address of the nearby key fob but would perform some communication between the two.

IIRC the Particle devices would allow for “secure” BLE communication but it’s not yet implemented.

We'll if they can be spoofed then we need to go for a dual Key approach.

So it looks like I need to use & modify your code you helped @neal_tommy with earlier where he was looking for help connecting to another known BLE device that was broadcasting temp data.

I'm thinking all I need to do is modify your code is to provide the BLE address of the Argon that will be inside the employee Key FOB and then have it transmit a key code the same as the example code is receiving a Temperature value.

I think I can figure the above out with some trial and error.

I tried to reduced your code to what’s really needed.
Since you already know the address of your desired peripheral the entire scanning block can be omitted. I also kicked out the dummy UUIDs and so I’m left with this.
Give this a try.

#include "Particle.h"
#include <math.h>

SYSTEM_MODE(MANUAL)
SYSTEM_THREAD(ENABLED)

SerialLogHandler  logger(LOG_LEVEL_INFO);

const uint16_t    vendor = 0x2A23;
const uint8_t     BLE_PEER_ADDRESS[] = {0xDF, 0x11, 0xFE, 0xD7, 0x6B, 0x08};    // MAC address of the peripheral device
const BleAddress  bleAddress(BLE_PEER_ADDRESS);
const BleUuid     serviceUuidRoot("7881CD3D-2F35-6AFA-7C3E-963A648DF526"); 
const BleUuid     charTempUuidLong("00002A6E-0000-1000-8000-00805F9B34FB");     // probably the long  UUID reported by nRF Connect App
const BleUuid     charTempUuidShort(0x2A6E);                                    // probably the short UUID reported by Bluefruit App
const uint16_t    charTempUuidNumeric(0x2A6E);                                  // number literal to work around the current bug

BlePeerDevice     peerDevice;
BleCharacteristic charTemp;

uint32_t loopDelay = 1000;                                                      // cadence for void loop()
uint16_t rawTemp   = 0;
float    temp      = 0;

bool attachPeer(BleAddress addr, BlePeerDevice& peer, int maxCharacteristics = 20);

void setup()
{
  Serial.begin();
}

void loop() {
  static uint32_t ms = 0;
  if (millis() - ms < loopDelay) return;
  ms = millis();
  
  if (!BLE.connected() && !attachPeer(bleAddress, peerDevice)) {                // first check connection, if not already connected, try to
    Log.warn("could not connect to peer device - retry in 5 seconds");          // when both checks fail log a warning
    loopDelay = 5000;
  }
  else if (charTemp.valid()) {
    loopDelay = 1000;                                                           // when connected read every second 
                                                                                //   could be modulated via delta temp 
                                                                                //   more volatile temperature may warrant a lower loop cadance
    charTemp.getValue(&rawTemp);                                                // request temperature from BLE peripheral
    temp = rawTemp / 100.0;                                                     // convert from 100ths, although too simple
    Serial.printlnf("Temp %.1f °C (raw: %d)", temp, rawTemp);
  }
}

bool attachPeer(BleAddress addr, BlePeerDevice& peer, int maxCharacteristics) {
  charTemp = BleCharacteristic();                                               // invalidate characteristic

  if (peer.connected())
    peer.disconnect();

  peer = BLE.connect(addr); 
  if (!peer.connected()) {
    Log.warn("Couldn't connect to device");
    return false;
  }

  Log.info("Found device and connected to it");

  // *** BUG: peer.getCharacteristicByUUID() is currently not guaranteed to actually find the characteristic even when it's there ***
  // *** depending on the original UUID the long the short or neither UUID can be found, in the latter case full scan is required ***. 
  if(!peer.getCharacteristicByUUID(charTemp, charTempUuidLong) && !peer.getCharacteristicByUUID(charTemp, charTempUuidShort)) {
    BleCharacteristic chars[maxCharacteristics];
    int foundChar = peer.discoverAllCharacteristics(chars, maxCharacteristics);   // discover its exposed characteristics
    Log.info("Found %d of %d characteristics", foundChar, maxCharacteristics);
    for (int c = 0; c < foundChar; c++) {
      Serial.printlnf("%d. Characteristic: %s (%04x)", c + 1, (const char *)chars[c].UUID().toString(), chars[c].UUID().shorted());
      for (int i = 0; i < 16; i++)
        Serial.printlnf(" %02x", chars[c].UUID().rawBytes()[i]);
    
      if (chars[c].UUID()           == charTempUuidLong
      ||  chars[c].UUID().shorted() == charTempUuidLong.shorted()
      ||  chars[c].UUID().shorted() == charTempUuidShort.shorted()
      ||  chars[c].UUID().shorted() == charTempUuidNumeric)              
      {                                                                           // check against expected value
        charTemp = chars[c];
        // alternatively we could just hook up a callback handler to deal with new data
        break;                                                                    // already found what we are looking for, so we can stop here
      }
    }
  }
  return charTemp.valid();
}

What I currently have no idea how to do is add the RSSI of the Key Fob into a variable that I can poll to verify if the signal is strong enough to be considered an Approved or Non-Approved event.

@ScruffR I know you said:

The RSSI value is part of the BleScanResult hence you shouldn’t even need to connect to the device.

I'm pretty sure the RSSI that the Boron is seeing from the KeyFob's signal is something I can pull up in code also? I wouldn't want the RSSI the Key Fob is reporting, I want the RSSI reading that the Boron is reporting about it's connection with the Key Fob. Does that make sense?

I'm just trying to make sure I'm digging down the right hole before I start digging into how to accomplish this. It seems pretty simple and should just require some tweaking of the code you have already put together above.

I know @rickkas7 has some BLE examples I have seen before where he was changing the colors of the status LED based on which BLE device was closest in proximity based on RSSI readings. I need to find that and look into it.

Any help or guidance here is appreciated Bigly :slight_smile:

I’ll have to look into this and investigate some ideas :wink:


Hey Ryan (@RWB) I wasn’t exactly diggint into the RSSI question as such but I had an alternative approach in mind where either device can take on the role of the box or the key and hence either one can scan for the other and as such read it’s counterparty’s RSSI (or do other stuff).

(again by hitting the MODE button you can trigger the main action - in this case becoming the BLE central and connecting/disconnecting to/from the peripheral)

#include "Particle.h"

#if (PLATFORM_ID == PLATFORM_XENON)
  SYSTEM_MODE(MANUAL)
#endif
SYSTEM_THREAD(ENABLED)

SerialLogHandler logger(LOG_LEVEL_ALL);

const uint16_t vendorBox = 0xFE01;
const BleUuid  serviceBoxUuid("0000FE01-0000-1000-8000-00805F9B34FB");
const BleUuid  charstxBoxUuid("0000FE02-0000-1000-8000-00805F9B34FB");
const uint16_t charstxBoxNum = 0xFE02;

const uint16_t vendorKey = 0xFE10;
const BleUuid  serviceKeyUuid("0000FE10-0000-1000-8000-00805F9B34FB");

BleAdvertisingData advData;
BleAddress         peerAddress;
bool               peerAddressValid = false;
BlePeerDevice      peerDevice;
BleCharacteristic  charstxBox;
BleCharacteristic  charstxKey;

enum      BLE_ROLE       { BOX, KEY };
enum      BLE_STATE      { IDLE, SCAN, SCAN_WAIT, CONNECT, CONNECT_WAIT, READ_DATA, SEND_DATA, DISCONNECT };
BLE_ROLE  bleRole        = BOX;
BLE_STATE bleState       = IDLE;

uint32_t  msStateTimeout = 10000;

bool attachPeer(BleAddress addr, BlePeerDevice& peer, int maxCharacteristics = 20);

void setup() {
  Serial.begin();
  System.on(button_click, clickHandler);
  pinMode(D7, OUTPUT);
  
  BLE.onConnected(onConnected, NULL);
  BLE.onDisconnected(onDisconnected, NULL);
  
  advData.appendLocalName("BleBox");
  advData.appendServiceUUID(serviceBoxUuid);

  BLE.advertise(&advData);
}

void loop() {
  static uint32_t msStateTime = 0;
  
  switch (bleState) {
    case IDLE: 
      break;

    case SCAN:
      BLE.stopAdvertising();
      peerAddressValid = false;
      BLE.scan(scanResultCallback, NULL);
      bleState = SCAN_WAIT;
      msStateTimeout = 5000;
      msStateTime = millis();
      break;

    case SCAN_WAIT:
      if (peerAddressValid)
        bleState = CONNECT;
      else if (millis() - msStateTime > msStateTimeout) {
        Log.warn("timeout at state %d", bleState);
        bleState = DISCONNECT;
      }
      break;

    case CONNECT:
      if (peerAddressValid) {
        attachPeer(peerAddress, peerDevice);
        bleState = CONNECT_WAIT;
        msStateTimeout = 5000;
        msStateTime = millis();
      }
      else 
        bleState = DISCONNECT;
        
      break;

    case CONNECT_WAIT:
      if (peerDevice.connected()) {
        Log.info("I'm a Key now");
        bleState = READ_DATA;
        msStateTimeout = 1000;
        msStateTime = millis();
      }
      else if (millis() - msStateTime > msStateTimeout) { 
        Log.warn("timeout at state %d", bleState);
        bleState = DISCONNECT;
      }
      break;

    case READ_DATA: 
      if (millis() - msStateTime < msStateTimeout)
        break;
        
      if (charstxBox.valid()) {
        uint32_t data;
        charstxBox.getValue((uint8_t*)&data, sizeof(data));
        Log.info("Box timestamp %012lu", data);
        charstxBox.setValue(data);
        msStateTimeout = 1000;
        msStateTime = millis();
      }
      else {
        Log.warn("invalid characteristic");
        bleState = DISCONNECT;
      }
      break;
      
    case SEND_DATA:
      if (millis() - msStateTime < msStateTimeout) 
        break;

      if (BLE.connected() && charstxBox.valid()) {
        uint32_t data = millis();
        charstxBox.setValue(data);
        Log.info("my timestamp %012lu", data);
        msStateTime = millis();
      }
      else {
        Log.warn("lost connection");
        bleState = DISCONNECT;
      }
      break;
      
    case DISCONNECT:
      BLE.disconnectAll();
      peerAddressValid = false;
      bleRole = BOX;
      bleState = IDLE;
      BLE.advertise(&advData);
      break;

    default:
      bleState = IDLE;
      break;
  }
}

void scanResultCallback(const BleScanResult *scanResult, void *context) {
  if (BLE.connected())
    BLE.disconnectAll();

  int countServ = 5;                                                            // how many do we expect
  BleUuid services[countServ];                                                  // reserve that many service UUIDs
  countServ = scanResult->advertisingData.serviceUUID(services, countServ);     // request up to countUuid services from advertisingData
  Log.info("Found %d UUIDs in advertisingData", countServ);
  for (int s = 0; s < countServ; s++) {                                         // iterate over found services
    Log.info("%d. UUID: %s (%04X)", s + 1, (const char *)services[s].toString(), services[s].shorted());
    if (services[s] == serviceBoxUuid || services[s].shorted() == vendorBox) {  // check against expected values
      Log.trace("%s: %s ?= %s || %04X ?= %04X"
               , (const char*)scanResult->address.toString()
               , (const char*)services[s].toString()
               , (const char*)serviceBoxUuid.toString()
               , services[s].shorted()
               , vendorBox);
      peerAddress = scanResult->address;
      peerAddressValid = true;
      BLE.stopScanning();
      break;
    }
  }
}

bool attachPeer(BleAddress addr, BlePeerDevice& peer, int maxCharacteristics) {
  charstxBox = BleCharacteristic();                                             // invalidate characterisics

  if (peer.connected())
    peer.disconnect();

  peer = BLE.connect(addr);                                                     // connect to the peer device
  if (!peer.connected()) {
    Log.warn("Couldn't connect to device");
    return false;
  }
  Log.info("found device and connected to it");

  if (!peer.getCharacteristicByUUID(charstxBox, charstxBoxUuid)) {  
    BleCharacteristic chars[maxCharacteristics];
    int foundChar = peer.discoverAllCharacteristics(chars, maxCharacteristics);
    Log.info("Found %d of %d characteristics", foundChar, maxCharacteristics);
    for (int c = 0; c < foundChar; c++)
    {
      Log.trace("%s =? %s, %04X ?= %04x", (const char*)chars[c].UUID().toString(), (const char*)charstxBoxUuid.toString(), chars[c].UUID().shorted(), charstxBoxUuid.shorted());
      if (chars[c].UUID()           == charstxBoxUuid
      ||  chars[c].UUID().shorted() == charstxBoxUuid.shorted()
      ||  chars[c].UUID().shorted() == charstxBoxNum 
      ){                                                                           
        charstxBox = chars[c];
        break;                                                                    
      }
    }
  }
  
  return charstxBox.valid();
}

void onConnected(const BlePeerDevice& peer, void* context) {
  if (bleRole == BOX) {
    charstxBox = BleCharacteristic("box", BleCharacteristicProperty::READ | BleCharacteristicProperty::WRITE_WO_RSP, charstxBoxUuid, serviceBoxUuid, onDataReceived, NULL);
    BLE.addCharacteristic(charstxBox);
    msStateTimeout = 1000;
    bleState = SEND_DATA;
    Log.info("connected to %s", (const char*)peer.address().toString());
  }
}

void onDisconnected(const BlePeerDevice& peer, void* context) {
  Log.info("have been disconnected from %s", (const char*)peer.address().toString());
  bleState = DISCONNECT;
}

void onDataReceived(const uint8_t *data, size_t len, const BlePeerDevice &peer, void *context) {
  Log.info("Received %d bytes from %s (context: %08x) ", len, (const char *)peer.address().toString(), context);
  for (size_t i=0; i < len; i++)
    Log.printf("%02x", data[i]);
  Log.print("\r\n");
  
  digitalWrite(D7, !digitalRead(D7));
}

void clickHandler(system_event_t event, int param)
{
  if (BLE.connected()) {
    bleState = DISCONNECT;
  }
  else {
    bleRole = KEY;
    bleState = SCAN;
  }
}

Alternatively could whichever side had done the scanning inform its counterparty about the found RSSI via charstx.setValue().

While this isn’t the solution to your problem it might provide some food for thought.

1 Like

Nice! I really appreciate your help with this.

I'll load this up on 2 devices and start learning the mechanics of how everything works and see if I can adapt to something that works for this application.

It sounds like your saying the RSSI is only transmitted during the scanning phase.

I guess we could code it so the RSSI has to meet a certain threshold before actually connecting and then sending the Secret Key over a broadcast.

There are a few ways to skin this cat :cat: it looks like.

I'll report back after some testing.

@rickkas7 Chime in here if you know how to consistently poll the RSSI strength on an active BLE connection between a Boron & Argon.

Since I didn't dive into the RSSI acquisition process (yet) I just tried to facilitate what's explicitly exposed as is.
Teasing out what might be hidden somewhere in the HAL would be my next step :wink:


Update:
After digging a bit deeper I got stuck at hal_ble_gap_get_rssi() and have no clue how this would be doing anything useful, so I have to pass this question on to the big guns like @rickkas7:

  • Is there a way to achieve something like BleLocalDevice::getRSSI() or BlePeerDevice::getRSSI() would do if they were implemented?
  • Why are they not?
2 Likes