Help needed hacking a BLE TPMS

Posted on
  • Disclaimer: I have played around with Espruino some while ago and my JS and BLE knowledge may have gotten a bit rusty - so sorry for any stupid questions in advance.

    I got a Chinese TPMS (tire pressure monitoring system) consisting of 4 coincell powered BLE enabled oversized valve caps and an iOS / Android App.
    The sensors support pressures over 10 Bar, but the App doesn't allow warning thresholds beyond 6,4 and since I want to monitor my bike tires I am stuck here.
    Espruino to the rescue! (so I thought) - so I started hacking.

    I can see the Sensors appearing in the iOS LightBlue app from time to time. Name "TPMSn-xxxxx" with n being 1..4 and xxxxxx being the hex suffix of the device ID.
    Each device reports 1 service, but any connection attempt times out.

    So I got out my old Nordic development board with the Espruino port loaded and finally used this:

    NRF.setScan(function(d){
      console.log(d);
    }, {
      filters:[{
        services: ['fbb0']
      }]
    });
    

    to log this:

    BluetoothDevice: {
      "id": "82:ea:ca:30:bc:95 public",
      "rssi": -53,
      "data": new Uint8Array([2, 1, 5, 3, 3, 176, 251, 19, 255, 0, 1, 130, 234, 202, 48, 188, 149, 0, 0, 0, 0, 121, 9, 0, 0, 85, 1]).buffer,
      "manufacturer": 256,
      "manufacturerData": new Uint8Array([130, 234, 202, 48, 188, 149, 0, 0, 0, 0, 121, 9, 0, 0, 85, 1]).buffer,
      "services": [
        "fbb0"
       ]
     }
    BluetoothDevice: {
      "id": "83:ea:ca:40:bc:fb public",
      "rssi": -55,
      "data": new Uint8Array([2, 1, 5, 3, 3, 176, 251, 19, 255, 0, 1, 131, 234, 202, 64, 188, 251, 0, 0, 0, 0, 134, 9, 0, 0, 75, 1]).buffer,
      "manufacturer": 256,
      "manufacturerData": new Uint8Array([131, 234, 202, 64, 188, 251, 0, 0, 0, 0, 134, 9, 0, 0, 75, 1]).buffer,
      "services": [
        "fbb0"
       ]
     }
    ...
    

    From time to time devices appear. But this is it.

    When I actually try to connect a device using this code:

    var gatt;
    NRF.requestDevice({ 
      filters: [{ namePrefix: 'TPMS' }],
      timeout: 60000
    }).then(function(device) {
      console.log(device);
      return device.gatt.connect();
    }).then(function(g) {
      gatt = g;
      return gatt.getPrimaryService("fbb0");
    }).then(function(service) {
      console.log("Service:" + service);
      return service.getCharacteristics();
    }).then(function(characteristics) {
      return console.log(characteristics);
    }).then(function() {
      gatt.disconnect();
      console.log("Done!");
    });
    
    • even with the service id as filter - I always time out.

    I guess the device avoids connections to save power.
    Or at least makes connections as brief as possible.

    So my questions:

    • Is there anything more I can try?
    • Is it possible that the whole sensor data is contained in either "data" or "manufacturerData" already and the advertised service is not needed at all?





    2 Attachments

    • IMG_1382.jpeg
    • IMG_1403.png
  • Is the bind really creating a connection or just take note of the device id in a registration and then taking into account only advertisements of the registered device id? That is the way to get away from having to connect and still get data, assuming the data is in the advertisement.

    TPMS has version bluetooth 4 and 5. Your's look like they are 4. But that's just from the outside look.

    Interesting pub at https://www.amarinfotech.com/differences­-comparisons-bluetooth-5-vs-4-2.html (print attached as .pdf)


    2 Attachments

  • Hey, @allObjects thanks for your response.

    Since the devices were quite cheap and on the market for a while I guess it's all old-fashioned BLE 4.

    My guess was also that a connection actually might have been avoided by the creators since it means complexity and drawing more battery power.

    But my question is this:
    Does the quoted advertisement data contain everything the device has to tell us?
    Where is the device name that my LightBlue BLE sniffer app shows?
    How can I obtain any data from the advertised service and its characteristics when I cannot connect to the device?

    PS: I just found the same item on aliexpress and they explicitly say it's BLE 4 - look: https://www.aliexpress.com/item/33041131­063.html


    1 Attachment

    • IMG_1405.PNG
  • Update:
    Doing some more research I found this:
    https://raspberrypi.stackexchange.com/qu­estions/76959/how-to-decode-tpms-sensor-­data-through-rpi3-bluetooth

    Maybe it is the same protocol, maybe similar.
    At least the temperature reading would make sense in my case - not sure with the pressure yet.

    So it most probably is about the manufacturerData and no connection is required.
    That would be quite simple to do, I'll just get my Pixl.js ready to take over from there... ;-)

  • OK. Another update.
    Some more investigation and some coding later I have a working solution now:

    const ttl = 30;
    var idlog = {};
    
    function i2h(i) {
      return ('0'+i.toString(16)).slice(-2);
    }
    
    function a2h(a, i, l) {
      var res = '';
      for(var j=i; j<i+l; j++) {
        res += i2h(a[j]);
      }
      return res;
    }
    
    function dec32(a, i) {
      var res = Uint32Array(1);
      res[0] = (a[i+3] << 24) | (a[i+2] << 16) | (a[i+1] << 8) | a[i];
      return res[0];
    }
    
    function decodeData(device) {
      var d = device.manufacturerData;
      var id = a2h(d, 0, 6);
      var data = {
        battery: d[14],
        flat: d[15],
        temp: dec32(d, 10) / 100,
        pressure: dec32(d, 6) / 100000
      };
    
      var t = getTime();
      if( idlog[id] == undefined || idlog[id].last + ttl < t) {
        idlog[id] = {
          last: t,
          data: data
        };
        console.log(id, data);
      }
    }
    
    function onInit() {
      NRF.setScan(function(device){
        decodeData(device);
      }, {
        filters:[{
          services: ['fbb0']
        }]
      });
    }
    

    This is the output from a few sensors (*ba52 sits on a "real" tire, the others are just blown into to wake them up):

    82eaca30bc95 { "battery": 85, "flat": 0, "temp": 23.11, "pressure": 0.07926 }
    82eaca30bc95 { "battery": 85, "flat": 1, "temp": 23.55, "pressure": 0 }
    80eaca10ba52 { "battery": 85, "flat": 0, "temp": 18.67, "pressure": 8.36479 }
    80eaca10ba52 { "battery": 85, "flat": 0, "temp": 18.67, "pressure": 8.36479 }
    80eaca10ba52 { "battery": 85, "flat": 0, "temp": 18.67, "pressure": 8.36479 }
    82eaca30bc95 { "battery": 85, "flat": 0, "temp": 23.7, "pressure": 0.07018 }
    81eaca207bdd { "battery": 75, "flat": 0, "temp": 23.73, "pressure": 0.053 }
    80eaca10ba52 { "battery": 85, "flat": 0, "temp": 18.67, "pressure": 8.36479 }
    82eaca30bc95 { "battery": 85, "flat": 1, "temp": 24.85, "pressure": 0 }
    81eaca207bdd { "battery": 75, "flat": 1, "temp": 24.15, "pressure": 0 }
    

    Now everything needs to be wrapped up, put into a working solution including low pressure / battery indication (sound, LED), missing signal detection, pairing etc...

  • @ChristianW, thanks, you just made my day! - you really push it to the limit with:

    Now everything needs to be wrapped up, put into a working solution including low pressure / battery indication (sound, LED), missing signal detection, pairing etc...

  • @allObjects what do you mean by push it to the limit?
    It is not ready yet, just the core piece.
    But at least I know it is possible...

  • That's great! Sorry I was a bit late to this, but yes, it makes sense that the relevant data would be in manufacturerData. Those look amazingly neat - I'd love to see something working on a Pixl.js or Bangle.js.

    The code you have there looks spot on - all I'd say is you can always use DataView to decode the data without needing dec32...

    d = new DataView(device.manufacturerData);
    d.getUint8(14) 
    //d.getUint32(10) or d.getUint32(10,1)  if byte order is different
    // you can also use getInt32 for signed data (eg temperature?)
    

    For anyone else interested in this you seem to be able to just search for "v11 tpms" and find them on eBay around £30.

    If you're doing this for a bike you could maybe even detect which sensor is which using signal strength (I guess the rear wheel would be lower strength than the front :).

  • Thanks @Gordon for the DataView hint. I'll try that soon.

    Actually I'd be planning it for the Pixl.js, but Bangle.js would also be great - maybe both.
    And actually it's not a bicycle, but a Velomobile (so a tadpole trike) meaning two tires in the front and one in the rear... ;-)

  • FYI @Gordon - I just posted a pimped up version on Pixl.js here

    DataView works perfectly by the way.

  • Nice - thanks!

  • @ChristianW, thanks a LOT. I have the exact same device and have been struggling with which bytes to decode.
    I already did most of the "wrapping up" in an android app i made for a home made tyre temperature monitor for trackdays. So just needed to convert to a bluetooth device and parse the data.

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

Help needed hacking a BLE TPMS

Posted by Avatar for ChristianW @ChristianW

Actions