Dynamic reading of BLE characteristic?

Posted on
  • Hello,

    I've been playing around with BLE with Espruino Puck, and it's been quite pleasant so far. I've got a question and I can't find the answer- which means it's probably staring me in the face.

    I'm experimenting/learning and used NRF.setServices to create a characteristic and I can read the value from it using Nordic nRF connect on my phone. I can also use NRF.updateServices to update the value, and if I use notify the value automatically updates on the phone screen. Yay: Bluetooth success!

    My question: Suppose (hypothetically) I turned the puck into a BLE thermometer. This might be used really infrequently, so I don't want to use setInterval with updateServices as this would keep taking temperature readings which were never used, and use more power than necessary.

    I only want to do the temperature reading (or whatever power hungry thing) when the data is requested by the phone.

    I want something like an 'onRead' function for the characteristic, much as there is onWrite. So my code would be something like

    onRead : function() { return doReallyPowerHungryThingThatReturnsAValu­e() }

    I can't find out how to do such a thing. Is this possible or is it a limitation of BLE/The Nordic SDK? (I can imagine some sort of timing restriction in BLE or something - what if my function took a long time to execute.).

    I'm grateful for any teachings!

    Thanks,

    Colin

    Example code:
    
    NRF.setServices({
      "047b0001-37e3-457d-3763-2a9b2d24cbb2" : {
        "047b0002-37e3-457d-3763-2a9b2d24cbb2": {
          maxLen : 10,
          value : "Unknown",
          readable : true,
          writable : true,
          notify : true,
          onWrite : function(evt) {
            var text = String.fromCharCode.apply(String, evt.data);
            console.log("Message received: ",text);
          },
        }
      }
    }, { uart : false });
    
    function updateTemperature() {
      NRF.updateServices({
        "047b0001-37e3-457d-3763-2a9b2d24cbb2" : {
          "047b0002-37e3-457d-3763-2a9b2d24cbb2" : {
            value : E.getTemperature().toString(),
            notify : true,
          }
        }
      });
    }
    
  • Fri 2020.04.17

    Hi @ColinP Collin, I don't have a good grasp on this, (yet, next month's proj) but do any of these provide some insight?

    Puck.js Control from Android using DroidScript

    Pixl.js Wireless Temperature Display

    or, is this more of a how do I wake from sleep mode to detect if the phone is trying to connect?

    http://www.espruino.com/Reference#l_NRF_­setScan


    'what if my function took a long time to execute'

    Create a WatchDog timer using setInterval() / setTimeout() to prevent that event.

  • Robin - thanks for your reply.

    I don't think those links covered it, though thanks for pointing them out.

    To try to explain a bit better what I was meaning:

    The pixl.js wireless temperature example above has a line 'setInterval(updateBT, 6000);' so updateBT() reads the temperature every 6 seconds, and updates the advertising packet. This is all you can do with advertising, as advertising doesn't know if anything is actually listening - it just broadcasts - like a TV station that keeps broadcasting even if nobody is watching the channel - it doesn't know. But doing this uses battery - and we're wasting battery taking temperature readings that nobody might ever want.

    If my understanding is correct when using GATT services and attributes the model is different - the phone must actively send a packet to the Puck to ask "Please tell me the value of this attribute of the temperature service". This normally returns the value field specified in nrf.setServices via a packet which is sent from the puck to the phone.

    Say (for example) we were developing a thermometer and we wanted the Puck battery to last as long as we could. This thermometer might be in a remote location, and is hardly ever used.

    Ideally, the puck would do nothing but periodically wake the radio to receive and see if there is a phone there talking to it. Normally there isn't, so it just sleeps all the time and uses little battery. It doesn't need read the temperature in all this time so it doesn't, and saves battery.

    Now say the user goes to the remote place to use this rather contrived thermometer. They get out their phone and connect to the Puck. The phone does its Bluetooth thing and sends the "Send me the value for this service and this attribute" packet to the Puck.

    The Puck hears the packet and only then does it actually need to read the temperature. "Finally! At last! Someone is interested in my temperature!" So it calls some sort of 'onRead' callback or something. This callback does the work of reading the temperature, and sends back the value to the phone via another packet.

    The user is done so the Puck has a good long sleep again, not ever needing to read the temperature again until the user comes back, and just sipping power from the battery as a result.

    I hope this slightly contrived example illustrates better the sort of thing I'm getting it. I'm not trying to do anything new against the code I posted - just curious as it seemed like a way which would optimise battery life but couldn't figure it out. I don't actually have a need for this, but always like to learn a better way.

    My point of "what if my function took a long time to execute": I don't think setInterval() would work here. I know BLE has connection intervals and things so that packets must be sent at the correct time. I was thinking that maybe the reason you can't do a read from a user 'onRead' callback (as I'm asking for here) might be that it could mess with the timing - if my callback took a long time it might mess up the timing and cause the Bluetooth stack to crash, or the phone to be upset if it didn't get a quick reply.

    I don't think it's setScan either - the Puck already wakes up to service the request so I don't think I need to mess around with that.

    I feel I've written a bit of a tome here - but hopefully my convoluted story illustrates better what I was trying to get at!

  • Fri 2020.04.17

    Found the link using a setWatch and setInterval to respond and wake the device:

    Power Consumption

    entire thread

    Sleep modes

    Not sure how to get NRF back up and running. There are functions to keep power usage down (broadcast distance), that would assist as the phone would then be near, once discovered.


    Testing for bluetooth connection

    This may be opposite direction, as is 'Pull' needed?

    Write Characteristic to Web Bluetooth

    Technical Article on the behind the covers BLE

    https://www.ncbi.nlm.nih.gov/pmc/article­s/PMC5751532/

  • You can use NRF.on('connect', ... and NRF.on('disconnect', ... events to turn on and off the sensor / sampling / updating.

    For example these two lines, turn on the magnetometer when connected, and turns it off when disconnected. The magnetometer event calls NRF.updateServices. So no magnetometer power consumption and updating of service data if nothing is connected:

      // on connect / disconnect blink the green / red LED turn on / off the magnetometer
      NRF.on('connect', function() {Puck.magOn(magRate); digitalPulse(LED2, 1, 100)})
      NRF.on('disconnect', function() {Puck.magOff(); digitalPulse(LED1, 1, 100)}) 
    

    Full code at https://github.com/AkosLukacs/PuckStream­ing

  • Fri 2020.04.17

    Hey @AkosLukacs, thank you for that working example. That does answer the turning on the high power side of things.

    By chance are you aware of any snippets that might resolve the catch-22 situation? Both @ColinP and I are trying to figure out a way to have the Puck (in your example) lie in wait in sleep mode, but still have the ability to respond when the smart phone is close enough, which somehow signals the sleeping Puck. Presumeably, the Mag ref is only to show how to enable a high power consuption device?

    My only idea was to wake say every minute and broadcast. If no immediate ~5 sec response, go back to sleep, thereby conserving precious battery. When in range, decrease broadcast power and decrease the listening interval to around every five seconds. Interesting dilema, and one I plan on tackling, once my flash device arrives. (presumably you've been following that progress in the other thread, and thanks for that gsmdevice comparablity link there) ;-)

    Beacons seem to do this task, but the tutorial here didn't seem to address that, unless I misunderstood. Tutorial iBeacons seem to be always on. I'll re-read tomorrow.

  • By default (connectable if you want to connect, but not connected to anything) the Puck consumes around 20uA. If this is good enough for you, than you are done. :)

    The Puck handles connection intervals dynamically, so if you are not connected it only sends small advertising packets every 375ms. And if you connect and there is data transfer, switches to lower connection interval.

    Quoting Puck's power consumption and Nordic's "Online Power Profiler":

    • Advertising, 375ms 0dBm (default mode) - 20uA (Nordic: 26uA)
    • Not doing anything - 3uA
    • Not doing anything, watching the button for presses - 12uA
    • Advertising, 1000ms 0dBm - Nordic: 11uA
    • Advertising, 5000ms 0dBm - Nordic: 3.8uA
    • Advertising, 10000ms 0dBm - Nordic: 2.9uA (this is lower than Gordon's Not doing anything might be because of other parts in the Puck)


    I think you can try these things:

    • call NRF.sleep(), and wake periodically with setInterval. I think this was in your last paragraph. Don't know the consumption of this one (the impact of setInterval). But would be kind of annoying if I had to wait & scan...
    • increase the connection interval with NRF.setConnectionInterval() from the default 375ms. Should save some power, and would be still easily connectable
    • turn off the radio, and only turn it back on button press. That would be the 12uA power consumption in idle

    So if believe Nordic's numbers, Espruino's internals don't interfere, and there is nothing else running, 1 second (and of course anything biger) advertising interval would be better than turning off the radio and watching a pin for button press. And would be still easily connectable without excessive waiting...

  • Thanks all for the info. It sounds as if I'm not missing an obvious onRead() at least, which was the thing I was wondering.

    on Connect is useful to know. I figured another way (albeit a bit hacky) could be to write to the characteristic from the phone, which triggers the read operation which in turns updates the value with a notify.

    As I mentioned I don't actually have a use for this right now - but am learning so just wanted to check the best way of doing this. So thanks for the suggestions - most helpful.

  • Sat 2020.04.18

    Thank you @AkosLukacs for the current usage detail.

    3.8 is much better than 20 so that's the one I'm after!!

    Not quite sure how I'm going to measure ~3uA (nothing) but this article provided some insight:

    https://articles.saleae.com/oscilloscope­s/how-to-measure-current-with-an-oscillo­scope

    Maybe usage over time might be a better way to analyze.

  • Just to add - there's no onRead at the moment, and while it may be possible to get notified after a characteristic is read, I don't think you can easily do something that returns the value you want to send as a response (and if you did, you'd probably be very time-limited).

    What I normally see devices do is:

    • host device subscribes to notifications on a characteristic
    • host device writes anything to another characteristic
    • when the reading/calculation is finished, the first characteristic is used to return the data
  • Gordon - thanks. Glad to confirm I wasn't missing the obvious. It's good to learn how to do these things.

    This relates to the thread above, so I hope I can continue it here rather than start a new thread. I can't find the answer in the forum.

    Getting an experiment using GATT between Puck and Pixl has been quite nice and easy - Got a Puck sending notifications to the Pixl every few minutes - managed to graph the temperature last night - until it stopped working. It looks as though the devices were a little bit too far apart so interference or whatever caused them to disconnect, as it's OK when they are closer. When too far apart the Pixl stops working with an error:

    Uncaught InternalError: BLE task completed that wasn't scheduled (3/0)

    I figured that I wasn't handling the disconnection that occurred, so I handled the 'gattserverdisconnected' event. This handler is doing something: my log function is called and prints reason '8', so is doing something.

    However I still get the error "Uncaught InternalError: BLE task completed that wasn't scheduled (3/0)"

    Is there something I should do to handle this and stop the Uncaught error? I tried adding a handler in NRF.on('disconnect')... but that doesn't seem to be called - I'm not sure why.

    I'm also unsure of how best to handle this case. I basically always want the two devices to connect when they can, so am making the Pixl periodically try to reconnect.

    For now, I've done this as follows:

    • Added a 'connected' flag which is set false initially.
    • Added a setInterval call to call the function that does the connection - to keep trying to connect until a successful connection is made.
    • When a device is connected the flag is set to True.
    • The gattserverdisconnected event sets 'connected' to false, so the timer function tries to periodically reconnect again.
    • I guess I should set and clear the interval rather than using a flag, but - ease of experiment.

    Is this the neatest way of handling this scenario? It looks as though the Uncaught Internal error is harmless, but it would be cleaner for me to do is correctly and remove the error.

    Code below shown - this connects to the puck running the service above.

    Thanks!

      NRF.on('disconnect', function () {
        console.log("NRF disconnected");
      });
    
      setInterval(function() {
        if (!connected) {
          console.log("Trying to connect...");
          connectRemoteTemperature(onTemperatureRe­ceived);
        }
      }, 3000);
      
      // Do Bluetooth stuff to connect to the first device found with name beginning
      // 'Puck' and with our service characteristics
      function connectRemoteTemperature(newTextRxedFunc­tion) {
        NRF.requestDevice({ filters: [{ namePrefix: 'Puck' }]
                      }).then(function(device) {
          console.log("Found Puck",device);
    
          return device.gatt.connect();
        }).then(function(g) {
          console.log("Connected");
          connected = true;
          g.device.on('gattserverdisconnected', function(reason) {
             console.log("GATT server disconnected ",reason);
             connected = false;
          });
    
          return g.getPrimaryService("047b0001-37e3-457d-­3763-2a9b2d24cbb2");
        }).then(function(service) {
          console.log("Got service");
          return service.getCharacteristic("047b0002-37e3­-457d-3763-2a9b2d24cbb2");
        }).then(function(characteristic) {
          console.log("Got characteristic");
          // Pulse the light to show we're connected
          digitalPulse(LED,1,[100,0,100,0,100]);
          
          // Called when the value updates. Decode the data from the event
          characteristic.on('characteristicvaluech­anged', function(event) {
              var dataview = event.target.value; // Gives us a JS dataview
              var resultArray = dataview.buffer; // Gives us an array of chars
              var text = String.fromCharCode.apply(String, resultArray); // Gives us a string
              newTextRxedFunction(text); // Call the handler function to update the graph/whatever
        });
    
        return characteristic.startNotifications();
        }).catch(function(e) {
          console.log("ERROR",e);
        });
      }
    
    
  • Uncaught InternalError: BLE task completed that wasn't scheduled (3/0)

    Yes, it 's a bug in the 2v04 firmware. If you install 2v05 it should work fine

    But it should be harmless as you say -with the gattserverdisconnected handler you should be fine. Does it not work?

    I tried adding a handler in NRF.on('disconnect')... but that doesn't seem to be called

    No, that one is called when a device disconnects from Espruino, rather than the other way around :)

    Is this the neatest way of handling this scenario?

    I think that's a nice easy solution. I think rather than a connected flag you could even use devcie.connected : http://www.espruino.com/Reference#l_Blue­toothDevice_connected

    You could respond to gattserverdisconnected and setTimeout a reconnect, but then you'll have to cope with rescheduling that if the reconnect fails. The setInterval is easy

    Only thing I'd say is 3 secs might be a bit tight - I think there's a possibility that some devices could almost take 3 seconds to connect, and then you might get an error when you tried to connect a second time while in the process of connecting.

  • ...and if I install 2v05 it is, indeed fine :-)

    The error message was harmless - it did still work before - but now it's gone which feels a bit cleaner.

    Thanks for your help..

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Dynamic reading of BLE characteristic?

Posted by Avatar for ColinP @ColinP

Actions