Skip to content

Commit d6e79aa

Browse files
committed
release: 1.4.15
- auto-select first detected serial port on startup - disconnect BLE device before SerialUI shutdown teardown - allow live pyqtgraph zooming/panning without forced range fights - fix header parser duplicate labels such as _1 and _2
1 parent 4a0daa3 commit d6e79aa

11 files changed

Lines changed: 351 additions & 119 deletions

File tree

.codex

Whitespace-only changes.

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,6 @@ On Windows amr64 platform, llvmlite is not available and numba acceleration is n
217217

218218
## Contributors
219219

220-
Urs Utzinger, 2022-2025 (University of Arizona),
221-
222-
Cameron K Brooks, 2024 (Western University),
223-
224-
GPT-5.3, OpenAI
220+
- Urs Utzinger, 2022-2026 (University of Arizona)
221+
- Cameron K Brooks, 2024 (Western University)
222+
- GPT-5.3,4, (OpenAI)

SerialUI.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,6 @@ def _run_c_parser_selftest_mode() -> int:
117117

118118
errors = []
119119

120-
try:
121-
from line_parsers import simple_parser # noqa: F401
122-
from line_parsers import header_parser # noqa: F401
123-
print("C parser self-test passed (backend=line_parsers)")
124-
return 0
125-
except Exception as exc:
126-
errors.append(f"line_parsers: {exc}")
127-
128120
try:
129121
from helpers.line_parsers import simple_parser # noqa: F401
130122
from helpers.line_parsers import header_parser # noqa: F401
@@ -133,6 +125,14 @@ def _run_c_parser_selftest_mode() -> int:
133125
except Exception as exc:
134126
errors.append(f"helpers.line_parsers: {exc}")
135127

128+
try:
129+
from line_parsers import simple_parser # noqa: F401
130+
from line_parsers import header_parser # noqa: F401
131+
print("C parser self-test passed (backend=line_parsers)")
132+
return 0
133+
except Exception as exc:
134+
errors.append(f"line_parsers: {exc}")
135+
136136
print(
137137
"C parser self-test failed: " + " | ".join(errors),
138138
file=sys.stderr,
@@ -1181,6 +1181,11 @@ def closeEvent(self, event):
11811181
f"[{self.instance_name[:15]:<15}]: Finishing workers..."
11821182
)
11831183

1184+
# Keep the BLE worker stack alive while the disconnect handshake completes.
1185+
if USE_BLE and getattr(self, "ble", None) is not None:
1186+
self.ble.disconnect_before_shutdown(timeout_ms=5000)
1187+
QCoreApplication.processEvents()
1188+
11841189
disconnect(self.mtocRequest, self.serial.on_mtocRequest)
11851190
disconnect(self.mtocRequest, self.chart.on_mtocRequest)
11861191
disconnect(self.mtocRequest, self.usbmonitor.on_mtocRequest)

ToDo.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Task List
2+
3+
## Auto select Serial Port
4+
When program boots up and serial port finds a suitable port it should select the first non empty port automatically and not the empty item.
5+
[Status: done]
6+
[Implemented: when the serial port list is refreshed and no port is currently connected, the first detected real port is selected instead of the trailing None entry.]
7+
8+
## BLE Connection
9+
If BLE serial device is already connected to system a scanning attempt will not find it.
10+
[Status: done]
11+
[Implemented: on shutdown, SerialUI now requests BLE disconnect before the BLE worker thread and event loop are torn down, so the device is cleanly disconnected when the program closes.]
12+
13+
14+
## Zooming and Panning while Life Update
15+
When charting is paused or stopped one can pan and zoom but when charting is running autoscaling is enabled and one can not zoom into the data while its updating. pyqtgraph has option to deselect plot items in the legend menu. Is it possible to autoscale only to the the items selected in the legend?
16+
[Status: done for pyqtgraph, postponed for fastplotlib]
17+
[Implemented: chart updates keep running while pyqtgraph mouse pan/zoom stays enabled. Manual pan/zoom suspends live x/y follow on the changed axis, and pyqtgraph View All or auto-range can re-enable live follow.]
18+
19+
20+
## Label Parsing
21+
With the data shown below received over BLEserial and displayed in terminal and with C accelerated parser enabled I get duplicate legend lables such as CH0_AVG_1 and and CH0_AVG_2 for all of the items except for "corr". The duplicates do not contain data on the chart.
22+
23+
CH0_AVG:836.9,CH0_RMS:120.36,CH1_AVG:827.1,CH1_RMS:0.00,lag:-15,phase_60Hz:-26.0,corr:0.06
24+
CH0_AVG:837.4,CH0_RMS:120.35,CH1_AVG:827.1,CH1_RMS:0.00,lag:-8,phase_60Hz:-27.9,corr:0.07
25+
CH0_AVG:837.9,CH0_RMS:120.29,CH1_AVG:827.1,CH1_RMS:0.00,lag:9,phase_60Hz:-20.1,corr:0.06
26+
27+
[Status: done]
28+
[Implemented: reproduced the duplicate labels in both the Python and C++ header parsers. The root cause was a trailing comma before the next header being interpreted as an extra empty sub-channel, so labels such as CH0_AVG became CH0_AVG_1 and CH0_AVG_2. Both parsers were updated to ignore that separator-only trailing segment, and the C++ parser module was rebuilt in place.]

config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from helpers.colors import color_names_sweet16 as COLORS
77
################################################################################################################################
88
# Constants General
9-
VERSION = "1.4.13" # this version
9+
VERSION = "1.5.0" # this version
1010
AUTHOR = "Urs Utzinger" # me
1111
DATE = "2026" # year of last update
1212
################################################################################################################################
@@ -52,7 +52,7 @@
5252
################################################################################################################################
5353
# Constants BLE
5454
# Medibrick
55-
DEFAULT_TARGET_DEVICE_NAME = "MediBrick" # The name of the BLE device to search for,
55+
DEFAULT_TARGET_DEVICE_NAME = "BLESerialDevice" # The name of the BLE device to search for if its already connected
5656
# Program searches for all Nordic Serial UART service by default
5757
BLEPIN = "123456" # Known pairing pin for Medibrick_BLE
5858
# UUIDs for the Nordic Serial UART service and characteristics

helpers/QBLE_helper.py

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from config import (FLUSH_INTERVAL_MS,
1818
BLEPIN,
1919
BLESCAN_SHORT, BLESCAN_LONG,
20+
DEFAULT_TARGET_DEVICE_NAME,
2021
SERVICE_UUID, RX_CHARACTERISTIC_UUID, TX_CHARACTERISTIC_UUID,
2122
USE_BLUETOOTHCTL, BLEMTUMAX, BLEMTUNORMAL, ATT_HDR, BLEMTUDEFAULT,
2223
PROFILEME, DEBUGSERIAL, DEBUG_LEVEL,
@@ -673,6 +674,8 @@ def on_deviceListReady(self, devices:list):
673674
# save current selected device
674675
currentIndex = self.ui.comboBoxDropDown_Device.currentIndex()
675676
selectedDevice = self.ui.comboBoxDropDown_Device.itemData(currentIndex)
677+
selected_address = getattr(selectedDevice, "address", None)
678+
current_address = getattr(self.device, "address", None)
676679

677680
self.ui.comboBoxDropDown_Device.blockSignals(True)
678681
self.ui.comboBoxDropDown_Device.clear()
@@ -681,7 +684,14 @@ def on_deviceListReady(self, devices:list):
681684

682685
# search for previous device and select it
683686
index_to_select = -1
684-
if selectedDevice is not None:
687+
if selected_address or current_address:
688+
for index in range(self.ui.comboBoxDropDown_Device.count()):
689+
device = self.ui.comboBoxDropDown_Device.itemData(index)
690+
device_address = getattr(device, "address", None)
691+
if device_address and device_address in {selected_address, current_address}:
692+
index_to_select = index
693+
break
694+
elif selectedDevice is not None:
685695
for index in range(self.ui.comboBoxDropDown_Device.count()):
686696
if self.ui.comboBoxDropDown_Device.itemData(index) == selectedDevice:
687697
index_to_select = index
@@ -1157,6 +1167,66 @@ def on_disconnectingSuccess(self, success):
11571167

11581168
self.ui.statusBar().showMessage('BLE device disconnected.', 2000)
11591169

1170+
def disconnect_before_shutdown(self, timeout_ms: int = 5000) -> bool:
1171+
"""
1172+
Request a BLE disconnect while the worker thread and event loop are still alive.
1173+
1174+
The disconnect button path works because it always queues the request to the
1175+
worker and lets the worker decide whether a client is still connected. Reuse
1176+
that same approach for shutdown instead of relying on cross-thread reads of
1177+
`client.is_connected`, which can be stale or already cleared by teardown.
1178+
"""
1179+
bleakWorker = getattr(self, "bleakWorker", None)
1180+
if not bleakWorker or not qobject_alive(bleakWorker):
1181+
return False
1182+
1183+
try:
1184+
bleakWorker.reconnect = False
1185+
except Exception:
1186+
pass
1187+
1188+
button_connected = False
1189+
try:
1190+
button_connected = (self.ui.pushButton_BLEConnect.text() == "Disconnect")
1191+
except Exception:
1192+
pass
1193+
1194+
client = getattr(bleakWorker, "client", None)
1195+
needs_disconnect = any((
1196+
button_connected,
1197+
bool(client is not None),
1198+
bool(getattr(bleakWorker, "disconnecting", False)),
1199+
bool(self.device_info.get("connected", False)),
1200+
bool(self.receiverIsRunning),
1201+
))
1202+
if not needs_disconnect:
1203+
return False
1204+
1205+
self.logSignal.emit(logging.INFO,
1206+
f"[{self.instance_name[:15]:<15}]: Requesting BLE device disconnect before shutdown."
1207+
)
1208+
if not getattr(bleakWorker, "disconnecting", False):
1209+
self.disconnectDeviceRequest.emit()
1210+
1211+
ok, args, reason = wait_for_signal(
1212+
bleakWorker.disconnectingSuccess,
1213+
timeout_ms=timeout_ms,
1214+
sender=bleakWorker,
1215+
)
1216+
if not ok and reason != "destroyed":
1217+
self.logSignal.emit(logging.WARNING,
1218+
f"[{self.instance_name[:15]:<15}]: BLE disconnect before shutdown timed out because of {reason}."
1219+
)
1220+
return False
1221+
1222+
if args and args[0] is False:
1223+
self.logSignal.emit(logging.WARNING,
1224+
f"[{self.instance_name[:15]:<15}]: BLE disconnect before shutdown reported failure."
1225+
)
1226+
return False
1227+
1228+
return bool(ok)
1229+
11601230
# Bluetoothctl SUCCESS
11611231
# ----------------------------------------
11621232

@@ -1270,6 +1340,9 @@ def cleanup(self):
12701340
btWorker = getattr(self, "bluetoothctlWorker", None)
12711341
btThread = getattr(self, "bluetoothctlThread", None)
12721342

1343+
# Explicit disconnect must happen before the worker thread is asked to finish.
1344+
self.disconnect_before_shutdown(timeout_ms=5000)
1345+
12731346
# Request both workers to finish (single signal wired to both)
12741347
self.finishWorkerRequest.emit()
12751348

@@ -1926,17 +1999,35 @@ async def _scanDevices(self, timeout: float = BLESCAN_LONG):
19261999
)
19272000

19282001
self.NSUdevices = []
2002+
seen_addresses = set()
2003+
known_addresses = {
2004+
getattr(self.device, "address", None),
2005+
getattr(self.device_backup, "address", None),
2006+
}
2007+
known_addresses = {address for address in known_addresses if address}
2008+
known_names = {
2009+
getattr(self.device, "name", None),
2010+
getattr(self.device_backup, "name", None),
2011+
DEFAULT_TARGET_DEVICE_NAME,
2012+
}
2013+
known_names = {name.strip() for name in known_names if isinstance(name, str) and name.strip()}
19292014
for device, adv in devices.values():
19302015
self.logSignal.emit(logging.INFO,
19312016
f"[{self.instance_name[:15]:<15}]: Found device: {device.name} ({device.address}) RSSI: {adv.rssi} dBm"
19322017
)
19332018
suids = adv.service_uuids or ()
1934-
for service_uuid in suids:
1935-
# self.logSignal.emit(logging.INFO,
1936-
# f"[{self.instance_name[:15]:<15}]: Found service UUID: {service_uuid}"
1937-
# )
1938-
if service_uuid.lower() == SERVICE_UUID.lower():
2019+
service_match = any(service_uuid.lower() == SERVICE_UUID.lower() for service_uuid in suids)
2020+
name_match = bool(device.name and device.name.strip() in known_names)
2021+
address_match = bool(device.address and device.address in known_addresses)
2022+
2023+
if service_match or name_match or address_match:
2024+
if device.address not in seen_addresses:
19392025
self.NSUdevices.append(device)
2026+
seen_addresses.add(device.address)
2027+
if not service_match and (name_match or address_match):
2028+
self.logSignal.emit(logging.INFO,
2029+
f"[{self.instance_name[:15]:<15}]: Keeping known BLE device without advertised NUS UUID: {device.name} ({device.address})"
2030+
)
19402031

19412032
if not self.NSUdevices:
19422033
self.logSignal.emit(logging.INFO,

0 commit comments

Comments
 (0)