Sciencemadness Discussion Board
Not logged in [Login ]
Go To Bottom

Printable Version  
Author: Subject: Inkbird IDT-34c-B temperature probe to Raspberry PI
semiconductive
Hazard to Others
***




Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline

Mood: Explorative

[*] posted on 11-11-2025 at 20:37
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]
View user's profile View All Posts By User
semiconductive
Hazard to Others
***




Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline

Mood: Explorative

[*] posted on 11-11-2025 at 21:46


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]
View user's profile View All Posts By User
semiconductive
Hazard to Others
***




Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline

Mood: Explorative

[*] posted on 17-11-2025 at 19:15


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:
Code:
bluetoothd -v 5.82


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]
View user's profile View All Posts By User
semiconductive
Hazard to Others
***




Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline

Mood: Explorative

[*] posted on 21-11-2025 at 12:44


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.
View user's profile View All Posts By User
semiconductive
Hazard to Others
***




Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline

Mood: Explorative

[*] posted on 27-11-2025 at 21:23


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.

View user's profile View All Posts By User
semiconductive
Hazard to Others
***




Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline

Mood: Explorative

[*] posted on 28-11-2025 at 20:19


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.





View user's profile View All Posts By User
semiconductive
Hazard to Others
***




Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline

Mood: Explorative

[*] posted on 10-12-2025 at 12:16


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

View user's profile View All Posts By User
semiconductive
Hazard to Others
***




Posts: 400
Registered: 12-2-2017
Location: Scappoose Oregon, USA.
Member Is Offline

Mood: Explorative

[*] posted on 26-1-2026 at 20:11


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



View user's profile View All Posts By User

  Go To Top