Skip to content

Bluetooth Debug Client Tools (`tools/bt_client/`)

Bluetooth Debug Client Tools (tools/bt_client/)

Section titled “Bluetooth Debug Client Tools (tools/bt_client/)”

The tools_communication_clients_bt_clients module provides three lightweight, developer-side desktop GUI tools for testing and debugging the pendant’s Bluetooth communication transports directly from a host PC. Each tool mirrors a specific CNC firmware × transport technology combination, allowing developers to simulate the pendant connection, inspect raw traffic, and replay firmware-specific initialisation sequences — without flashing or rebuilding the pendant firmware.

Tool fileTransportFirmware targetDependency
fluidnc_ble.pyBLE (GATT)FluidNCbleak
fluidnc_bt.pyBT Classic SPPFluidNCpyserial
grblhal_bt.pyBT Classic SPPgrblHALpyserial

All three tools share a common design pattern: a Tkinter GUI on the main thread, a background receive thread (or dedicated asyncio loop thread for BLE), and colour-coded terminal logging.

Scope — these are host-side development utilities only. They do not run on the ESP32. For the on-device transport implementations see tools_communication_clients.md and the firmware modules documented in Communication_Transports.md.


Pendant Firmware - ESP32

Radio / OS

Transport Library

Host PC - tools/bt_client/

fluidnc_ble.py

FluidNcBle

fluidnc_bt.py

FluidNcBt

grblhal_bt.py

GrblHalBt

bleak

BLE GATT client

pyserial

Serial port

pyserial

Serial port

Bluetooth Radio

(BLE advertisement + GATT)

OS COM Port

(BT Classic SPP pairing)

esp3d_bt_ble_client

BLE GATT server

service 0xFFF0

esp3d_bt_serial_client

SPP server

Design Principles Shared by All Three Tools

Section titled “Design Principles Shared by All Three Tools”
  • Single-window Tkinter GUI — no external UI framework required; runs anywhere Python 3 is installed.
  • Non-blocking I/O — receive work runs in a daemon thread (SPP tools) or in a dedicated asyncio event loop thread (BLE tool); the Tkinter main thread is never blocked.
  • Colour-coded log — cyan TX, green RX, dim grey for pendant internal log lines starting with [, yellow info, red errors.
  • Timestamped entries — every log line is prefixed with HH:MM:SS.
  • CLI pre-seeding — all tools accept optional positional arguments to pre-fill connection fields so they can be launched straight into a known configuration.
  • Graceful dependency check — missing Python packages are caught at import time and displayed as a native dialog before exit; no cryptic tracebacks for the user.

1. FluidNcBle — BLE GATT Client for FluidNC (fluidnc_ble.py)

Section titled “1. FluidNcBle — BLE GATT Client for FluidNC (fluidnc_ble.py)”

Connects to the pendant’s BLE transport (ESP3DBTBleClient) and exercises the full FluidNC startup flow over BLE. Useful for verifying GATT notification delivery, write-without-response chunking, and service discovery from a PC.

GATT Service 0xFFF0

write_gatt_char

no response, 20-byte chunks

BLE notify callback

_on_notify

RX Characteristic 0xFFF1

Notify - CNC to PC

TX Characteristic 0xFFF2

Write - PC to CNC

Host PC

FluidNcBle

UUIDDirectionGATT propertyDescription
0000fff0-0000-1000-8000-00805f9b34fb—Primary ServiceFluidNC BLE service
0000fff1-0000-1000-8000-00805f9b34fbCNC → PCNotifyRX — pendant sends CNC responses
0000fff2-0000-1000-8000-00805f9b34fbPC → CNCWrite (no-resp)TX — host sends G-code / commands
00002a00-0000-1000-8000-00805f9b34fbscan onlyReadGeneric Access / Device Name (name resolution during scan)
BLE Radio / PendantBLE asyncio Loop ThreadTkinter Main ThreadBLE Radio / PendantBLE asyncio Loop ThreadTkinter Main Thread_schedule(coro) via run_coroutine_threadsafeBleakClient.connect()connectedstart_notify(RX_CHAR_UUID, _on_notify)root.after(0, _on_connected, addr)notification (CNC data)root.after(0, _log, "RX: ...", tag)_schedule(_do_write(data))write_gatt_char(TX_CHAR_UUID, chunk 0-20 bytes)write_gatt_char(TX_CHAR_UUID, chunk 20-40 bytes)
  • The BLE asyncio loop runs permanently in a daemon thread (_ble_thread), started once at construction.
  • All BLE coroutines are submitted from the UI thread via _schedule() → asyncio.run_coroutine_threadsafe.
  • All UI updates from the BLE thread use root.after(0, …) to marshal back to the Tkinter thread safely.

BLE write-without-response is bounded by the negotiated ATT MTU. FluidNcBle uses a conservative 20-byte default (the minimum guaranteed payload by the BLE spec) and splits every write:

mtu = 20 # safe BLE default
for i in range(0, len(data), mtu):
chunk = data[i: i + mtu]
await self._client.write_gatt_char(TX_CHAR_UUID, chunk, response=False)

Yes

No

User clicks Scan

BleakScanner.discover

return_adv=True - 8 s timeout

Devices with

no name?

_query_device_name

brief GATT connect

read 0x2A00 - 3 s timeout per device

Merge resolved names

Sort:

1. has service 0xFFF0 marked star

2. RSSI descending

Populate Combobox

Pre-select first starred device

MethodDescription
_start_scan / _do_scanPassive+active BLE scan, name resolution, service filtering
_query_device_nameBriefly connects and reads GATT Device Name (0x2A00) for unnamed devices
_toggle_connect / _do_connectConnect with disconnected_callback; starts notify on RX char
_do_disconnectGraceful disconnect; clears _client reference
_on_notifyBLE notification handler; accumulates bytes in _rx_buf, flushes on \n
_do_writeAsync write with 20-byte MTU chunking
_send_rawSends pre-built bytes (used for real-time single-byte commands)
_send_text_directSends a named UTF-8 text command appended with \n
_send_textReads the command entry field and sends as text + \n
_send_full_sequenceReplays INIT_COMMANDS in a daemon thread with 300 ms gaps
_send_hexParses space-separated hex tokens and calls _send_raw

2. FluidNcBt — BT Classic SPP Client for FluidNC (fluidnc_bt.py)

Section titled “2. FluidNcBt — BT Classic SPP Client for FluidNC (fluidnc_bt.py)”

Connects to the pendant’s BT Serial transport (ESP3DBTSerialClient) via the OS COM port created when a Classic Bluetooth SPP device is paired. Reproduces the pendant startup sequence to validate the full FluidNC handshake over SPP.

serial.Serialrecv_thread daemonTkinter Main Threadserial.Serialrecv_thread daemonTkinter Main Threadloop[Receive loop]serial.Serial(port, baud, timeout=1)threading.Thread(_recv_loop).start()ser.read(256) blocking 1 s timeoutbytes from pendantroot.after(0, _log, "RX: ...", tag)ser.write(data) - UI thread, small non-blocking write
  • _recv_loop runs in a daemon thread; reads up to 256 bytes at a time, line-buffers on \n, dispatches to the UI via root.after.
  • All writes are synchronous on the UI thread. Serial writes are small (single command lines) and do not block the UI perceptibly.

The tool replicates the exact command sequence sent by the pendant on startup:

OrderCommandPurpose
1$IIdentify firmware build info
2$Report/Interval=100Set auto-report interval to 100 ms
3$planner_blocksQuery planner buffer size
4$10=3Enable full status reports

The ▶ Full sequence button runs these with 200 ms inter-command gaps in a daemon thread to avoid blocking the UI.

MethodDescription
_refresh_portsEnumerates available serial ports via serial.tools.list_ports
_connectOpens serial.Serial, starts _recv_loop thread
_disconnectStops receive loop, closes port, resets UI state
_recv_loopDaemon thread: reads, line-buffers, dispatches to UI
_send_rawWrites raw bytes (used for real-time single-byte commands)
_send_text_directWrites a named UTF-8 text command + \n
_send_textReads the command entry field, sends as text + \n
_send_hexParses space-separated hex tokens, calls _send_raw
_send_full_sequenceReplays INIT_COMMANDS in a daemon thread with 200 ms gaps

3. GrblHalBt — BT Classic SPP Client for grblHAL (grblhal_bt.py)

Section titled “3. GrblHalBt — BT Classic SPP Client for grblHAL (grblhal_bt.py)”

Connects to the pendant’s BT Serial transport when the pendant targets grblHAL firmware. grblHAL uses additional single-byte real-time commands beyond the standard grbl set — notably MPG and Auto-Report mode toggles. This tool exposes those grblHAL-specific commands directly via dedicated buttons.

Button labelByte valuegrblHAL meaning
0x87 — Status report all\x87Extended status report (all fields)
0x8B — MPG toggle\x8BToggle MPG (Manual Pulse Generator) mode
0x8C — AR toggle\x8CToggle Auto-Report mode
0x18 — Soft reset\x18Soft reset (same as standard grbl)
! — Feed hold!Feed hold
~ — Cycle start~Cycle start / resume
0x85 — Jog cancel\x85Cancel jog
? — Status query?Inline status query

Note: 0x8B (MPG toggle) is particularly important for pendant workflows. grblHAL requires the MPG token to be claimed before accepting jog commands from an external pendant. This button lets developers confirm the token handshake without a physical pendant present.

Identical to FluidNcBt: a daemon receive thread reads from the serial port and dispatches lines to the UI thread via root.after. All writes are synchronous on the UI thread.

MethodDescription
_refresh_portsEnumerates COM ports via serial.tools.list_ports
_connect / _disconnectOpen/close serial.Serial, manage receive thread
_recv_loopDaemon thread: reads, line-buffers, dispatches (all RX displayed as lime — no dim-log distinction)
_send_rawWrites raw bytes with hex-formatted log line
_send_textReads entry field, sends as UTF-8 text + \n
_send_hexParses space-separated hex tokens, calls _send_raw

Featurefluidnc_ble.pyfluidnc_bt.pygrblhal_bt.py
Tkinter GUI✓✓✓
Colour-coded timestamped log✓✓✓
Manual text command entry✓✓✓
Hex byte sender✓✓✓
Single-byte real-time commands✓✓✓
Clear log button✓✓✓
CLI argument pre-seeding✓✓✓
Missing-dependency dialog✓✓✓
BLE device scanner (RSSI + service filter)✓——
GATT device name resolution (0x2A00)✓——
asyncio event loop thread✓——
MTU-aware 20-byte write chunking✓——
Notify subscription on RX characteristic✓——
COM port enumeration—✓✓
Baud rate selector (9600–921600)—✓✓
FluidNC init sequence replay✓✓—
Dim log style for pendant […] lines✓✓—
grblHAL MPG / AR toggle buttons——✓

ESP32 Pendant BLE GATT ServerBLE asyncio ThreadTkinter UIUserESP32 Pendant BLE GATT ServerBLE asyncio ThreadTkinter UIUserClick Scan_schedule(_do_scan)BleakScanner.discover 8 sAdvertisement - service 0xFFF0_on_scan_done - Combobox starred device pre-selectedClick Connect_schedule(_do_connect(addr))BleakClient.connect()Connectedstart_notify(0xFFF1, _on_notify)_on_connected - status "Connected" limeClick "? Status query"_schedule(_do_write(b"?"))write_gatt_char(0xFFF2, b"?")notify(0xFFF1, b"<Idle|MPos:...>\n")root.after(0, _log, "RX: <Idle|...>", "rx")Lime line in log

SPP Clients (fluidnc_bt.py / grblhal_bt.py)

Section titled “SPP Clients (fluidnc_bt.py / grblhal_bt.py)”
ESP32 Pendant BT SPP Serverserial.Serialrecv_threadTkinter UIUserESP32 Pendant BT SPP Serverserial.Serialrecv_threadTkinter UIUserloop[Receive loop]loop[INIT_COMMANDS with 200 ms gap]Click Connect - COM5 at 115200serial.Serial("COM5", 115200, timeout=1)start recv_thread daemonser.read(256)bytes from pendantroot.after(0, _log, "RX: ...", tag)Click "! Feed hold"ser.write(b"!")_log("TX: [! Feed hold] bytes: 21", "tx")Click Full sequencestart daemon threadser.write("$I\n")ser.write("$Report/Interval=100\n")ser.write("$planner_blocks\n")ser.write("$10=3\n")

These desktop tools exercise the corresponding on-device firmware modules:

Desktop toolFirmware moduleSource path
fluidnc_ble.pyESP3DBTBleClientmain/modules/bt_ble/esp3d_bt_ble_client.cpp
fluidnc_bt.pyESP3DBTSerialClientmain/modules/bt_serial/esp3d_bt_serial_client.cpp
grblhal_bt.pyESP3DBTSerialClientmain/modules/bt_serial/esp3d_bt_serial_client.cpp

The BLE GATT server on the firmware side registers the same service (0xFFF0) and characteristics (0xFFF1 notify, 0xFFF2 write) that fluidnc_ble.py connects to. The SPP server exposed by ESP3DBTSerialClient is what the OS pairs with and presents as a COM port to fluidnc_bt.py and grblhal_bt.py.

See bluetooth_ble.md and bluetooth_serial.md for the on-device lifecycle, FreeRTOS task details, and connection state machine.

Build constraint reminder — Bluetooth and WiFi are mutually exclusive on this board (no PSRAM). A pendant build with a BT transport active will have WiFi disabled. See cmake/sanity_check.cmake and the feature resource matrix in the project docs.


fluidnc_ble.py

bleak

pip install bleak

fluidnc_bt.py

pyserial

pip install pyserial

grblhal_bt.py

tkinter

Python stdlib - no install needed

ToolInstall commandMinimum Python
fluidnc_ble.pypip install bleak3.8
fluidnc_bt.pypip install pyserial3.6
grblhal_bt.pypip install pyserial3.6

tkinter is part of the Python standard library. On Debian/Ubuntu Linux it may need a separate package: sudo apt install python3-tk.


Terminal window
# Interactive — open GUI, scan for devices manually
python tools/bt_client/fluidnc_ble.py
# Pre-seed with a known MAC address (skips the scan step)
python tools/bt_client/fluidnc_ble.py AA:BB:CC:DD:EE:FF
# Pre-seed with a device label as shown in the scan combobox
python tools/bt_client/fluidnc_ble.py "FluidNC BLE [AA:BB:CC:DD:EE:FF]"

Typical workflow:

  1. Click Scan — collects BLE advertisements for 8 s, resolves names via GATT for unnamed devices.
  2. Devices advertising service 0xFFF0 are marked ★ and sorted to the top; select one.
  3. Click Connect — subscribes to notifications on 0xFFF1 (RX characteristic).
  4. Use Real-time commands for instant single-byte control (?, !, ~, soft reset, jog cancel).
  5. Use individual Init sequence buttons or ▶ Full sequence to replay the pendant startup flow.
  6. Type arbitrary G-code or $ commands in the Command field and press Enter or Send.
  7. Use the Hex field for raw byte injection (e.g. 18 for \x18 soft reset).
Terminal window
# Interactive
python tools/bt_client/fluidnc_bt.py
# Pre-seed COM port only (baud defaults to 115200)
python tools/bt_client/fluidnc_bt.py COM5
# Pre-seed COM port and baud rate
python tools/bt_client/fluidnc_bt.py COM5 115200

Platform note: On Windows, pair the pendant via Bluetooth & devices → Add device. After pairing, a new COM port appears in Device Manager under Ports (COM & LPT). On Linux/macOS, use the BlueZ stack (rfcomm bind) or BlueZ’s D-Bus API to create a /dev/rfcomm* device and select it in the dropdown.

Terminal window
# Interactive
python tools/bt_client/grblhal_bt.py
# Pre-seed COM port
python tools/bt_client/grblhal_bt.py COM5
# Pre-seed COM port and baud rate
python tools/bt_client/grblhal_bt.py COM5 115200

grblHAL MPG token workflow:

  1. Connect to the pendant’s SPP COM port.
  2. Click 0x8B — MPG toggle to claim the MPG pendant token.
  3. The firmware responds with a status report confirming MPG mode is active.
  4. Send jog commands or use ? to poll status.
  5. Click 0x8B again to release the token when finished.

All three tools use the same colour convention:

ColourTagMeaning
CyantxData sent by the PC to the pendant
Lime greenrxCNC firmware responses received from the pendant
Dim grey #606060rx_logPendant internal log lines starting with [ — FluidNC tools only
YellowinfoConnection events, scan results, sequence headings
RederrErrors, connection loss, invalid input