RadonEye BLE Interface

Seems a little strange. I think 0x002a resets to all 0’s after responding, and the read just gets the current, reset value.

Also:

$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A --mtu=507 --char-write-req --handle=0x002a --value=0x50 --listen
Characteristic value was written successfully
^C
$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A --mtu=507 --char-write-req --handle=0x002a --value=50 --listen
Characteristic value was written successfully
Notification handle = 0x002c value: 50 0a 30 00 3f 00 00 00 04 00 08 00 
^C

Seems like writing 0x50 doesn’t work, but writing 50 does, as long as --listen is used

The other problem is that non-interactive gatttool does not set MTU:

$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A --mtu=507 --char-write-req --handle=0x002a --value=41 --listen
Characteristic value was written successfully
Notification handle = 0x002f value: 41 04 01 fa 19 00 30 00 30 00 26 00 2e 00 2e 00 2d 00 23 00 
Notification handle = 0x002f value: 41 04 02 fa 26 00 26 00 10 00 1a 00 13 00 20 00 2b 00 31 00 
Notification handle = 0x002f value: 41 04 03 fa 31 00 3b 00 38 00 3e 00 30 00 44 00 31 00 2b 00 
Notification handle = 0x002f value: 41 04 04 79 27 00 2b 00 38 00 3b 00 41 00 48 00 44 00 35 00 
^C

The first three responses are each supposed to be 507 bytes total transmission (last one = 249), and this only returns 23 bytes (the default, minimum value).

For example, interactive:

$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A -I
[94:3C:C6:DE:31:7A][LE]> connect
Attempting to connect to 94:3C:C6:DE:31:7A
Connection successful
[94:3C:C6:DE:31:7A][LE]> mtu 507
MTU was exchanged successfully: 507
[94:3C:C6:DE:31:7A][LE]> char-write-req 0x002a 41
Characteristic value was written successfully
Notification handle = 0x002f value: 41 04 01 fa 19 00 30 00 30 00 26 00 2e 00 2e 00 2d 00 23 00 20 00 1d 00 2a 00 17 00 17 00 12 00 1d 00 1d 00 2b 00 34 00 2e 00 37 00 27 00 21 00 30 00 37 00 3a 00 2d 00 2b 00 1a 00 30 00 16 00 10 00 13 00 12 00 0f 00 17 00 16 00 24 00 19 00 16 00 26 00 1c 00 20 00 34 00 3f 00 26 00 35 00 3f 00 30 00 31 00 38 00 3f 00 44 00 38 00 26 00 19 00 24 00 2a 00 2e 00 21 00 20 00 1c 00 24 00 1c 00 26 00 38 00 2e 00 48 00 34 00 35 00 37 00 38 00 44 00 44 00 31 00 41 00 44 00 3b 00 27 00 2b 00 27 00 1d 00 17 00 2d 00 2b 00 27 00 26 00 2e 00 3b 00 44 00 44 00 3e 00 45 00 4e 00 37 00 34 00 44 00 2e 00 24 00 30 00 24 00 2b 00 2d 00 2a 00 30 00 1c 00 30 00 23 00 23 00 1c 00 23 00 31 00 20 00 3a 00 3e 00 3f 00 38 00 45 00 55 00 45 00 3a 00 42 00 2a 00 37 00 42 00 35 00 2e 00 38 00 2a 00 35 00 21 00 21 00 2a 00 26 00 24 00 3b 00 31 00 30 00 3e 00 30 00 3a 00 24 00 37 00 3b 00 37 00 26 00 41 00 30 00 1c 00 34 00 21 00 23 00 34 00 26 00 19 00 23 00 24 00 1d 00 19 00 16 00 2a 00 2e 00 34 00 3e 00 41 00 38 00 30 00 21 00 3a 00 2d 00 2a 00 2b 00 21 00 24 00 24 00 30 00 24 00 19 00 1c 00 16 00 1a 00 1a 00 21 00 27 00 2b 00 23 00 2b 00 27 00 2b 00 2a 00 3b 00 2d 00 2a 00 12 00 17 00 27 00 26 00 24 00 1d 00 27 00 20 00 1a 00 1d 00 20 00 23 00 24 00 31 00 20 00 2a 00 2a 00 31 00 34 00 3a 00 2d 00 42 00 34 00 35 00 34 00 3b 00 30 00 37 00 2e 00 27 00 26 00 17 00 2a 00 2b 00 20 00 19 00 26 00 2d 00 21 00 27 00 2e 00 38 00 37 00 38 00 30 00 27 00 21 00 31 00 24 00 26 00 19 00 2a 00 24 00 1a 00 1c 00 20 00 13 00 2a 00 
Notification handle = 0x002f value: 41 04 02 fa 26 00 26 00 10 00 1a 00 13 00 20 00 2b 00 31 00 3e 00 34 00 19 00 35 00 35 00 2e 00 30 00 37 00 24 00 2a 00 30 00 21 00 26 00 23 00 1c 00 19 00 1d 00 17 00 21 00 20 00 1d 00 2a 00 3b 00 24 00 20 00 26 00 24 00 30 00 2e 00 26 00 38 00 30 00 31 00 2a 00 2d 00 2b 00 2b 00 2b 00 26 00 2a 00 1d 00 24 00 1a 00 23 00 2e 00 30 00 45 00 41 00 2e 00 2b 00 34 00 38 00 3e 00 45 00 2d 00 42 00 35 00 2d 00 2a 00 26 00 1d 00 34 00 2e 00 20 00 26 00 2e 00 21 00 21 00 1c 00 2d 00 4e 00 41 00 45 00 38 00 31 00 3f 00 3a 00 38 00 35 00 23 00 26 00 27 00 24 00 16 00 2d 00 30 00 1c 00 2a 00 10 00 27 00 20 00 20 00 1d 00 26 00 2d 00 31 00 37 00 3e 00 31 00 2e 00 3a 00 2d 00 49 00 4b 00 41 00 37 00 2d 00 3f 00 3b 00 30 00 26 00 31 00 27 00 41 00 30 00 3b 00 41 00 5c 00 5a 00 45 00 44 00 4b 00 49 00 4c 00 45 00 30 00 48 00 45 00 41 00 38 00 2b 00 3a 00 31 00 37 00 24 00 27 00 26 00 17 00 31 00 34 00 23 00 2b 00 45 00 34 00 31 00 35 00 31 00 35 00 30 00 3e 00 42 00 2e 00 3a 00 2e 00 20 00 2a 00 23 00 1d 00 13 00 2e 00 16 00 1d 00 26 00 2a 00 27 00 31 00 37 00 37 00 3f 00 31 00 50 00 37 00 37 00 34 00 26 00 41 00 20 00 2b 00 23 00 2d 00 27 00 2d 00 34 00 2a 00 2b 00 35 00 2a 00 3b 00 42 00 49 00 45 00 50 00 50 00 3e 00 42 00 3a 00 4c 00 52 00 3f 00 41 00 42 00 3a 00 37 00 48 00 31 00 3e 00 30 00 3e 00 3e 00 3f 00 3a 00 38 00 2e 00 45 00 60 00 45 00 63 00 45 00 59 00 4c 00 38 00 52 00 42 00 3b 00 3a 00 35 00 3e 00 2a 00 30 00 26 00 24 00 2e 00 26 00 16 00 20 00 20 00 16 00 24 00 2d 00 35 00 34 00 23 00 
Notification handle = 0x002f value: 41 04 03 fa 31 00 3b 00 38 00 3e 00 30 00 44 00 31 00 2b 00 2a 00 37 00 1c 00 31 00 20 00 1d 00 30 00 2a 00 27 00 3a 00 2a 00 30 00 48 00 48 00 44 00 37 00 2d 00 2e 00 35 00 4b 00 3b 00 31 00 35 00 3e 00 1d 00 27 00 2e 00 2d 00 2d 00 2a 00 30 00 20 00 20 00 37 00 35 00 49 00 56 00 4c 00 3a 00 2b 00 3e 00 3a 00 3e 00 4c 00 37 00 30 00 38 00 2a 00 30 00 2d 00 2e 00 1d 00 2b 00 3a 00 3b 00 27 00 49 00 50 00 3b 00 41 00 42 00 5d 00 4c 00 3b 00 3a 00 23 00 30 00 31 00 3a 00 49 00 26 00 2a 00 26 00 2d 00 20 00 20 00 20 00 26 00 1d 00 24 00 1a 00 0f 00 2d 00 23 00 24 00 23 00 26 00 1d 00 34 00 35 00 31 00 34 00 23 00 2a 00 2a 00 26 00 27 00 1d 00 26 00 35 00 30 00 2e 00 13 00 35 00 34 00 24 00 2a 00 26 00 3a 00 34 00 3e 00 3b 00 2d 00 3e 00 44 00 3a 00 2b 00 3b 00 31 00 23 00 35 00 2e 00 30 00 2d 00 30 00 26 00 45 00 3e 00 3f 00 38 00 38 00 37 00 30 00 24 00 2b 00 42 00 41 00 3f 00 3f 00 3b 00 30 00 30 00 3f 00 30 00 35 00 13 00 1a 00 20 00 27 00 3b 00 2d 00 35 00 38 00 41 00 38 00 42 00 3f 00 31 00 3e 00 2e 00 30 00 4e 00 3b 00 38 00 31 00 3a 00 31 00 38 00 2e 00 2b 00 23 00 37 00 2a 00 19 00 2b 00 31 00 2a 00 31 00 52 00 66 00 52 00 5c 00 49 00 4c 00 42 00 52 00 45 00 37 00 37 00 3f 00 4b 00 23 00 3b 00 30 00 27 00 2a 00 2a 00 2d 00 37 00 2a 00 20 00 2a 00 31 00 3b 00 31 00 30 00 31 00 4c 00 3f 00 5f 00 41 00 3b 00 31 00 42 00 3e 00 3a 00 2d 00 24 00 34 00 2d 00 2e 00 21 00 31 00 2e 00 26 00 2d 00 26 00 56 00 3e 00 41 00 3b 00 4b 00 56 00 45 00 4c 00 3f 00 34 00 45 00 3e 00 42 00 42 00 30 00 
Notification handle = 0x002f value: 41 04 04 79 27 00 2b 00 38 00 3b 00 41 00 48 00 44 00 35 00 38 00 4e 00 3b 00 3e 00 3e 00 4b 00 71 00 42 00 56 00 52 00 4c 00 37 00 31 00 34 00 30 00 20 00 19 00 24 00 1d 00 2a 00 19 00 24 00 2d 00 1a 00 31 00 2b 00 37 00 34 00 3a 00 41 00 4c 00 38 00 3f 00 3b 00 2d 00 37 00 3f 00 35 00 27 00 2a 00 23 00 2a 00 23 00 2a 00 37 00 3f 00 38 00 4e 00 3e 00 44 00 48 00 49 00 38 00 41 00 38 00 45 00 50 00 41 00 38 00 34 00 3a 00 3f 00 30 00 44 00 21 00 37 00 2d 00 21 00 19 00 3b 00 30 00 2d 00 4e 00 4b 00 59 00 55 00 44 00 3a 00 52 00 50 00 66 00 5d 00 31 00 4c 00 42 00 42 00 2e 00 30 00 2d 00 21 00 24 00 31 00 2a 00 24 00 37 00 24 00 2e 00 42 00 49 00 5c 00 64 00 5a 00 50 00 42 00 48 00 48 00 38 00 41 00 41 00 31 00 52 00 2d 00 31 00 
[94:3C:C6:DE:31:7A][LE]> disconnect

(gatttool:2066): GLib-WARNING **: 16:04:05.144: Invalid file descriptor.

[94:3C:C6:DE:31:7A][LE]> exit

Definitely not getting all of the bytes using non-inteactive. Also, trying to read that value (0x002f) instad of --listen with non-interactive also fails:

$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A --mtu=507 --char-write-req --handle=0x002a --value=41 --char-read --handle 0x002f
Characteristic value/descriptor: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

I am using a pretty hacked together python3 script that uses pygatt, with a gatttool backend. It works fine, other than its ugliness and lack of error trapping:

#! /usr/bin/env python3

import pygatt
import time
from struct import unpack
import csv
from datetime import timedelta
from datetime import datetime
import os
from influxdb import InfluxDBClient

## This changes directory to the location of the script when called by crontab (on external storage device):
abspath = os.path.abspath(__file__)
dirname = os.path.dirname(abspath)
os.chdir(dirname)

## Change this to RD200 MAC:
radon_mac = 'XX:XX:XX:XX:XX:XX'

## influx configuration - edit these:
ifuser = "username"
ifpass = "password"
ifdb   = "dbname"
ifhost = "127.0.0.1"
ifport = 8086
measurement_name = "Radon"

## Use GATTTool backend on RPi:
adapter = pygatt.GATTToolBackend()

complete = False
history = []
## Get current time:
now = datetime.now()

## Round to the nearest 5 minutes (not required):
now = now - timedelta(minutes=now.minute % 5,
                      seconds=now.second,
                      microseconds=now.microsecond)

## This manages history data and writes to CSV
## Eventually I want to clear/recreate InfluxDB table with new info
## handle_data gets the history values using 0x41:
def handle_data(handle, value):
#    print("Received data: %s" % value.hex())
    
    ## Using global lets me tell main program that this is finished
    global complete
    global history
    req_no = value.pop(0)
    tot_resps = value.pop(0)
    this_resp = value.pop(0)
    num_values = value.pop(0)
    ## After removing the first 4 bytes, the rest contain history in Bq/m^3:
    Bq_m3 = unpack('<'+'H'*(len(value)//2),value)
#    print(len(Bq_m3))
    Bq_m3 = list(Bq_m3)
    pCi_L = [x/37 for x in Bq_m3]
    resp_len = len(Bq_m3)
    if num_values != resp_len:
        print("Number of values didn't match!")

#    print(f"Requested {req_no}, got {tot_resps} replies, this is reply {this_resp} containing {num_values} values.")
#    print(f"Values: {value.hex()}")
#    print(f"First 4 values out of {resp_len} total in this response: {Bq_m3[0:4]} Bq/m^3")
#    print(f"First 4 values out of {resp_len} total in this response: {pCi_L[0:4]} pCi/L")

    ## This adds the latest values to history:
    history.extend(pCi_L)

    ## If this response is the last response, set complete to True:
    if this_resp == tot_resps:
        #print("Completed")
        complete = True


## handle_current gets the current data using 0x40:
def handle_current(handle, value):
    currentTime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    ## take a timestamp for this measurement
    time = datetime.utcnow()

    #print("Received data: %s" % value.hex())
    
    ## global complete lets the main program know this is finished:
    global complete
    req_no = value.pop(0) # first byte is 0x40 from write
    extra_no = value.pop() # don't know what last byte is for... removed it
    
    Bq_m3 = unpack('<'+'H'*(len(value)//2),value)
#    print(len(Bq_m3))
    resp_len = len(Bq_m3)
#    print(f"Values: {value.hex()}")
#    print(Bq_m3)
    latest = Bq_m3[16]/37 # the latest value is stored at 16
    peak = Bq_m3[25]/37 # the peak value is stored at 25
#    print(currentTime)
    print(f"Latest value = {latest:.2f} pCi/L at {currentTime}")
    print(f"Peak value = {peak:.2f} pCi/L")
    
    ## Append to CSV file using rounded time (now):
    with open("RadonCurrentValues.csv", "a") as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow([now,latest,peak])

    ## format the data as a single measurement for influx
    body = [
        {
        "measurement": measurement_name,
        "time": time,
        "fields": {
            "radon": latest,
            "peak": peak
            }
        }
    ]
    ## connect to influx
    ifclient = InfluxDBClient(ifhost,ifport,ifuser,ifpass,ifdb)
    ## write the measurement
    ifclient.write_points(body)

    complete = True

try:
    adapter.start()

    try:
        device = adapter.connect(radon_mac,timeout=20)
    except:
        print("Couldn't connect, retrying...")
        device = adapter.connect(radon_mac,timeout=20)

    ## Set mtu to get the full history responses:
    try:
        device.exchange_mtu(507)
    except:
        print("Couldn't set MTU, retrying...")
        device.exchange_mtu(507)

    
    print("Subscribing to history response...")
    
    ## For some reason there is no subscribe_handle, even though char_write_handle exists... weird.
    #device.subscribe_handle(0x002f,
    #                 callback=handle_data,
    #                 indication=False,
    #                 wait_for_response=False)

    ## Subscribe to UUID:
    device.subscribe("00001526-0000-1000-8000-00805f9b34fb",
                     callback=handle_data,
                     indication=False,
                     wait_for_response=False)

    ## Request history data by sending 0x41:
    print("Requesting data...")
    device.char_write_handle(0x002a,
                      bytearray([0x41]),
                      wait_for_response=False)

    ## Could also subscribe to UUID instead of handle:
    #device.char_write("00001524-0000-1000-8000-00805f9b34fb",
    #                  bytearray([0x41]),
    #                  wait_for_response=False)

    while not (complete) :
        time.sleep(1) # wait one second until handle_data sets complete to True
#        print(f"Complete = {complete}")

    device.unsubscribe("00001526-0000-1000-8000-00805f9b34fb")

    ## This doesn't store dates for history, just one per hour
    ## Cound history and subtract 1 hour each (assuming device was kept running continuously):
    historyCount = len(history)
    difference = timedelta(hours=1)
    times = [now - x*difference for x in list(range(historyCount-1, -1, -1))]
    
    ## Write history values to CSV file or DB here (new file each time):
    with open("RadonHistory.csv", "w") as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(["Timestamp","Radon (pCi/L)"])
        for value in range(len(history)):
            writer.writerow([times[value], history[value]])


    ## Now, set complete to False again to get current values:
    complete = False

    ## Make sure file exists, otherwise write header:
    if not os.path.isfile("RadonCurrentValues.csv"):
        with open("RadonCurrentValues.csv", "w") as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(["Timestamp","Radon (pCi/L)","Peak value (pCi/L)"])


    print("Subscribing to current response...")
    
    ## Subscribe to 1525 for current data:
    device.subscribe("00001525-0000-1000-8000-00805f9b34fb",
                     callback=handle_current,
                     indication=False,
                     wait_for_response=False)
    
    print("Requesting data...")

    ## Request current data by sending 0x40:
    device.char_write_handle(0x002a,
                      bytearray([0x40]),
                      wait_for_response=False)

    while not (complete) :
        time.sleep(1) # wait one second until handle_data sets complete to True
#        print(f"Complete = {complete}")

    #device.unsubscribe("00001525-0000-1000-8000-00805f9b34fb")
    ## Why is it not subscribed? Leave it out...
    ## For some reason that caused an error

finally:
    adapter.stop()
#    print(f"Historical data (Bq/m^3): {history}")
    print(f"Total hours returned for history: {len(history)}")

Pygatt required me to subscribe to the response to get the correct data. I don’t know how that relates to using non-interactive gatttool, but just reading from the handle would return 0’s unless it was a response to the request.