semiconductive
Hazard to Others
 
Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline
Mood: Explorative
|
|
Inkbird IDT-34c-B temperature probe to Raspberry PI
Temperature probes are useful in process control, especially when they can be easily read on an embedded computer like a Raspberry PI.
This thread is a (barely working) hack to get the Inkbird IDT-34c-B "barbeque" temperature probe to interface with a Raspberry PI. This thread
follows up on my OWON BT41T+ multimeter interface thread from last year.
The code in this post is experimental, unreliable, and there is no warranty of any kind. I post this for educational purposes and in hopes that it
might be useful to someone else.
Unfortunately:
The linux documention on Bluetooth is very sparse and unclear. As of this writing (2025) the BlueZ 5. daemon handles bluetooth low energy devices
reliably. But, the daemon is designed to talk to the rest of Linux through a package called D-bus (the desktop bus.).
Rather than write a low level driver like the Owon BT41 meter has, I decided to see if I can access the bluetooth generically using python.
After experimenting with several D-bus libraries under python, it's obvious to me that there are many problems in the D-bus system. Most D-bus
libraries for python are not being maintained and have aged to the point where bugs make them useless with Blue-Tooth low energy devices.
A less powerful library, but at least one that is coded well and being actively maintained is called DASBUS. I have successfully written a python
program that reads the inkbird four temperature device and prints the four temperatures onto a terminal prompt using DASBUS.
This hack is very slow and has problems, but it's very simple to understand and may be useful for educational purposes. eg: To experiment with on
Linux/Raspberry PI platforms. It's also likely to work for many versions of Linux because of how few assumptions I made in it.
If you know about bluetooth interfacing, I'd appreciate comments and pointers on how to correctly and more reliably interface to this device.
Online resources are so obscure that it will take me weeks to figure anything more out (if ever), although I'm willing to share my knowledge as I get
it.
My code works by searching the Linux D-bus object manager for devices on HCi0 (the first bluetooth adapter under linux). I look for a device named
"IDT-34c-B". If the code does not find a device, it will start an active scan for sleeping/offline bluetooth low energy devices. After waiting 10
seconds, the code will then re-check for the IDT-34c-B. If it still doesn't find a device, it will exit with an error message.
For production environments, a callback scan or a 'call me on change status' would be better than what I have done. But I'm not yet familiar enough
with the bluetooth stack under Linux to reliably write that kind of code.
Therefore, I merely open the bluetooth device *before* I re-read all entries in the D-bus object manager searching for "GATT" chars that belong to
the already found inkbird low energy device.
I do this because gatt variables do not always show up on my Raspberry PI until the bluetooth device is connected to.
I don't have a data-sheet for the inkbird, and it also uses proprietary UUIDS.
So, if you get a different device revision it might have slightly different behavior.
You may need to debug this code to get it to work with your device.
The command "busctl bluez tree", is extremely helpful in finding out how the GATT device names have changed. In my device, the tree goes:
| Quote: |
└─ /org
└─ /org/bluez
├─ /org/bluez/hci0
│ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F
│ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0001
│ │ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0001/char0002
│ │ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0001/char0004
│ │ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0001/char0006
│ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0008
│ │ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0008/char0009
│ │ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0008/char0009/desc0>
│ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000c
│ │ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000c/char000d
│ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000f
│ │ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000f/char0010
│ │ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000f/char0010/desc0>
│ │ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000f/char0010/desc0>
│ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014
│ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0015
│ │ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0015/desc0>
│ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0018
│ │ └─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0018/desc0>
│ ├─ /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char001b
|
If you can't get this tree and your bluetooth device has power, you might explore command line tool hcitool or bluetoothctl and manually 'trust' your
device.
Nodes on my tree labeled called "desc" are acutally useless. It's supposed to be a descriptive comment like "farenheight" or "Celsius" -- but I don't
see any useful meta information in my inkbird under desc. The node immediately before the desc, is useful!
After manually probing several GATT interfaces, noticed that GATT data changes with temperature on only one node of one path. The useful data is in
the first char path node of the special uuid group, "0000ff00-0000-1000-8000-00805f9b34fb"
In my tree the useful GATT are only in the path containing the name service0014. But I wrote my code to figure out the service number and path
automatically. I did this in hopes that the program will work with more device revisions. My code should still work for as long as the UUIDs truly
are unique...
When I invoked "characteristic.ReadValue({})" on the 0000FF01 ... Gatt interface -- a 13 byte integer array was returned with four sets of little
endian integers in a row.
I noticed that these first four pairs of numbers appear to correspond to four temperature probe values in Farenheight, scaled by a factor of 10.
Therefore; I wrote a simple procedure to translate a little endian pair of bytes into a properly scaled decimal value.
I then print the temperature in Farenheight on the command line.
Immediately after, I CLOSE the inkbird device. Which, I'd rather not do ...
Because a useful program would repetitively read the temperatures and log them to a file as quickly as possible.
Closing the device means it takes around 15 seconds to run the whole program and I only get one set of four temperatures read.
Unfortunately:
Any attempt I made to read the temperatures without closing and re-opening the device crashed the GATT interfaces. I'm only able to successfully call
the ".ReadValue()" D-BUS method once each time I've opened a connection to the inkbird device.
I do not know why multiple readings aren't working. My code may be doing something wrong because I'm not accessing the device in a "normal" way.
(There's really no useful documentation available that I can find!)
I do know that when the "ReadValue()" method is invoked FOR ANY of the GATT interfaces that *ALL* the GATT chars get read and cached.
I mean, even GATT chars that I didn't call "ReadValue()" on.
I can access all cached Values from all GATTs FF01 to FF07 as many times as I like without problems. ( Athough I have no idea what the other values
mean. ) For this reason, Therefore I read an arbitrary GATT char, and discard the return value in favor of reading results as many times as I like
from the cached GATT variable parameter.
Unfortunately, these cached values do not update. The inkbird device will dis-appear before any of the numbers change.
| Code: |
#!/bin/env python
# Read temperature from inkbird IDT-34c-B
# Written November, 2025 by Andrew Robinson of Scappoose.
# Version 0.99.0
# This code is released under the GNU public license, 3.0
# https://www.gnu.org/licenses/gpl-3.0.en.html
#
# Pre-Requisites virtual python, and dasbus.
# Virtual python is required on most systems with system package installs of python.
# It allows a local user (not superuser) to install python packages without wrecking
# the operating system's version of python.
#
# python3 -m venv --system-site-packages py_envs
# pip3 install dasbus
# Note, the inkbird protocol is proprietary and may change.
# To scan your active bluetooth devices and services do:
# busctl tree org.bluez
import time
import signal
from dasbus.connection import SystemMessageBus
from dasbus.loop import EventLoop
from dasbus.typing import Variant
inkbird_path=False
INKBIRD_NAME='IDT-34c-B'
ADAPTER_PATH = "/org/bluez/hci0" # Service is org.bluez.Adapter1
SERVICE_NAME = "org.bluez"
MANAGER_IFACE="org.freedesktop.DBus.ObjectManager"
PROP_IFACE="org.freedesktop.DBus.Properties"
DEVICE_IFACE="org.bluez.Device1"
ADAPTER_IFACE="org.bluez.Adapter1"
GATT_SERVICE_IFACE="org.bluez.GattService1"
GATT_CHAR_IFACE="org.bluez.GattCharacteristic1"
GATT_DESC_IFACE="org.bluez.GattDescriptor1"
TEMPERATURE_UUID="0000ff00-0000-1000-8000-00805f9b34fb"
bus=SystemMessageBus()
loop=EventLoop()
adapter = bus.get_proxy( SERVICE_NAME, ADAPTER_PATH )
manager = bus.get_proxy( SERVICE_NAME, '/' )
def signal_handler( signum, frame ):
try:
adapter.SetDiscoveryFilter({})
adapter.StopDiscovery()
except Exception as e:
print(f"Could not stop discovery: {e}")
raise
if (signum is None):
quit(2)
loop.quit()
def find_inkbird_path():
""" Scans through managed objects to locate first inkbird. """
managed_objects = manager.GetManagedObjects()
for obj_path, interfaces in managed_objects.items():
if DEVICE_IFACE in interfaces:
device_properties = interfaces[DEVICE_IFACE]
name = device_properties.get('Name').unpack()
if ( name == INKBIRD_NAME ):
return obj_path
return False
def find_gatt_characteristics( uuid ):
""" Locate list of open characteristic paths for uuid """
gatt_paths=[]
gatt_service_path=False
managed_objects = manager.GetManagedObjects()
for obj_path, interfaces in managed_objects.items():
if ( obj_path.startswith(inkbird_path) and
GATT_SERVICE_IFACE in interfaces and
interfaces[GATT_SERVICE_IFACE]["UUID"].unpack()==uuid
):
gatt_service_path=obj_path
break
else:
return False
for obj_path, interfaces in managed_objects.items():
if ( GATT_CHAR_IFACE in interfaces and
obj_path.startswith(gatt_service_path)
):
gatt_paths.append( obj_path )
return gatt_paths
inkbird = find_inkbird_path()
if (inkbird_path==False):
print("Active scan initiated")
try:
adapter.Set( ADAPTER_IFACE,"Pairable", Variant("b", False) )
adapter.SetDiscoveryFilter({
"UUIDs":Variant("as",[TEMPERATURE_UUID]),
"Transport":Variant("s","le")
})
signal.signal(signal.SIGINT, signal_handler )
signal.signal(signal.SIGTERM, signal_handler )
adapter.StartDiscovery()
time.sleep(10)
inkbird_path = find_inkbird_path()
except Exception as e:
print(f"exception {e}")
signal_handler( None,None )
else:
adapter.StopDiscovery()
if (inkbird_path == False):
print( "Inkbird device not found." )
quit()
def temperature( lsbyte, msbyte ):
value = (msbyte<<8) + lsbyte
t = value/10
print( "% 4.1f F"%t )
print("Found Inkbird.", inkbird_path )
inkbird = bus.get_proxy( SERVICE_NAME, inkbird_path )
print("Connect to inkbird")
inkbird.Connect()
gatt=find_gatt_characteristics( TEMPERATURE_UUID )
if (gatt != False ):
gatt.sort()
characteristic = bus.get_proxy( SERVICE_NAME, gatt[0] )
try:
characteristic.ReadValue({})
except Exception as e:
print(f"exception {e}")
b = characteristic.Value
temperature( *b[0:2] )
temperature( *b[2:4] )
temperature( *b[4:6] )
temperature( *b[6:8] )
else:
print( "Failed to find GATT characteristics.")
#
inkbird.Disconnect()
|
[Edited on 13-11-2025 by semiconductive]
|
|
|
semiconductive
Hazard to Others
 
Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline
Mood: Explorative
|
|
Sample run of code:
In my .bashrc login shell, or as the first thing done after logging in, I run:
source /home/my_home_directory/py_envs/bin/activate
This turns on virtual python and dasbus.
The program from the last post is already saved as inkbird.py, and marked as executable. This is what happens when I run it.
| Code: |
(py_envs) andrew3@raspberrypi:~ $ ./inkbird.py
Active scan initiated
Found Inkbird. /org/bluez/hci0/dev_A4_C1_38_B7_5B_0F
Connect profile
259.0
70.2
70.6
70.3
|
Probe 1 was touching a soldering iron, probes 2 to 4 were just laying on a desk in open air.
[Edited on 12-11-2025 by semiconductive]
|
|
|
semiconductive
Hazard to Others
 
Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline
Mood: Explorative
|
|
I figured out how to monitor the bluetooth stack under linux/raspberry PI version 3B.
Several commands are out of date in this next video, but the basic process is still the same:
https://www.youtube.com/watch?v=a77soe8oFVI
Linux bluetooth version 5.4 or greater is needed to avoid serious bugs, my raspberry PI shows:
The bluetooth bus can be monitored with:
| Code: |
sudo dbus-monitor --system "destination='org.bluez'" "sender='org.bluez'" > log.txt &
tail -F log.txt
|
However gatttool was designed for the previous bluez damon version, and often fails to talk to inkbird correctly under bluez 5.8.
And ... of course the proposed replacement that does exist in bluez-tools, btgatt-client, as of nov-2025 is not part of the PI repository. sigh.
| Code: |
sudo apt-get install bluez bluez-tools
(py_envs) andrew3@raspberrypi:~ $ bt
bt-adapter btattach btfdiff btmgmt bt-network btuart
bt-agent bt-device bthelper btmon bt-obex
|
So, the only other choice I have is to use bluetoothctl, and figure out how to do gatt with it:
| Code: |
sudo bluetoothctl
Agent registered
[bluetoothctl]> scan le
SetDiscoveryFilter success
hci0 type 6 discovering on
...
[bluetoothctl]> connect A4:C1:38:B7:5B:0F
Attempting to connect to A4:C1:38:B7:5B:0F
Connection successful
[IDT-34c-B]>
|
At this point, if I wait, my inkbird device will suddenly list Primary service handles, and characteristics, but then a few moments later will
disconnect.
| Code: |
[NEW] Primary Service (Handle 0x0001)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0001
00001800-0000-1000-8000-00805f9b34fb
Generic Access Profile
[NEW] Characteristic (Handle 0x0002)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0001/char0002
00002a00-0000-1000-8000-00805f9b34fb
Device Name
[NEW] Characteristic (Handle 0x0004)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0001/char0004
00002a01-0000-1000-8000-00805f9b34fb
Appearance
[NEW] Characteristic (Handle 0x0006)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0001/char0006
00002a04-0000-1000-8000-00805f9b34fb
Peripheral Preferred Connection Parameters
[NEW] Primary Service (Handle 0x0008)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0008
00001801-0000-1000-8000-00805f9b34fb
Generic Attribute Profile
[NEW] Characteristic (Handle 0x0009)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0008/char0009
00002a05-0000-1000-8000-00805f9b34fb
Service Changed
[NEW] Descriptor (Handle 0x000b)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0008/char0009/desc000b
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[NEW] Primary Service (Handle 0x000c)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000c
0000180a-0000-1000-8000-00805f9b34fb
Device Information
[NEW] Characteristic (Handle 0x000d)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000c/char000d
00002a50-0000-1000-8000-00805f9b34fb
PnP ID
[NEW] Primary Service (Handle 0x000f)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000f
00010203-0405-0607-0809-0a0b0c0d1912
Vendor specific
[NEW] Characteristic (Handle 0x0010)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000f/char0010
00010203-0405-0607-0809-0a0b0c0d2b12
Vendor specific
[NEW] Descriptor (Handle 0x0012)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000f/char0010/desc0012
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[NEW] Descriptor (Handle 0x0013)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service000f/char0010/desc0013
00002901-0000-1000-8000-00805f9b34fb
Characteristic User Description
[NEW] Primary Service (Handle 0x0014)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014
0000ff00-0000-1000-8000-00805f9b34fb
Unknown
[NEW] Characteristic (Handle 0x0015)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0015
0000ff01-0000-1000-8000-00805f9b34fb
Unknown
[NEW] Descriptor (Handle 0x0017)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0015/desc0017
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[NEW] Characteristic (Handle 0x0018)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0018
0000ff02-0000-1000-8000-00805f9b34fb
Unknown
[NEW] Descriptor (Handle 0x001a)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0018/desc001a
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[NEW] Characteristic (Handle 0x001b)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char001b
0000ff03-0000-1000-8000-00805f9b34fb
Unknown
[NEW] Descriptor (Handle 0x001d)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char001b/desc001d
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[NEW] Characteristic (Handle 0x001e)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char001e
0000ff04-0000-1000-8000-00805f9b34fb
Unknown
[NEW] Descriptor (Handle 0x0020)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char001e/desc0020
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[NEW] Characteristic (Handle 0x0021)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0021
0000ff05-0000-1000-8000-00805f9b34fb
Unknown
[NEW] Descriptor (Handle 0x0023)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0021/desc0023
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[NEW] Characteristic (Handle 0x0024)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0024
0000ff06-0000-1000-8000-00805f9b34fb
Unknown
[NEW] Descriptor (Handle 0x0026)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0024/desc0026
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[NEW] Characteristic (Handle 0x0027)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0027
00002a19-0000-1000-8000-00805f9b34fb
Battery Level
[NEW] Descriptor (Handle 0x0029)
/org/bluez/hci0/dev_A4_C1_38_B7_5B_0F/service0014/char0027/desc0029
00002902-0000-1000-8000-00805f9b34fb
Client Characteristic Configuration
[CHG] Device A4:C1:38:B7:5B:0F UUIDs: 00001800-0000-1000-8000-00805f9b34fb
[CHG] Device A4:C1:38:B7:5B:0F UUIDs: 00001801-0000-1000-8000-00805f9b34fb
[CHG] Device A4:C1:38:B7:5B:0F UUIDs: 0000180a-0000-1000-8000-00805f9b34fb
[CHG] Device A4:C1:38:B7:5B:0F UUIDs: 0000ff00-0000-1000-8000-00805f9b34fb
[CHG] Device A4:C1:38:B7:5B:0F UUIDs: 00010203-0405-0607-0809-0a0b0c0d1912
[CHG] Device A4:C1:38:B7:5B:0F ServicesResolved: yes
[CHG] Device A4:C1:38:B7:5B:0F Name: INKBIRD
[CHG] Device A4:C1:38:B7:5B:0F Alias: INKBIRD
[CHG] Device A4:C1:38:B7:5B:0F Appearance: 0x0300 (768)
[CHG] Device A4:C1:38:B7:5B:0F Modalias: usb:v248Ap8266d0001
hci0 A4:C1:38:B7:5B:0F type LE Public disconnected with reason 3
[CHG] Device A4:C1:38:B7:5B:0F ServicesResolved: no
[CHG] Device A4:C1:38:B7:5B:0F Connected: no
hci0 type 6 discovering off
|
The process is very time sensitive!
You don't really have time to think and type in stuff.
I need to do the following commands in order, and at appropriate moments:
| Code: |
bluetoothctl
scan le
connect A4:C1:38:B7:5B:0F
menu gatt
select-attribute 0000ff01-0000-1000-8000-00805f9b34fb
read
|
This will give me the 13 byte data for the thermometer temperatures.
However, no matter how often I type 'read' again after one successful read, I get the same data and the device disconnects after about 10 seconds.
At least I can inspect the DBUS messages to see what signals appeared and in what order.
After I type, connect ...., this is what typically happens:
Attachment: log.txt (50kB) This file has been downloaded 35 times
The sequence of events goes:
connect command is sent.
A signal for INKBIRD connect happens.
After a variable time delay, a lot of interface added signals appear.
(no data is in the signals, just empty values.)
A read method is called,
and a property change signal appears including the data.
The previous script doesn't wait for service resolution to occur after a connection is established.
This is apparently what is causing all the strange behavior and bugs.
By adding time.sleep(20) after the connect command, I am able to get it to read consistently AND to read multiple times.
Facts learned:
Bluetooth low energy will disconnect after a certain period of time and it's not pairable in the traditional sense. It can be trusted, but that's
all.
Therefore, I waste time checking for the device in the object manager before scanning.
Discovery mode is not incompatible with reading and writing GATT services. If I leave discovery mode on, I can probably work around the disconnection
time-out.
Multiple reads are allowed after the service resolution signal following a 'connect' command is properly waited for.
By comparison of the DBUS messages caused by my script, vs. blutoothctl commands, it's fairly obvious that internally all data from a primary service
is read at-once, when any part of the primary service is read.
I don't know, however, if it is more efficient to read from the cached value or from the read method.
The simplest way I can think to monitor the inkbird temperatures, is to keep an on-going active scan for devices; Then catch all the interface added
signals to build a python dictionary of the device's path structure. When a services resolved signal finally arrives, then immediately read the
desired paths from the inkbird.
I'm not sure how I can keep the inkbird from disconnecting, or whats the fastest read-update cycle I can do, but at least I know enough to make a
reliable script to get the temperature repetitively.
more to come....
[Edited on 18-11-2025 by semiconductive]
|
|
|
semiconductive
Hazard to Others
 
Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline
Mood: Explorative
|
|
After several experiments, I can say the bluetooth low energy daemon with DBUS on the RaspberryPI is --- "very fragile!"
Race conditions happen between applications and get the system into a 'stuck' state where enabling or disabling scanning no longer works. Restarting
the blue tooth service doesn't clear it, and the power cycling feature of adapter hci0 is disabled. I have been forced to reboot my PI several
times.... !!
This happens on my Raspberry PI system:
| Quote: |
uname -a
Linux raspberrypi 6.12.47+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.12.47-1+rpt1 (2025-09-16) aarch64 GNU/Linux
|
To try and work around the problem I rewrote my script. The now script *passively* uses callback functions in order to 'wait' for the services
resolved signal.
But, I don't want to set the 'Notify' handler yet, because I'm not sure how notify works; (Crash course in hacking) so I only use methods Connect()
and Disconnect() in this script.
Connection cycling is very wasteful in terms of system resources, but in theory Connect and Disconnect ought to be utterly safe to do. Connection
cycling keeps the BLE device alive by forcing a 'services resolved' signal to occur repeatedly every 5 to 15 seconds. Therefore, the device is never
removed from the bus by the bluetooth daemon.
In order to avoid scanning conflicts, I also rewrote the script to NOT INITIATE a scan.
All this new script does is passively watch for the 'interfaces added' callback and signal changed -- then it connects, reads temperatures, and
dis-connects. ( That's all folks! )
After starting the script, you may need to manually initiate and stop bluetooth le scanning:
| Code: |
bluetoothctl
scan le
# Wait a few seconds.
scan off
|
The script will generally find the inkbird IDT-34c-B without problem within five seconds.
In my script, a simple for loop goes through the object manager *once* at startup. The for loop sends (possibly stale) "interface added" callbacks to
the script. Therefore, even if you turn scanning on and off before running the new script, the script will still find the device as long as you run
the script within 180 seconds of doing the scan.
I have not mastered the 'notify' API yet, so in this script I brute force poll for data.
I wait for the "services resolved" change signal to occur, I read the temperature data (and the battery level data), and then I Disconnect() the
device and after a pause Connect() it again. .
This method is also a bug tester for the DBUS-bluetooth system and (surprisingly) will generally crash my bluetooth stack within a half hour of
starting. AKA, if someone wants to forward it to the linux bluetooth development team and file a bug report, please do! I don't have an account,
there. I'm willing to mail them an Inkbird device to test with....
The crash:
Typically, watching the bus -- I see four "object manager requests' in a row initiated by the normal RPI desktop (for who knows what reason) and then
a massive data transfer of interfaces happens on D-BUS. During the transfer, my script gets an exception saying my bluetooth LE connection was
terminated 'locally'.
After the exception occurs, regardless of whether I kill my script or not, I can't initiate scans any more nor stop them. My system locks up. I am
not a bluetooth expert. I don't know if DASBUS is crashing the system, or the bug is in dbus itself, or the daemon.
On the other hand, until the randomly occurring exception happens; this script successfully reads the temperature repetitively.
Often the script works for up to a half hour before the hci0 controller crash happens due to other applications interfering.
Note: When I search online, I see several comments about Raspberry PI both 3 (and 4!), not having a fully operational bluetooth stack because of
chipset qualification issues. ( But, I don't know exactly what this means or what to do with the controller I have. )
This script is supplied for educational purposes and debugging, use at your own risk.
There is no warranty.
| Code: |
#!/bin/env python
# Read temperature from inkbird IDT-34c-B
# Written November, 2025 by Andrew Robinson of Scappoose.
# Version 0.99.1 ( Caution: Can crash systems that have bugs in bluetooth stack. )
# This code is released under the GNU public license, 3.0
# https://www.gnu.org/licenses/gpl-3.0.en.html
#
# Pre-Requisites virtual python, and dasbus.
# Virtual python is required on most systems with system package installs of python.
# It allows a local user (not superuser) to install python packages without wrecking
# the operating system's version of python.
#
# python3 -m venv --system-site-packages py_envs
# pip3 install dasbus
# Note, the inkbird protocol is proprietary and may change.
# To scan your active bluetooth devices and services do:
# busctl tree org.bluez
import time
import signal
import os
from dasbus.connection import SystemMessageBus
from dasbus.loop import EventLoop
from dasbus.typing import Variant
INKBIRD_NAME='IDT-34c-B'
ADAPTER_PATH = "/org/bluez/hci0"
SERVICE_NAME = "org.bluez"
PROP_IFACE="org.freedesktop.DBus.Properties"
DEVICE_IFACE="org.bluez.Device1"
ADAPTER_IFACE="org.bluez.Adapter1"
GATT_SERVICE_IFACE="org.bluez.GattService1"
GATT_CHAR_IFACE="org.bluez.GattCharacteristic1"
GATT_DESC_IFACE="org.bluez.GattDescriptor1"
TEMPERATURE_UUID="0000ff00-0000-1000-8000-00805f9b34fb"
bus=SystemMessageBus()
loop=EventLoop()
adapter = bus.get_proxy( SERVICE_NAME, ADAPTER_PATH )
manager = bus.get_proxy( SERVICE_NAME, "/" )
inkbirds={}
gatt_services={}
temperatures={}
batteries={}
def signal_handler( signum, frame ):
if (not loop is None):
loop.quit()
exit(2)
def print_temperatures( data ):
def temperature( lsbyte, msbyte ):
value = (msbyte<<8) + lsbyte
t = (value-320)/18 # Convert to celsius
print( "% 6.1f [C] % 6.1f [F]"%(t,value/10.) )
temperature( *data[0:2] )
temperature( *data[2:4] )
temperature( *data[4:6] )
temperature( *data[6:8] )
def print_battery( data ):
print( "battery=",data[0],"%" )
def services_resolved_callback( obj_path, obj_iface, obj_dict, invalidated ):
if not 'ServicesResolved' in obj_dict:
return
if obj_dict['ServicesResolved'].unpack()==True:
try:
print_temperatures( temperatures[obj_path].ReadValue({}) )
print_battery( batteries[obj_path].ReadValue({}) )
except Exception as e:
print(f"BLE ReadValue() failed: {e}")
try:
inkbirds[obj_path].Disconnect()
time.sleep(1)
inkbirds[obj_path].Connect()
except Exception as e:
print(f"Disconnect-Re-Connect failed: {e}")
def interface_added_callback( obj_path, obj_dict ):
if DEVICE_IFACE in obj_dict:
properties = obj_dict[DEVICE_IFACE]
try:
name = properties.get('Name').unpack()
except:
return # Ignore weird quirk where invalid data comes with interface.
if (name == INKBIRD_NAME):
new_inkbird = bus.get_proxy( SERVICE_NAME, obj_path )
inkbirds[ obj_path ]=new_inkbird
print( "Added inkbird device",obj_path )
print( new_inkbird.Disconnect() )
signal_callback = lambda a,b,c : services_resolved_callback( obj_path, a,b,c )
print( new_inkbird.PropertiesChanged.connect( signal_callback ) )
inkbirds[obj_path].Connect()
print( "initial connection.")
return
if GATT_SERVICE_IFACE in obj_dict:
properties=obj_dict[GATT_SERVICE_IFACE]
if properties["UUID"].unpack()==TEMPERATURE_UUID:
if properties["Device"].unpack() in inkbirds:
print("added GattService",obj_path)
gatt_services[ obj_path ]=True
return
if GATT_CHAR_IFACE in obj_dict:
parent_path = os.path.dirname(obj_path)
if parent_path in gatt_services:
uuid = obj_dict[GATT_CHAR_IFACE]["UUID"].unpack()
proxy = bus.get_proxy( SERVICE_NAME, obj_path )
if uuid.startswith("0000ff01"):
temperatures[os.path.dirname( parent_path )]=proxy
print("added temperature characteristic",obj_path)
if uuid=="00002a19-0000-1000-8000-00805f9b34fb":
batteries[os.path.dirname( parent_path )]=proxy
print("added battery characteristic",obj_path)
return
return
#
# ------------------- Main logic proceedure begins here --------------------------
try:
manager.InterfacesAdded.connect( interface_added_callback )
signal.signal(signal.SIGINT, signal_handler )
signal.signal(signal.SIGTERM, signal_handler )
for obj_path, obj_dict in (manager.GetManagedObjects()).items():
interface_added_callback( obj_path, obj_dict )
loop.run()
except Exception as e:
print(f"Main loop exception {e}")
|
To figure out how the inkbird normally operates, I installed the inkbird app on my phone and did an android bluetooth hci snoop. I'm currently
studying it in detail using wire-shark.
I already notice differences between android and linux Bluez / DBUS.
Eg: The temperature data is in a 13 byte packet instead of an 11 byte packet under handle 0x16.
I see that handle 0x0019 is used during setup, and is likely some kind of control or flag interface.
I see that handle 0x0028 is a battery level in percent. (one byte).
Handle 0x25 sends byte arrays to my phone that grow over time. I suspect this is a history log of temperatures, although I'm not sure. The app
graphs temperature history, and that's a likely use-case.
| Code: |
1549 337.122746 TelinkSemico_b7:5b:0f (IDT-34c-B) TCTmobile_bc:15:8c (TCL 30 Z) ATT 87 Rcvd Handle Value Notification, Handle: 0x0025 (Unknown: Unknown)
Value: 04000000bf8a1f6940fe7ffe7ffe7f0103fe7ffe7ffe7fff02fe7ffe7ffe7ffa02fe7ffe7ffe7ff702fe7ffe7ffe7ffe7ffe7ffe7f0003fe7ffe7ffe7ff602fe7ffe7ffe7fef02fe7f33a5
|
I know that a dis-connected temperature probe reports the value 0xfe7f --> little endian -2
This suggests that the inkbird can't handle below freezing temperatures.
I'll have to test that, later.
When all thermometers are disconnected, handle 0x1c begins reporting.
I think 0x1c is some kind of error/status register.
| Code: |
... during setup of device by direct read ...
948 118.002859 TelinkSemico_b7:5b:0f (IDT-34c-B) TCTmobile_bc:15:8c (TCL 30 Z) ATT 19 Rcvd Handle Value Notification, Handle: 0x001c (Unknown: Unknown)
Value: 00000001400620
...
... after all probes disconnected, by automatic Notify ...
1321 238.632238 TelinkSemico_b7:5b:0f (IDT-34c-B) TCTmobile_bc:15:8c (TCL 30 Z) ATT 25 Rcvd Handle Value Notification, Handle: 0x0016 (Unknown: Unknown)
Value: fe7ffe7ffe7ffe7ffe7ffe7f7f
1322 238.646988 TelinkSemico_b7:5b:0f (IDT-34c-B) TCTmobile_bc:15:8c (TCL 30 Z) ATT 19 Rcvd Handle Value Notification, Handle: 0x001c (Unknown: Unknown)
Value: 00000000400620
1334 245.952295 TelinkSemico_b7:5b:0f (IDT-34c-B) TCTmobile_bc:15:8c (TCL 30 Z) ATT 25 Rcvd Handle Value Notification, Handle: 0x0016 (Unknown: Unknown)
Value: fe7ffe7f0d03fe7ffe7ffe7f7f
1335 245.952750 TelinkSemico_b7:5b:0f (IDT-34c-B) TCTmobile_bc:15:8c (TCL 30 Z) ATT 19 Rcvd Handle Value Notification, Handle: 0x001c (Unknown: Unknown)
Value: 00000100400620
|
If I assume a byte per temperature probe of flags, then I suspect the number 1 appears when a probe is plugged in on the first four most significant
bytes of the Value of handle 0x1c.
Probe 4 = 00000001
Probe 3 = 00000100
Probe 2 = 00010000
Probe 1 = 01000000
Thoughts:
Because of the race condition during Connect(), I don't want to Connect() and Disconnect() from the device a large number of times as it risks
crashing the bluetooth hci-interface.
But, because the device records a log of temperatures, I'm going to assume the record is not longer than a long cooking/grilling session of 4 hours.
Therefore, I'll want to restart the device at least once every four hours to avoid putting the device into 'edge case' conditions where bugs are
likely to show up.
|
|
|
semiconductive
Hazard to Others
 
Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline
Mood: Explorative
|
|
Here is an alpha version of the callback version of the driver.
It's cantankerous to get the inkbird to attach -- but it works once you've succeeded.
( I'm not a bluetooth programmer, and I haven't read the 3000 page manual. There's no warranty. )
But: This code is fairly stable and will read and print temperatures for very long periods of time once you get your device to connect. ( Subject to
battery life... )
Note: I've come to the conclusion that the inkbird™ uses it's own proprietary pairing system. Attempting to pair inkbirds™ using standard
bluetooth methods, always fails. Also, only a full Connect() method works for the device as Connect_profile() always hangs.
The problem might be because the Raspberry PI bluetooth chipset is substandard, or it might be because the inkbird design isn't fully compatible. I
don't know.
But: The device will never stay online for more than two minutes unless you give a proper proprietary control code sequence after connecting to it.
From a fresh power-up; bluetooth discovery is followed by gatt services being resolved, and then a pre-defined control code is sent:
Variant('ay',[0xfd,0x00,0x00,0x00,0x00,0x00,0x00]) --> handle 0x19. ( Gatt Characteristic 0x0000ff02-... ). # Ask user to push the button on the
inkbird!!!
The device will blink the bluetooth icon for about a minute. During this time, if you press the light button on the inkbird™, the device will send
code '0xfd' back to handle 0x19. But: If you wait too long, or an error occurs, the device will disconnect.
Once the button is pushed at the correct time -- the device can stay online indefinitely. ( This is the secret to happiness! )
Immediately after the device's button is pushed, the inkbird™ app on android would (without any extra information coming from the inkbird) to send
an identification code to handle 0x19.
I don't know how android chooses the code, or maybe it's just random ?
The identifier code starts with the byte value 0x19 -- and is followed by six bytes. This code allows configuration mode to be entered for the
inkbird device. This six byte code must be reused for as long as the batteries on the device do not go dead.
( It's effectively a 'binding' identifier of some kind. )
This is one code that has been seen in the wild...
[ 0x19,0x28,0x93,0x28,0x69,0x5d,0x01 ]
If it doesn't work, your device will repetitively disconnect immediately after you push the button on the device. You'll be somewhat stuck.
Because, to re-connect without pressing the button, you need to know a passcode
command that starts with byte 0xfc. The typical pattern is: [ 0xfc, XX, 0x03, 0x27, 0x93, 0x28, 0x69, YY ] Where XX and YY are cryptic.
I don't intend to crack any encryption. But, the reconnect code is somehow computed from the random byte sequence starting with 0x19.
I just copied the code which android sent to the inkbird (as shown above), and the same passcode works over and over again on my inkbird. So, I think
a hash code is computed from the random code in order to re-connect the inkbird.
Comments from technical geniuses welcome.
| Code: |
#!/bin/env python
# Read temperature from inkbird IDT-34c-B
# Written November, 2025 by Andrew Robinson of Scappoose.
# Version 0.99.2 (unstable alpha code).
# This code is released under the GNU public license, 3.0
# https://www.gnu.org/licenses/gpl-3.0.en.html
#
# Pre-Requisites virtual python, and dasbus.
# Virtual python is required on most systems with system package installs of python.
# It allows a local user (not superuser) to install python packages without wrecking
# the operating system's version of python.
#
# python3 -m venv --system-site-packages py_envs
# pip3 install dasbus
# Note, the inkbird protocol is proprietary and may change.
# To list your active bluetooth devices and services do:
# busctl tree org.bluez
# dsbus introspect -y -d "org.bluez" -o "/org/bluez/hci0"
# Scan
# frame 672: Write Link policy settings: 0101 [!Park][Sniff][!Hold][Role_switch]
# frame 676: Supported LE features (Read-remote features)
# LE Encryption, Extended Reject Indication, Peripheral-Initiated Features Exchange, Ping, Data Packet Length Extension, Channel Selection Algorithm #2
# frame 679: ReadRemoteVersionInformation:
# Manufacturer Name: Telink Semiconductor Co. Ltd (0x0211), LMP version: 5.0 LMP subversion: 7196
import time
import signal
import os
from dasbus.connection import SystemMessageBus
from dasbus.loop import EventLoop
from dasbus.typing import Variant
# Characteristic properties bitmask
# [Extended][Auth_Sign][Indicate][Notify] [Write][Write-NoResp][Read][Broadcast]
INKBIRD_NAME='IDT-34c-B'
ADAPTER_PATH = "/org/bluez/hci0"
SERVICE_NAME = "org.bluez"
PROP_IFACE="org.freedesktop.DBus.Properties"
DEVICE_IFACE="org.bluez.Device1"
ADAPTER_IFACE="org.bluez.Adapter1"
GATT_SERVICE_IFACE="org.bluez.GattService1"
GATT_CHAR_IFACE="org.bluez.GattCharacteristic1"
GATT_DESC_IFACE="org.bluez.GattDescriptor1"
TEMPERATURE_UUID="0000ff00-0000-1000-8000-00805f9b34fb"
bus=SystemMessageBus()
loop=EventLoop()
adapter = bus.get_proxy( SERVICE_NAME, ADAPTER_PATH )
manager = bus.get_proxy( SERVICE_NAME, "/" )
inkbirds={}
gatt_services={}
commands={}
temperatures={}
batteries={}
bind={}
def signal_handler( signum, frame ):
if (not loop is None):
loop.quit()
exit(2)
def print_temperature( data ):
def temperature( lsbyte, msbyte ):
value = ((msbyte^0x80)<<8)+lsbyte - 0x8000
if (value == 32766):
print(" ----- [°C] ----- [°F]")
return
t = (value-320)/18 # Convert to celsius
print( "% 6.1f [°C] % 6.1f [°F]"%(t,value/10.) )
return
temperature( *data[0:2] )
temperature( *data[2:4] )
temperature( *data[4:6] )
temperature( *data[6:8] )
def print_battery( data ):
print( "battery=",data[0],"%" )
def temperature_callback( obj_path, obj_iface, obj_dict, invalidated ):
print("Temperature notify\t", obj_path, invalidated )
if ( "Value" in obj_dict ):
print_temperature( obj_dict['Value'].unpack() )
return True
def command_callback( obj_path, obj_iface, obj_dict, invalidated ):
print( "Command notify\t\t", obj_path,invalidated )
if ( "Value" in obj_dict ):
print( "Value=",obj_dict['Value'].unpack() )
return True
def extra_callback( obj_path, obj_iface, obj_dict, invalidated ):
print( "Extra notify\t\t", obj_path,invalidated )
if ( "Value" in obj_dict ):
print( "Value=",obj_dict['Value'].unpack() )
return True
def battery_callback( obj_path, obj_iface, obj_dict, invalidated ):
print("Battery notify\t\t", obj_path, invalidated )
if ( "Value" in obj_dict ):
print_battery( obj_dict['Value'].unpack() )
return True
def bind_notify( proxy, callback, o_path ):
proxy.PropertiesChanged.connect(
lambda o_iface,o_dict,o_inval:callback(o_path,o_iface,o_dict,o_inval)
)
proxy.StartNotify()
new_pair=[
Variant('ay',[0xfd,0x00,0x00,0x00,0x00,0x00,0x00]), # Thermometer waits 60 seconds for button press
]
re_pair=[
[
Variant('ay',[0xfc,0xdc,0x03,0x27,0x93,0x28,0x69,0x74]), # A4_C1_38_3F_87_B9
],
[
Variant('ay',[0xfc,0xce,0x03,0x27,0x93,0x28,0x69,0x45]), # A4_C1_38_B7_5B_0F
]
]
# Generic initialization sequence seen sent to two devices.
init_pair=[
Variant( 'ay', [0x19,0x28,0x93,0x28,0x69,0x5d,0x01] ), # Troublesome hash, or random?
Variant( 'ay', [0x02,0x01,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x02,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x04,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x08,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x04,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0446
Variant( 'ay', [0x06,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0663
Variant( 'ay', [0x08] ), # 0x080f00
Variant( 'ay', [0x0a,0x0f,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x0c,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0c5a
Variant( 'ay', [0x0f,0x00,0x00,0x00,0x00,0x00,0x00] ), # *Hash returned,varies.
Variant( 'ay', [0x11,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x111100
Variant( 'ay', [0x13,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x13fe
Variant( 'ay', [0x18]), # self +0x000000000000
Variant( 'ay', [0x24]), # self +0x0f0000000000000000 *droppable
Variant( 'ay', [0x26,0x01]), # self +0x0000000000000000 *droppable
Variant( 'ay', [0x26,0x02]), # self +0x0000000000000000 *droppable
Variant( 'ay', [0x26,0x04]), # self +0x0000000000000000 *droppable
Variant( 'ay', [0x26,0x08]), # self +0x0000000000000000
]
def services_resolved_callback( obj_path, obj_iface, obj_dict, invalidated ):
if not 'ServicesResolved' in obj_dict:
return False
if obj_dict['ServicesResolved'].unpack()==True:
print( "Services resolved." )
try:
for i in bind[obj_path]:
bind_notify(*i)
except Exception as e:
print( f"Binding failed {e}" )
if (obj_path in bind):
del bind[obj_path]
j = 1 if 0x0F==int(obj_path[-2:],16) else 0
print( "j===========",j)
j=0
for i in new_pair:
commands[ obj_path ].WriteValue( i, { 'type':Variant('s','request') } )
for i in init_pair:
commands[ obj_path ].WriteValue( i, { 'type':Variant('s','request') } )
return True
if obj_path in inkbirds:
print("Disconnecting\t\t",obj_path, invalidated )
try:
inkbirds[obj_path].Disconnect()
except Exception as e:
print("Discconect failed");
return True
def interface_added_callback( obj_path, obj_dict ):
if DEVICE_IFACE in obj_dict:
properties = obj_dict[DEVICE_IFACE]
try:
name = properties.get('Name').unpack()
except:
return True # Ignore weird quirk where invalid data comes with interface.
if (name == INKBIRD_NAME):
print( "Adding inkbird device ..",obj_path, end="" )
new_inkbird = bus.get_proxy( SERVICE_NAME, obj_path )
inkbirds[ obj_path ]=new_inkbird
new_inkbird.Disconnect()
new_inkbird.Set( DEVICE_IFACE, "Trusted", Variant('b',True) )
# new_inkbird.Trusted=True # Also works since this is a proxy.
new_inkbird.PropertiesChanged.connect(
lambda a,b,c : services_resolved_callback( obj_path, a,b,c )
)
new_inkbird.Connect()
print( " Connected!")
return True
if GATT_SERVICE_IFACE in obj_dict:
properties=obj_dict[GATT_SERVICE_IFACE]
if properties["UUID"].unpack()==TEMPERATURE_UUID:
if properties["Device"].unpack() in inkbirds:
print("added GattService",obj_path)
gatt_services[ obj_path ]=True
return True
if GATT_CHAR_IFACE in obj_dict:
parent_path = os.path.dirname(obj_path)
if parent_path in gatt_services:
uuid = obj_dict[GATT_CHAR_IFACE]["UUID"].unpack()
proxy = bus.get_proxy( SERVICE_NAME, obj_path )
dev_path = os.path.dirname(parent_path)
if not ( dev_path in bind ):
bind[dev_path]=[]
if uuid.startswith("0000ff01"):
temperatures[dev_path]=proxy
bind[dev_path].append((proxy, temperature_callback, dev_path ))
return
if uuid.startswith("0000ff02"):
commands[dev_path]=proxy
bind[dev_path].append((proxy, command_callback, dev_path ))
return
if uuid.startswith("0000ff"):
if uuid.startswith("0000ff05"):
return
bind[dev_path].append((proxy, extra_callback, dev_path ))
return
if uuid=="00002a19-0000-1000-8000-00805f9b34fb":
batteries[dev_path]=proxy
bind[dev_path].append((proxy, battery_callback, dev_path ))
return
return True
return False
#
# ------------------- Main logic proceedure begins here --------------------------
try:
manager.InterfacesAdded.connect( interface_added_callback )
signal.signal(signal.SIGINT, signal_handler )
signal.signal(signal.SIGTERM, signal_handler )
for obj_path, obj_dict in (manager.GetManagedObjects()).items():
interface_added_callback( obj_path, obj_dict )
loop.run()
except Exception as e:
print(f"Main loop exception {e}")
|
Hints:
You'll need to manually run bluetoothctl in another terminal.
When a connect fails, it's usually best to "remove MM:MM:MM:MM:MM" where MM is the mac address sequence of your device.
Then you can manually start a scan, "scan le", wait a count of about two seconds, and then "scan off".
After multiple failed connections, I often notice the callback functions being called with the same data multiple times. Even though I tried to issue
a .StopNotify() before calling .StartNotify() -- apparently the dbus+bluez daemon is too stupid to clean up the object's state.
caution:
If you re-boot your raspberry PI, and the devices are still trusted, you'll get into situations where you get "Le-connect aborted locally" messages
that kill the inkbird script.
You'll need to manually un-trust the inkbird device and remove it, before the script will work again.
|
|
|
semiconductive
Hazard to Others
 
Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline
Mood: Explorative
|
|
I did a double check, today, to see if there is a consistent way to get the inkbirds to come up without having to press the button every time as-if
pairing them.
I removed the battery from the inkbird, and let it sit overnight.
Then I accessed it using the inkbird app, closed the app, pulled the batteries, waited and retried.
The codes used to allow the inkbird to stay online, today, are different from those used yesterday. I never pushed the button on the inkbird when the
app was running (today).
As I have no intention of cracking the code, I don't know whether the code is a dynamic hash that depends on manufacturer data -- or if it's a
bluetooth channel swapping feature that's normal.
But, since the device doesn't pair using standard bluetooth command -- it means you'll have to decide for yourself if the device is secure enough or
not for your applications.
Definitely, anyone with a snooper could monitor the temperatures sent to my computer. Depending on their skills, they could also figure out what code
my computer authenticated to the device with, and they could reuse my code to gain control of my device.
For peneteration testing concerns:
This is how the authentication phase went immediately after my device services (Gatt) are resolved.
| Quote: |
# From battery pulled condition:
H 0x19 <-- 0xfb000000000000 --> 0xfbe1fa41c25b9d
H 0x19 <-- 0xfb000000000000 --> 0xfb0ad248c2515e
H 0x19 <-- 0xfc8a00c45b2a6907 --> 0xfcee
H 0x19 <-- 0xfca600c45b2a69cd --> 0xfc00
H 0x19 <-- 0x19c95b2a69b801 --> 0x20c95b2a69b801
app-quit.
After battery removed short time, reconfigure goes:
H 0x19 <-- 0xfb000000000000 --> 0xfb29c6751ef005
H 0x19 <-- 0xfc5603135c2a693f --> 0xfc00
H 0x19 <-- 0x19145c2a69cf00 --> 0x20145c2a69cf00
app-quit:
After battery removed for longer time, reconfiguration goes:
H 0x19 <-- 0xfb000000000000 --> 0xfb29c6751ef005
H 0x19 <-- 0xfc5603135c2a693f --> 0xfc00
H 0x19 <-- 0x19145c2a69cf00 --> 0x20145c2a69cf00
|
Yesterday, one of two actions happened -- either the code 0xFD000000... was sent and the button had to be pushed; or a code beginning with 0xfc was
sent and sometimes was accepted with a 0xfc00 response or sometimes was rejected with a 0xfcee response.
If it is rejected, the android phone sends another 0xfc code until it finds the correct one for the device -- or else the device times out.
Today, the process was slightly different.
My inkbirds have been "paired" by android and the inkbird app for over 24 hours, now.
When my android phone first scans the inkbird device from a power up condition of the phone, the phone *inquired* of the inkbird for a hash challenge.
eg: This was done by the phone sending a new code starting with 0xfb0000 ... to the control handle 0x19.
I did not power down my phone, again, after the initial connection. But I did pull the batteries out of the inkbird and waited for it to reset
itself. Then I powered it up again and connected to it using the phone after killing the app and restarting the app.
As you can see from the log, the inkbird sometimes sent different hashes -- sometimes the same hashes.
But when the same hash is sent by the inkbird, an android™ phone immediately responds with the same answer it gave previously.
I think this means I can save challenge response sequences for a particular inkbird device, and save them on my raspberry PI.
I doubt the same hashes will work for different inkbird devices having different MAC address numbers.
SIgh, it's always possible that inkbird may have designed the device to fail if accessed using the same code repeatedly; which would waste my money
(designed obsolence is a problem these days.) -- but I'll at least be able to do a few chemistry experiments before that happens.
I'll report when/if the devices suddenly fail in the next month.
Happy hacking, everyone.
|
|
|
semiconductive
Hazard to Others
 
Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline
Mood: Explorative
|
|
This post contains a barely stable (pre-release) alpha version of a thermometer logger for the IDT-34c-B device from inkbird. 0.99.10. The code is
an open source example offered without warranty, and is not safe for hazard prone processes or where a ruined experiment will bankrupt someone.
I have used this code with two inkbird devices at the same time, which logs 8 temperatures into a file under /tmp/thermal.log. The log file location
is hard-coded. In theory, the code can handle up to 24 thermometers at a time; but I am not sure that 6 bluetooth devices operating simultaneously
won't interfere with each other.
The script doesn't do any form of encryption, which means that every time an ink-bird device dis-connects you will have to manually press the button
on the device to reconnect.
The initial order that the iinkbird devices are 'paired' determines their thermometer number offsets in the logging file.
You do not need to run the program fresh to connect to devices.
The script is set to re-scan the dbus object manager every 90 seconds and attempt to put all inkbird devices into pairing mode. Note: It will not
play nice with incompatible inkbird devices that rename themselves "INKBIRD" -- I don't know how to distinguish models using the manufacturer data and
that isn't implemented in this script. Do not use this script around non IDT-34c-B inkbird devices.
It's important to note that the inkbird button has no 'pairing' effect when the bluetooth icon is just flashing by itself. Rather, the button push to
'connect' the device only works when the there are two black arrows > < are flashing together with the bluetooth icon on the IDT-34c-B.
If devices disconnect, but the program itself is not broken/stopped from running, they will usually reconnect to their original thermometer number:
AKA: so long as no new (unpaired) inkbird devices are added.
Because the communications are un-reliable, this code should not be used to control batch processes with fast changing heating units. You need have
safety devices or trained people that prevent over-heating separately from this device. EG: If you are grilling mushrooms, you need to be watching
them or it is quite possible they will catch on fire when bluetooth quits for no apparent reason.
I use electrical drop out switches that shut down my experiments (and ruins them, usually) in the even of power failure. But, redoing an experiment
is safer than fires starting.
Be safe!
The purpose of open source is to allow you access to the code so that bugs can be corrected by you or by professional programmers, and adapted to your
needs.
There are likely bugs in this driver/interface that I don't know about.
It is a best effort design with filtering of temperatures to statistically reduce severely corrupted data. But, I have no way to insure the code will
not malfunction.
| Code: |
#!/bin/env python
# Read temperature from inkbird IDT-34c-B
# Written November, December, 2025 by Andrew Robinson of Scappoose.
# Version 0.99.10 (barely stable alpha code).
# This code is released under the GNU public license, 3.0
# https://www.gnu.org/licenses/gpl-3.0.en.html
#
# Pre-Requisites virtual python, and dasbus linrary.
# Virtual python is required on most systems with system package installs of python.
# It allows a local user (not superuser) to install python packages without wrecking
# the operating system's version of python.
#
# python3 -m venv --system-site-packages py_envs
# pip3 install dasbus
# Note, the inkbird protocol is proprietary and may change.
# To list your active bluetooth devices and services do:
# busctl tree org.bluez
# busctl introspect "org.bluez" "/org/bluez/hci0/dev_xx_xx_xx_xx_xx_xx"
# To log bluetooth bus activity for wireshark analysis:
# sudo btmon -w > rpi.log
import time
import threading
import signal
import os
from dasbus.connection import SystemMessageBus
from dasbus.loop import EventLoop
from dasbus.typing import Variant
from dasbus.signal import Signal
# Characteristic properties bitmask
# [Extended][Auth_Sign][Indicate][Notify] [Write][Write-NoResp][Read][Broadcast]
MAXTEMP = 1802.5
WATCHTIME = 90.1
INKBIRD_NAME='IDT-34c-B'
FRIENDLY_NAME='INKBIRD'
ADAPTER_PATH = "/org/bluez/hci0"
SERVICE_NAME = "org.bluez"
PROP_IFACE="org.freedesktop.DBus.Properties"
DEVICE_IFACE="org.bluez.Device1"
ADAPTER_IFACE="org.bluez.Adapter1"
GATT_SERVICE_IFACE="org.bluez.GattService1"
GATT_CHAR_IFACE="org.bluez.GattCharacteristic1"
GATT_DESC_IFACE="org.bluez.GattDescriptor1"
TEMPERATURE_UUID="0000ff00-0000-1000-8000-00805f9b34fb"
bus=SystemMessageBus()
loop=EventLoop()
adapter = bus.get_proxy( SERVICE_NAME, ADAPTER_PATH )
manager = bus.get_proxy( SERVICE_NAME, "/" )
fout = open( "/tmp/thermal.log", 'w' )
thermostamp=[ float('NaN') ]*24
thermofilter=[ 0. ]*24
thermocount=[ 0 ]*24
stamp = False
allocated_offsets={}
free_offsets={ 0:0, 4:4, 8:8, 12:12, 16:16, 20:20 }
inkbirds={}
gatt_services={}
commands={}
temperatures={}
batteries={}
bind={}
last_temp=[]
def signal_handler( signum, frame ):
for path in inkbirds:
try:
inkbirds[path].Disconnect()
except:
pass
if (not loop is None):
loop.quit()
exit(2)
def deallocate(obj_path):
if obj_path in allocated_offsets:
free_offsets[ obj_path ] = allocated_offsets[ obj_path ]
del allocated_offsets[ obj_path ]
def allocate(obj_path):
offset = 1e9
best = None
if obj_path in free_offsets:
best,offset = obj_path, free_offsets[ obj_path ]
else:
for key in free_offsets:
if offset > free_offsets[key]:
best,offset = key,free_offsets[key]
allocated_offsets[obj_path] = offset
del free_offsets[best]
def reinitialize_inkbird( obj_path ):
generic_init=[
Variant( 'ay', [0x02,0x01,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x02,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x04,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x08,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x04,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0446
Variant( 'ay', [0x06,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0663
Variant( 'ay', [0x08] ), # 0x080f00
Variant( 'ay', [0x0a,0x0f,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x0c,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0c5a
Variant( 'ay', [0x0f,0x00,0x00,0x00,0x00,0x00,0x00] ), # *Hash returned,varies.
Variant( 'ay', [0x11,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x111100
Variant( 'ay', [0x13,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x13fe
Variant( 'ay', [0x18]), # self +0x000000000000
Variant( 'ay', [0x24]), # self +0x0f0000000000000000 *droppable
Variant( 'ay', [0x26,0x01]), # self +0x0h000000000000000 *droppable
Variant( 'ay', [0x26,0x02]), # self +0x0000000000000000 *droppable
Variant( 'ay', [0x26,0x04]), # self +0x0000000000000000 *droppable
Variant( 'ay', [0x26,0x08]), # self +0x0000000000000000
]
print("re-initializing ",obj_path )
for i in generic_init:
commands[ obj_path ].WriteValue( i, { 'type':Variant('s','request') } )
def print_battery( data ):
print( "battery=",data[0],"%" )
def update_temperatures( obj_path, data ):
global stamp
def temperature( lsbyte, msbyte ):
value = ((msbyte^0x80)<<8)+lsbyte - 0x8000
return (value-320)/18 # Convert to celsius
t4vec = [ temperature( *data[2*i:2*i+2] ) for i in range(0,4) ]
offset = allocated_offsets[ obj_path ]
for i,value in enumerate( t4vec ):
vlast = thermostamp[offset+i]
if ((thermocount[offset+i] < 100) and
(value == vlast or value==thermofilter[offset+i])):
continue
thermocount[offset+i]=0
if abs( value-vlast )>1.5 :
if (value>MAXTEMP) or thermostamp[offset+i]>MAXTEMP:
thermostamp[ offset+i ] = value
else:
thermostamp[ offset+i ] = (value+thermostamp[offset+i])/2.
thermofilter[ offset+i ] = thermostamp[ offset+i ]
continue
thermofilter[offset+i]=thermostamp[offset+i]
thermostamp[offset+i]=value
stamp = True
def temperature_callback( obj_path, obj_iface, obj_dict, invalidated ):
if ( "Value" in obj_dict and obj_path in allocated_offsets):
update_temperatures( obj_path, obj_dict['Value'].unpack() )
return True
def command_callback( obj_path, obj_iface, obj_dict, invalidated ):
print( "Command notify\t\t", obj_path,invalidated )
if ( "Value" in obj_dict ):
Value = obj_dict['Value'].unpack()
print( "Value=",Value )
return True
def extra_callback( obj_path, obj_iface, obj_dict, invalidated ):
print( "Extra notify\t\t", obj_path,invalidated )
if ( "Value" in obj_dict ):
Value=obj_dict['Value'].unpack()
print( "Value=",Value )
if len(Value)==7:
if (Value[5]&1) and not ( obj_path in allocated_offsets):
print("Pairing complete")
allocate(obj_path)
inkbirds[obj_path].Trusted=True
reinitialize_inkbird(obj_path)
return True
def battery_callback( obj_path, obj_iface, obj_dict, invalidated ):
print("Battery notify\t\t", obj_path, invalidated )
if ( "Value" in obj_dict ):
print_battery( obj_dict['Value'].unpack() )
return True
def bind_notify( proxy, callback, o_path ):
proxy.PropertiesChanged.connect(
lambda o_iface,o_dict,o_inval:callback(o_path,o_iface,o_dict,o_inval)
)
proxy.StartNotify()
def services_resolved_callback( obj_path, obj_iface, obj_dict, invalidated ):
if not 'ServicesResolved' in obj_dict:
return False
if obj_dict['ServicesResolved'].unpack()==True:
print( "ServicesResolved." )
for path in gatt_services:
if path.startswith( obj_path ):
if gatt_services[path]==False and len(bind[obj_path])<6:
print("Service has wrong size. Disconnecting:",obj_path)
inkbird[obj_path].Disconnect()
return True
gatt_services[path]=True
n=0
try:
for n,i in enumerate( bind[obj_path] ):
bind_notify(*i)
except Exception as e:
print( f"Binding failed on {i} object: {e}" )
del bind[0:n]
inkbirds[obj_path].Disconnect()
return True
print("Pairing")
try:
commands[ obj_path ].WriteValue(
Variant('ay',[0xfd,0x00,0x00,0x00,0x00,0x00,0x00]), { 'type':Variant('s','request') }
)
except Exception as e:
print( f"Pairing failed:{e}" )
pass
return True
print("Services Delete",obj_path)
deallocate(obj_path)
return True
def interface_added_callback( obj_path, obj_dict ):
if DEVICE_IFACE in obj_dict:
try:
properties = obj_dict[DEVICE_IFACE]
name = properties.get('Name').unpack()
if ( name!=INKBIRD_NAME and name!=FRIENDLY_NAME ): return False
except:
return True # Ignore corrupt device data quirk.
print("inkbird ",obj_path)
try:
new_inkbird=inkbirds[obj_path]
except:
if len(free_offsets)==0:
print("Inkbird script has insufficient thermometer memory")
return False
new_inkbird=bus.get_proxy(SERVICE_NAME, obj_path)
print("new-inkbird proxy")
else: # known inkbirds, test for stall
try:
if new_inkbird.ServicesResolved==True:
print("Check for stall in ",obj_path)
offset = allocated_offsets[ obj_path ]
for i in range(0,4):
if thermocount[ offset+i ]>120:
offset = True
break
if (offset is True):
reinitialize_inkbird( obj_path )
return True # All good connections exit from here.
except:
raise
pass
# Either the connection is new or it is corrupted.
if new_inkbird.Connected:
print( "Corrupted connection state:",obj_path, name )
deallocate( obj_path )
try:
new_inkbird.Disconnect()
except: # FIXME: not sure the following is allowed.
new_inkbird.Connected=False
new_inkbird.ServicesResolved=False
return False # Something's wrong, see if time resolves it.
# new device connection.
print( "Connecting inkbird device ",obj_path )
if not obj_path in inkbirds:
inkbirds[ obj_path ]=new_inkbird
new_inkbird.PropertiesChanged.connect(
lambda a,b,c : services_resolved_callback( obj_path, a,b,c )
)
new_inkbird.Connect()
return True
parent_path = os.path.dirname(obj_path)
if GATT_SERVICE_IFACE in obj_dict:
if obj_path in gatt_services: return True
properties=obj_dict[GATT_SERVICE_IFACE]
if properties["UUID"].unpack()==TEMPERATURE_UUID:
if properties["Device"].unpack() in inkbirds:
gatt_services[ obj_path ]=False
bind[parent_path]=[]
print( "gatt service ", obj_path )
else:
print(" Error",properties['Device'].unpack())
return True
if GATT_CHAR_IFACE in obj_dict:
if parent_path in gatt_services:
if gatt_services[parent_path]: return True # Proxies are already bound
uuid = obj_dict[GATT_CHAR_IFACE]["UUID"].unpack()
proxy = bus.get_proxy( SERVICE_NAME, obj_path )
dev_path = os.path.dirname(parent_path)
if "0000ff01-0000-1000-8000-00805f9b34fb"==uuid:
temperatures[dev_path]=proxy
bind[dev_path].append((proxy, temperature_callback, dev_path ))
return
if "0000ff02-0000-1000-8000-00805f9b34fb"==uuid:
commands[dev_path]=proxy
bind[dev_path].append((proxy, command_callback, dev_path ))
return
if uuid.startswith("0000ff"):
if uuid.startswith("0000ff05"):
return
bind[dev_path].append((proxy, extra_callback, dev_path ))
return
if uuid=="00002a19-0000-1000-8000-00805f9b34fb":
batteries[dev_path]=proxy
bind[dev_path].append((proxy, battery_callback, dev_path ))
return
return True
return False
def logger():
global stamp
if (stamp==True):
sample = enumerate( list(thermostamp) )
stamp=False
for i in range( 0,len(thermocount) ):
thermocount[i]+=1
print( "%6.2f "%(time.time()), end="", file=fout )
for i,value in sample:
print( "% 6.1f "%( value if value<MAXTEMP else float('NaN') ), end="", file=fout )
print( " [°C] ",file=fout )
fout.flush()
(threading.Timer( 1, logger )).start()
def scan_dbus():
print("Scan dbus")
for obj_path, obj_dict in (manager.GetManagedObjects()).items():
interface_added_callback( obj_path, obj_dict )
(threading.Timer( WATCHTIME, scan_dbus )).start()
return
#
# ------------------- Main logic proceedure begins here --------------------------
#
try:
signal.signal(signal.SIGINT, signal_handler )
signal.signal(signal.SIGTERM, signal_handler )
manager.InterfacesAdded.connect( interface_added_callback )
(threading.Timer( 1, scan_dbus )).start()
(threading.Timer( 1, logger )).start()
loop.run()
except Exception as e:
print(f"Main loop exception {e}")
raise
|
|
|
|
semiconductive
Hazard to Others
 
Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline
Mood: Explorative
|
|
Update notes for version 0.99.11
→ I've changed how the 'pseudo-pairing' works. It no longer waits for a second push of the button before sending initialization codes. The new
python script assumes the inkbird device is stable whenever the first temperature reading is sent from the inkbird to the python script. This may/or
not be a good idea. (Experimental).
→ I tried to clean up how the device closes out connections and fixed an obvious object delete bug. But: I discovered another bug. When multiple
inkbird devices are paired, re-connecting without breaking the script does not always work. Resource deadlock is a problem. I suspect it's over
signal handlers or object proxy references.
→ I modified the logic deciding when to log a changed temperatures to prevent queue stalling for long periods of time. This script is better, but
It's still not great.
→ When obviously corrupt temperature data appears, I simply reject it now; but that could mean that significant temperature changes get delayed
unpredictably before being stamped into the log. ( See previous comment. )
| Code: |
#!/bin/env python
# Read temperature from inkbird IDT-34c-B
# Written November, 2025 - January, 2026 by Andrew Robinson of Scappoose.
# Version 0.99.11 (unstable alpha code).
# This code is released under the GNU public license, 3.0
# https://www.gnu.org/licenses/gpl-3.0.en.html
#
# Pre-Requisites virtual python, and dasbus library.
# Virtual python is required on most systems with system package installs of python.
# V.P. allows a local user (not superuser) to install python packages without wrecking
# the operating system's version of python.
#
# python3 -m venv --system-site-packages py_envs
# pip3 install dasbus
# Note, the inkbird protocol is proprietary and may change.
# To list your active bluetooth devices and services do:
# busctl tree org.bluez
# busctl introspect "org.bluez" "/org/bluez/hci0/dev_xx_xx_xx_xx_xx_xx"
# To log bluetooth bus activity for wireshark analysis:
# sudo btmon > rpi.log
# Scan
import time
import threading
import signal
import os
from dasbus.connection import SystemMessageBus
from dasbus.loop import EventLoop
from dasbus.typing import Variant
from dasbus.signal import Signal
# Characteristic properties bitmask
# [Extended][Auth_Sign][Indicate][Notify] [Write][Write-NoResp][Read][Broadcast]
MAXTEMP = 1802.5
WATCHTIME = 90.1
INKBIRD_NAME='IDT-34c-B'
FRIENDLY_NAME='INKBIRD'
ADAPTER_PATH = "/org/bluez/hci0"
SERVICE_NAME = "org.bluez"
PROP_IFACE="org.freedesktop.DBus.Properties"
DEVICE_IFACE="org.bluez.Device1"
ADAPTER_IFACE="org.bluez.Adapter1"
GATT_SERVICE_IFACE="org.bluez.GattService1"
GATT_CHAR_IFACE="org.bluez.GattCharacteristic1"
GATT_DESC_IFACE="org.bluez.GattDescriptor1"
TEMPERATURE_UUID="0000ff00-0000-1000-8000-00805f9b34fb"
bus=SystemMessageBus()
loop=EventLoop()
adapter = bus.get_proxy( SERVICE_NAME, ADAPTER_PATH )
manager = bus.get_proxy( SERVICE_NAME, "/" )
MAXWAIT=20 # Maximum stamping before termperature is not considered redundant.
fout = open( "/tmp/thermal.dat", 'w' )
thermostamp=[ float('NaN') ]*24
thermofilter=[ 0. ]*24
thermocount=[ 0 ]*24 # How many times has temperature already been stamped to the log file. (Redundancy limiter).
stamp = False
laststamp = time.time()
allocated_offsets={}
free_offsets={ 0:0, 4:4, 8:8, 12:12, 16:16, 20:20 }
inkbirds={}
gatt_services={}
commands={}
temperatures={}
batteries={}
bind={}
last_temp=[]
def signal_handler( signum, frame ):
for path in inkbirds:
try:
inkbirds[path].Disconnect()
except:
pass
if (not loop is None):
loop.quit()
exit(2)
def deallocate(obj_path):
if obj_path in allocated_offsets:
free_offsets[ obj_path ] = allocated_offsets[ obj_path ]
del allocated_offsets[ obj_path ]
def allocate(obj_path):
offset = 1e9
best = None
if obj_path in free_offsets:
best,offset = obj_path, free_offsets[ obj_path ]
else:
for key in free_offsets:
if offset > free_offsets[key]:
best,offset = key,free_offsets[key]
allocated_offsets[obj_path] = offset
del free_offsets[best]
def reinitialize_inkbird( obj_path ):
generic_init=[
Variant( 'ay', [0x02,0x01,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x02,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x04,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x02,0x08,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x04,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0446
Variant( 'ay', [0x06,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0663
Variant( 'ay', [0x08] ), # 0x080f00
Variant( 'ay', [0x0a,0x0f,0x00,0x00,0x00,0x00,0x00] ), # self +0x0000
Variant( 'ay', [0x0c,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x0c5a
Variant( 'ay', [0x0f,0x00,0x00,0x00,0x00,0x00,0x00] ), # *Hash returned,varies.
Variant( 'ay', [0x11,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x111100
Variant( 'ay', [0x13,0x00,0x00,0x00,0x00,0x00,0x00] ), # 0x13fe
Variant( 'ay', [0x18]), # self +0x000000000000
Variant( 'ay', [0x24]), # self +0x0f0000000000000000 *droppable
Variant( 'ay', [0x26,0x01]), # self +0x0h000000000000000 *droppable
Variant( 'ay', [0x26,0x02]), # self +0x0000000000000000 *droppable
Variant( 'ay', [0x26,0x04]), # self +0x0000000000000000 *droppable
Variant( 'ay', [0x26,0x08]), # self +0x0000000000000000
]
print("re-initializing ",obj_path )
for i in generic_init:
commands[ obj_path ].WriteValue( i, { 'type':Variant('s','request') } )
def print_battery( data ):
print( "battery=",data[0],"%" )
def update_temperatures( obj_path, data ):
global stamp
def temperature( lsbyte, msbyte ):
value = ((msbyte^0x80)<<8)+lsbyte - 0x8000
return (value-320)/18 # Convert to celsius
if data[8:12] != [0xFE,0x7F,0xFE,0x7F]:
print( "Suspicious temperature packet", data )
return # Do not process questionable packets.
t4vec = [ temperature( *data[2*i:2*i+2] ) for i in range(0,4) ]
offset = allocated_offsets[ obj_path ]
for i,value in enumerate( t4vec ):
vlast = thermostamp[offset+i]
redundant = thermocount[offset+i]
if ( redundant and (redundant<MAXWAIT) and
(value == vlast or value==thermofilter[offset+i]) ):
continue
if abs( value-vlast )>1.5 :
if (value>MAXTEMP) or thermostamp[offset+i]>MAXTEMP:
thermostamp[ offset+i ] = value
else:
thermostamp[ offset+i ] = (value+thermostamp[offset+i])/2.
thermofilter[ offset+i ] = thermostamp[ offset+i ]
continue
thermofilter[offset+i]=thermostamp[offset+i]
thermostamp[offset+i]=value
if (stamp==False) and abs( lastvalue ):
thermocount[offset+i]=0
stamp = True
def temperature_callback( obj_path, obj_iface, obj_dict, invalidated ):
if ( "Value" in obj_dict ):
if not (obj_path in allocated_offsets ):
if inkbirds[obj_path].Connected == True:
allocate(obj_path)
inkbirds[obj_path].Trusted=True
else:
print( "Temperature notify for disconnected inkbird:", obj_path, allocated_offsets )
return False
update_temperatures( obj_path, obj_dict['Value'].unpack() )
return True
def command_callback( obj_path, obj_iface, obj_dict, invalidated ):
print( "Command notify\t\t", obj_path,invalidated )
if ( "Value" in obj_dict ):
Value = obj_dict['Value'].unpack()
print( "Value=",Value )
return True
def extra_callback( obj_path, obj_iface, obj_dict, invalidated ):
print( "Extra notify\t\t", obj_path,obj_dict )
if ( "Value" in obj_dict ):
Value=obj_dict['Value'].unpack()
print( "Value=",Value )
return True
def battery_callback( obj_path, obj_iface, obj_dict, invalidated ):
print("Battery notify\t\t", obj_path, invalidated )
if ( "Value" in obj_dict ):
print_battery( obj_dict['Value'].unpack() )
return True
def bind_notify( proxy, callback, o_path ):
proxy.PropertiesChanged.connect(
lambda o_iface,o_dict,o_inval:callback(o_path,o_iface,o_dict,o_inval)
)
proxy.StartNotify()
def services_resolved_callback( obj_path, obj_iface, obj_dict, invalidated ):
if not 'ServicesResolved' in obj_dict:
return False
if obj_dict['ServicesResolved'].unpack()==True:
print( "ServicesResolved." )
for path in gatt_services:
if path.startswith( obj_path ):
if gatt_services[path]==False and len(bind[obj_path])<6:
print("Service has wrong size. Disconnecting:",obj_path)
inkbird[obj_path].Disconnect()
return True
gatt_services[path]=True
n=0
try:
for n,i in enumerate( bind[obj_path] ):
bind_notify(*i)
except Exception as e:
print( f"Binding failed on {i} object: {e}" )
bind[obj_path]=[]
inkbirds[obj_path].Disconnect()
return True
print("Pseudo Pairing")
try:
commands[ obj_path ].WriteValue(
Variant('ay',[0xfd,0x00,0x00,0x00,0x00,0x00,0x00]), { 'type':Variant('s','request') }
)
except Exception as e:
print( f"Pseudo Pairing failed:{e}" )
pass
return True
print("Services Delete",obj_path)
deallocate(obj_path)
return True
def interface_added_callback( obj_path, obj_dict ):
if DEVICE_IFACE in obj_dict:
try:
properties = obj_dict[DEVICE_IFACE]
name = properties.get('Name').unpack()
if ( name!=INKBIRD_NAME and name!=FRIENDLY_NAME ): return False
except:
print("Ignoring unstable interface in memory")
return True
print("inkbird ",obj_path)
try:
new_inkbird=inkbirds[obj_path]
except:
if len(free_offsets)==0:
print("Inkbird script has insufficient thermometer memory")
return False
new_inkbird=bus.get_proxy(SERVICE_NAME, obj_path)
print("new-inkbird proxy")
else:
print(" Already known")
return True # All good connections exit from here.
# Either the connection is new or it is corrupted.
if new_inkbird.Connected:
print( "Corrupted connection state:",obj_path, name )
deallocate( obj_path )
try:
new_inkbird.Disconnect()
except: # FIXME: not sure the following is allowed.
new_inkbird.Connected=False
new_inkbird.ServicesResolved=False
return False # Something's wrong, see if time resolves it.
# new device connection.
print( "Connecting inkbird device ",obj_path )
if not obj_path in inkbirds:
inkbirds[ obj_path ]=new_inkbird
new_inkbird.PropertiesChanged.connect(
lambda a,b,c : services_resolved_callback( obj_path, a,b,c )
)
new_inkbird.Connect()
return True
parent_path = os.path.dirname(obj_path)
if GATT_SERVICE_IFACE in obj_dict:
if obj_path in gatt_services: return True
properties=obj_dict[GATT_SERVICE_IFACE]
if properties["UUID"].unpack()==TEMPERATURE_UUID:
if properties["Device"].unpack() in inkbirds:
gatt_services[ obj_path ]=False
bind[parent_path]=[]
print( "gatt service ", obj_path )
else:
print(" Error",properties['Device'].unpack())
return True
if GATT_CHAR_IFACE in obj_dict:
if parent_path in gatt_services:
if gatt_services[parent_path]: return True # Proxies are already bound
uuid = obj_dict[GATT_CHAR_IFACE]["UUID"].unpack()
proxy = bus.get_proxy( SERVICE_NAME, obj_path )
dev_path = os.path.dirname(parent_path)
if "0000ff01-0000-1000-8000-00805f9b34fb"==uuid:
temperatures[dev_path]=proxy
bind[dev_path].append((proxy, temperature_callback, dev_path ))
return
if "0000ff02-0000-1000-8000-00805f9b34fb"==uuid:
commands[dev_path]=proxy
bind[dev_path].append((proxy, command_callback, dev_path ))
return
if uuid.startswith("0000ff"):
if uuid.startswith("0000ff05"):
return
bind[dev_path].append((proxy, extra_callback, dev_path ))
return
if uuid=="00002a19-0000-1000-8000-00805f9b34fb":
batteries[dev_path]=proxy
bind[dev_path].append((proxy, battery_callback, dev_path ))
return
return True
return False
def logger():
global stamp,laststamp
if (stamp==False and (time.time()-laststamp)>30):
print("logger stalled, attempting to clear.")
for services in gatt_services:
if gatt_services[services] is True:
obj_path = os.path.dirname( services )
print("Unstalling ",obj_path)
temperatures[ obj_path ].ReadValue({ 'type':Variant('s','request') })
laststamp = time.time()
if (stamp==True):
t=time.time()
sample = enumerate( list(thermostamp) )
stamp,laststamp=False,t
for i in range( 0,len(thermocount) ):
n=thermocount[i]
thermocount[i] = n+1 if n<MAXWAIT else 0
print( "%6.2f "%(t), end="", file=fout )
for i,value in sample:
print( "% 6.1f "%( value if value<MAXTEMP else float('NaN') ), end="", file=fout )
print( " [°C] ",file=fout )
fout.flush()
(threading.Timer( 1, logger )).start()
def scan_dbus():
print("Scan dbus")
for obj_path, obj_dict in (manager.GetManagedObjects()).items():
interface_added_callback( obj_path, obj_dict )
(threading.Timer( WATCHTIME, scan_dbus )).start()
return
#
# ------------------- Main logic proceedure begins here --------------------------
#
try:
signal.signal(signal.SIGINT, signal_handler )
signal.signal(signal.SIGTERM, signal_handler )
manager.InterfacesAdded.connect( interface_added_callback )
(threading.Timer( 1, scan_dbus )).start()
(threading.Timer( 1, logger )).start()
loop.run()
except Exception as e:
print(f"Main loop exception {e}")
raise
|
|
|
|
|