Skip to content

Commit 90c7605

Browse files
committed
faster async disconnects
1 parent 674eff7 commit 90c7605

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+77207
-73072
lines changed

doc/help_dialogs/Output_html/energy_help.html

+162-162
Large diffs are not rendered by default.

doc/help_dialogs/Output_html/keyboardshortcuts_help.html

+506-506
Large diffs are not rendered by default.

src/artisanlib/acaia.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -534,10 +534,10 @@ def __init__(self, model:int, ident:Optional[str], name:Optional[str], connected
534534
Scale.__init__(self, model, ident, name)
535535
AcaiaBLE.__init__(self, connected_handler = connected_handler, disconnected_handler=disconnected_handler)
536536

537-
def connect(self) -> None:
537+
def connect_scale(self) -> None:
538538
self.start(address=self.ident)
539539

540-
def disconnect(self) -> None:
540+
def disconnect_scale(self) -> None:
541541
self.stop()
542542

543543
def weight_changed(self, new_value:int) -> None:
@@ -549,3 +549,6 @@ def battery_changed(self, new_value:int) -> None:
549549
def on_disconnect(self) -> None:
550550
self.disconnected_signal.emit()
551551
AcaiaBLE.on_disconnect(self)
552+
553+
def tare_scale(self) -> None:
554+
self.send_tare()

src/artisanlib/async_comm.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ def start_background_loop(loop:asyncio.AbstractEventLoop) -> None:
6060

6161
def __del__(self) -> None:
6262
self.__loop.call_soon_threadsafe(self.__loop.stop)
63-
self.__thread.join()
63+
# self.__thread.join()
64+
# WARNING: we don't join and expect the clients running on this thread to stop themself
65+
# (using self._running) to finally get rid of this thread to prevent hangs
6466

6567
@property
6668
def loop(self) -> asyncio.AbstractEventLoop:
@@ -139,7 +141,7 @@ async def readuntil(self, separator:bytes = b'\n') -> bytes:
139141

140142
class AsyncComm:
141143

142-
__slots__ = [ '_asyncLoopThread', '_write_queue', '_host', '_port', '_serial', '_connected_handler', '_disconnected_handler',
144+
__slots__ = [ '_asyncLoopThread', '_write_queue', '_running', '_host', '_port', '_serial', '_connected_handler', '_disconnected_handler',
143145
'_verify_crc', '_logging' ]
144146

145147
def __init__(self, host:str = '127.0.0.1', port:int = 8080, serial:Optional['SerialSettings'] = None,
@@ -148,6 +150,7 @@ def __init__(self, host:str = '127.0.0.1', port:int = 8080, serial:Optional['Ser
148150
# internals
149151
self._asyncLoopThread: Optional[AsyncLoopThread] = None # the asyncio AsyncLoopThread object
150152
self._write_queue: 'Optional[asyncio.Queue[bytes]]' = None # noqa: UP037 # quotes for Python3.8 # the write queue
153+
self._running:bool = False # while True we keep running the thread
151154

152155
# connection
153156
self._host:str = host
@@ -256,7 +259,7 @@ async def handle_writes(self, writer: asyncio.StreamWriter, queue: 'asyncio.Queu
256259
# if serial settings are given, host/port are ignore and communication is handled by the given serial port
257260
async def connect(self, connect_timeout:float=5) -> None:
258261
writer = None
259-
while True:
262+
while self._running:
260263
try:
261264
if self._serial is not None:
262265
_log.debug('connecting to serial port: %s ...', self._serial['port'])
@@ -311,7 +314,7 @@ async def connect(self, connect_timeout:float=5) -> None:
311314
except Exception as e: # pylint: disable=broad-except
312315
_log.exception(e)
313316

314-
await asyncio.sleep(0.7)
317+
await asyncio.sleep(0.5)
315318

316319
def send(self, message:bytes) -> None:
317320
if self.async_loop_thread is not None and self._write_queue is not None:
@@ -324,6 +327,7 @@ def start(self, connect_timeout:float=5) -> None:
324327
try:
325328
_log.debug('start sampling')
326329
if self._asyncLoopThread is None:
330+
self._running = True
327331
self._asyncLoopThread = AsyncLoopThread()
328332
# run sample task in async loop
329333
asyncio.run_coroutine_threadsafe(self.connect(connect_timeout), self._asyncLoopThread.loop)
@@ -332,7 +336,7 @@ def start(self, connect_timeout:float=5) -> None:
332336

333337
def stop(self) -> None:
334338
_log.debug('stop sampling')
335-
del self._asyncLoopThread
339+
self._running = False
336340
self._asyncLoopThread = None
337341
self._write_queue = None
338342
self.reset_readings()

src/artisanlib/ble_port.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ def scan_and_connect(self,
161161
blacklist:Set[str], # list of client addresses to ignore as they don't offer the required service
162162
case_sensitive:bool=True,
163163
disconnected_callback:Optional[Callable[[BleakClient], None]] = None,
164-
scan_timeout:float=10,
165-
connect_timeout:float=3,
164+
scan_timeout:float=3,
165+
connect_timeout:float=2,
166166
address:Optional[str] = None # if given, connect only to the device with this ble address
167167
) -> Tuple[Optional[BleakClient], Optional[str]]:
168168
if hasattr(self, '_asyncLoopThread') and self._asyncLoopThread is None:
@@ -284,7 +284,7 @@ def connected(self) -> Optional[str]:
284284

285285

286286
# connect and re-connect while self._running to BLE
287-
async def _connect(self, case_sensitive:bool=True, scan_timeout:float=10, connect_timeout:float=4, address:Optional[str] = None) -> None:
287+
async def _connect(self, case_sensitive:bool=True, scan_timeout:float=3, connect_timeout:float=2, address:Optional[str] = None) -> None:
288288
blacklist:Set[str] = set()
289289
while self._running:
290290
# scan and connect
@@ -384,7 +384,7 @@ def read(self, read_characteristic:Optional[str] = None) -> Optional[bytes]:
384384
return None
385385

386386
async def _keep_alive(self) -> None:
387-
while self._heartbeat_frequency > 0:
387+
while self._heartbeat_frequency > 0 and self._running:
388388
await asyncio.sleep(self._heartbeat_frequency)
389389
self.heartbeat()
390390

@@ -394,7 +394,7 @@ async def _connect_and_keep_alive(self,case_sensitive:bool,scan_timeout:float, c
394394
self._keep_alive())
395395

396396

397-
def start(self, case_sensitive:bool=True, scan_timeout:float=10, connect_timeout:float=4, address:Optional[str] = None) -> None:
397+
def start(self, case_sensitive:bool=True, scan_timeout:float=3, connect_timeout:float=2, address:Optional[str] = None) -> None:
398398
_log.debug('start')
399399
if self._running:
400400
_log.error('BLE client already running')
@@ -420,7 +420,6 @@ def stop(self) -> None:
420420
if self._ble_client is None:
421421
ble.terminate_scan() # we stop ongoing scanning
422422
self._disconnect()
423-
#del self._async_loop_thread # on this level the released object should be automatically collected by the GC
424423
self._async_loop_thread = None
425424
self._ble_client = None
426425
self._connected_service_uuid = None

src/artisanlib/canvas.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -12825,8 +12825,8 @@ def OnMonitor(self) -> None:
1282512825
# CHARGE handler disactivated to not trigger CHARGE after CHARGE is signalled to the machine by START
1282612826
# NOTE: only after CHARGE the heater
1282712827
# charge_handler=lambda : (self.markChargeDelaySignal.emit(0) if (self.timeindex[0] == -1) else None),
12828-
dry_handler=lambda : (self.markDRYSignal.emit(False) if (self.timeindex[2] == 0) else None),
12829-
fcs_handler=lambda : (self.markFCsSignal.emit(False) if (self.timeindex[1] == 0) else None),
12828+
dry_handler=lambda : (self.markDRYSignal.emit(False) if (self.timeindex[1] == 0) else None),
12829+
fcs_handler=lambda : (self.markFCsSignal.emit(False) if (self.timeindex[2] == 0) else None),
1283012830
scs_handler=lambda : (self.markSCsSignal.emit(False) if (self.timeindex[4] == 0) else None),
1283112831
drop_handler=lambda : (self.markDropSignal.emit(False) if (self.timeindex[6] == 0) else None))
1283212832
self.aw.santoker.setLogging(self.device_logging)

src/artisanlib/devices.py

+112-7
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
if TYPE_CHECKING:
2727
from artisanlib.main import ApplicationWindow # noqa: F401 # pylint: disable=unused-import
2828

29-
from artisanlib.util import deltaLabelUTF8, setDeviceDebugLogLevel, argb_colorname2rgba_colorname, rgba_colorname2argb_colorname, toInt
29+
from artisanlib.util import (deltaLabelUTF8, setDeviceDebugLogLevel, argb_colorname2rgba_colorname, rgba_colorname2argb_colorname,
30+
toInt, weight_units, convertWeight)
3031
from artisanlib.dialogs import ArtisanResizeablDialog
3132
from artisanlib.widgets import MyContentLimitedQComboBox, MyQComboBox, MyQDoubleSpinBox, wait_cursor
3233
from artisanlib.scale import SUPPORTED_SCALES, ScaleSpecs
@@ -1453,6 +1454,8 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
14531454

14541455
self.scale1_devices:ScaleSpecs = [] # discovered scale1 devices
14551456
self.scale2_devices:ScaleSpecs = [] # discovered scale2 devices
1457+
self.scale1_weight:Optional[float] = None # weight of scale 1 in g
1458+
self.scale2_weight:Optional[float] = None # weight of scale 2 in g
14561459

14571460
scale1ModelLabel = QLabel(QApplication.translate('Label','Model'))
14581461
self.scale1ModelComboBox = QComboBox()
@@ -1463,6 +1466,13 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
14631466
self.scale1NameComboBox.setMinimumWidth(150)
14641467
self.scale1ScanButton = QPushButton(QApplication.translate('Button', 'Scan'))
14651468
self.scale1ScanButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1469+
self.scale1Weight = QLabel() # displays the current reading
1470+
self.scale1Weight.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
1471+
self.scale1Weight.setMinimumWidth(40)
1472+
self.scale1Weight.setEnabled(False)
1473+
self.scale1TareButton = QPushButton(QApplication.translate('Button', 'Tare'))
1474+
self.scale1TareButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1475+
self.scale1TareButton.setEnabled(False)
14661476
if self.aw.scale1_model is None:
14671477
self.scale1NameComboBox.setEnabled(False)
14681478
self.scale1ScanButton.setEnabled(False)
@@ -1475,6 +1485,8 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
14751485
self.scale1ModelComboBox.currentIndexChanged.connect(self.scale1ModelChanged)
14761486
self.scale1NameComboBox.currentIndexChanged.connect(self.scale1NameChanged)
14771487
self.scale1ScanButton.clicked.connect(self.scanScale1)
1488+
self.scale1TareButton.clicked.connect(self.tareScale1)
1489+
self.update_scale1_weight(None)
14781490

14791491
if self.aw.scale1_name and self.aw.scale1_id:
14801492
self.updateScale1devices([(self.aw.scale1_name, self.aw.scale1_id)])
@@ -1485,11 +1497,18 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
14851497
scale1Grid.addWidget(self.scale1NameLabel,1,0)
14861498
scale1Grid.addWidget(self.scale1NameComboBox,1,1)
14871499
scale1Grid.addWidget(self.scale1ScanButton,1,2)
1500+
scale1Grid.addWidget(self.scale1Weight,1,3,Qt.AlignmentFlag.AlignCenter)
1501+
scale1Grid.addWidget(self.scale1TareButton,1,4,Qt.AlignmentFlag.AlignRight)
1502+
scale1Grid.setHorizontalSpacing(10)
1503+
scale1Grid.setVerticalSpacing(10)
1504+
scale1Grid.setContentsMargins(10,10,10,10)
14881505
scale1HLayout = QHBoxLayout()
14891506
scale1HLayout.addLayout(scale1Grid)
14901507
scale1HLayout.addStretch()
1508+
scale1HLayout.setContentsMargins(0,0,0,0)
14911509
scale1Layout = QVBoxLayout()
14921510
scale1Layout.addLayout(scale1HLayout)
1511+
scale1Layout.setContentsMargins(0,0,0,0)
14931512

14941513
scale2ModelLabel = QLabel(QApplication.translate('Label','Model'))
14951514
self.scale2ModelComboBox = QComboBox()
@@ -1500,6 +1519,13 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
15001519
self.scale2NameComboBox.setMinimumWidth(150)
15011520
self.scale2ScanButton = QPushButton(QApplication.translate('Button', 'Scan'))
15021521
self.scale2ScanButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1522+
self.scale2Weight = QLabel() # displays the current reading
1523+
self.scale2Weight.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
1524+
self.scale2Weight.setMinimumWidth(40)
1525+
self.scale2Weight.setEnabled(False)
1526+
self.scale2TareButton = QPushButton(QApplication.translate('Button', 'Tare'))
1527+
self.scale2TareButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
1528+
self.scale2TareButton.setEnabled(False)
15031529
if self.aw.scale2_model is None:
15041530
self.scale2NameComboBox.setEnabled(False)
15051531
self.scale2ScanButton.setEnabled(False)
@@ -1512,6 +1538,8 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
15121538
self.scale2ModelComboBox.currentIndexChanged.connect(self.scale2ModelChanged)
15131539
self.scale2NameComboBox.currentIndexChanged.connect(self.scale2NameChanged)
15141540
self.scale2ScanButton.clicked.connect(self.scanScale2)
1541+
self.scale2TareButton.clicked.connect(self.tareScale2)
1542+
self.update_scale2_weight(None)
15151543

15161544
if self.aw.scale2_name and self.aw.scale2_id:
15171545
self.updateScale2devices([(self.aw.scale2_name, self.aw.scale2_id)])
@@ -1522,11 +1550,18 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
15221550
scale2Grid.addWidget(self.scale2NameLabel,1,0)
15231551
scale2Grid.addWidget(self.scale2NameComboBox,1,1)
15241552
scale2Grid.addWidget(self.scale2ScanButton,1,2)
1553+
scale2Grid.addWidget(self.scale2Weight,1,3,Qt.AlignmentFlag.AlignCenter)
1554+
scale2Grid.addWidget(self.scale2TareButton,1,4,Qt.AlignmentFlag.AlignRight)
1555+
scale2Grid.setHorizontalSpacing(10)
1556+
scale2Grid.setVerticalSpacing(10)
1557+
scale2Grid.setContentsMargins(10,10,10,10)
15251558
scale2HLayout = QHBoxLayout()
1559+
scale2HLayout.setContentsMargins(0,0,0,0)
15261560
scale2HLayout.addLayout(scale2Grid)
15271561
scale2HLayout.addStretch()
15281562
scale2Layout = QVBoxLayout()
15291563
scale2Layout.addLayout(scale2HLayout)
1564+
scale2Layout.setContentsMargins(0,0,0,0)
15301565

15311566
self.taskWebDisplayGreenURL = QLabel()
15321567
self.taskWebDisplayGreenURL.setOpenExternalLinks(True)
@@ -1712,14 +1747,50 @@ def scale1NameChanged(self, i:int) -> None:
17121747
scale = self.aw.scale_manager.get_scale(self.aw.scale1_model, self.aw.scale1_id, self.aw.scale1_name)
17131748
self.aw.scale_manager.set_scale1(scale)
17141749
if scale is not None:
1715-
scale.set_connected_handler(lambda : self.aw.sendmessageSignal.emit(QApplication.translate('Message', '{} connected').format(self.aw.scale1_name),True,None))
1716-
scale.set_disconnected_handler(lambda : self.aw.sendmessageSignal.emit(QApplication.translate('Message', '{} disconnected').format(self.aw.scale1_name),True,None))
1717-
scale.connect()
1750+
scale.set_connected_handler(self.scale1connected)
1751+
scale.set_disconnected_handler(self.scale1disconnected)
1752+
scale.weight_changed_signal.connect(self.scale1_weight_changed) # type:ignore[call-overload]
1753+
scale.connect_scale()
17181754
# i == -1 if self.scale1NameComboBox is empty!
17191755
else:
17201756
scale1 = self.aw.scale_manager.get_scale1()
17211757
if scale1 is not None:
1722-
scale1.disconnect()
1758+
scale1.weight_changed_signal.disconnect() # type:ignore[call-overload]
1759+
scale1.disconnect_scale()
1760+
1761+
def scale1connected(self) -> None:
1762+
self.aw.sendmessageSignal.emit(QApplication.translate('Message', '{} connected').format(self.aw.scale1_name),True,None)
1763+
self.scale1Weight.setEnabled(True)
1764+
self.scale1TareButton.setEnabled(True)
1765+
1766+
def scale1disconnected(self) -> None:
1767+
self.aw.sendmessageSignal.emit(QApplication.translate('Message', '{} disconnected').format(self.aw.scale1_name),True,None)
1768+
self.scale1Weight.setEnabled(False)
1769+
self.scale1TareButton.setEnabled(False)
1770+
self.update_scale1_weight(None)
1771+
1772+
@pyqtSlot(int)
1773+
def scale1_weight_changed(self, w:int) -> None:
1774+
self.update_scale1_weight(w)
1775+
1776+
# returns formated weight converted to current weight unit
1777+
def format_scale_weight(self, w:Optional[float]) -> str:
1778+
if w is None:
1779+
return ''
1780+
unit = weight_units.index(self.aw.qmc.weight[2])
1781+
if unit == 0: # g selected
1782+
# metric
1783+
return f'{w:.0f}g' # never show decimals for g
1784+
if unit == 1: # kg selected
1785+
# metric (always keep the accuracy to the g
1786+
return f'{w/1000:.3f}kg'
1787+
# non-metric
1788+
v = convertWeight(w,0,weight_units.index(self.aw.qmc.weight[2]))
1789+
return f'{v:.2f}{self.aw.qmc.weight[2].lower()}'
1790+
1791+
def update_scale1_weight(self, weight:Optional[float]) -> None:
1792+
self.scale1_weight = weight
1793+
self.scale1Weight.setText(self.format_scale_weight(self.scale1_weight))
17231794

17241795
def updateScale1devices(self, devices:ScaleSpecs) -> None:
17251796
self.scale1_devices = devices
@@ -1739,6 +1810,12 @@ def scanScale1(self, _:bool = False) -> None:
17391810
else:
17401811
self.scale1NameComboBox.setEnabled(False)
17411812

1813+
@pyqtSlot(bool)
1814+
def tareScale1(self, _:bool = False) -> None:
1815+
scale = self.aw.scale_manager.get_scale1()
1816+
if scale is not None:
1817+
scale.tare_scale()
1818+
17421819
@pyqtSlot(int)
17431820
def scale2ModelChanged(self, i:int) -> None:
17441821
if i > 0 and len(SUPPORTED_SCALES) > i-1 and len(SUPPORTED_SCALES[i-1]) > 0:
@@ -1760,12 +1837,35 @@ def scale2NameChanged(self, i:int) -> None:
17601837
scale = self.aw.scale_manager.get_scale(self.aw.scale2_model, self.aw.scale2_id, self.aw.scale2_name)
17611838
self.aw.scale_manager.set_scale2(scale)
17621839
if scale is not None:
1763-
scale.connect()
1840+
scale.set_connected_handler(self.scale2connected)
1841+
scale.set_disconnected_handler(self.scale2disconnected)
1842+
scale.weight_changed_signal.connect(self.scale2_weight_changed) # type:ignore[call-overload]
1843+
scale.connect_scale()
17641844
# i == -1 if self.scale2NameComboBox is empty!
17651845
else:
17661846
scale2 = self.aw.scale_manager.get_scale2()
17671847
if scale2 is not None:
1768-
scale2.disconnect()
1848+
scale2.weight_changed_signal.disconnect() # type:ignore[call-overload]
1849+
scale2.disconnect_scale()
1850+
1851+
@pyqtSlot(int)
1852+
def scale2_weight_changed(self, w:int) -> None:
1853+
self.update_scale2_weight(w)
1854+
1855+
def update_scale2_weight(self, weight:Optional[float]) -> None:
1856+
self.scale2_weight = weight
1857+
self.scale2Weight.setText(self.format_scale_weight(self.scale2_weight))
1858+
1859+
def scale2connected(self) -> None:
1860+
self.aw.sendmessageSignal.emit(QApplication.translate('Message', '{} connected').format(self.aw.scale2_name),True,None)
1861+
self.scale2Weight.setEnabled(True)
1862+
self.scale2TareButton.setEnabled(True)
1863+
1864+
def scale2disconnected(self) -> None:
1865+
self.aw.sendmessageSignal.emit(QApplication.translate('Message', '{} disconnected').format(self.aw.scale2_name),True,None)
1866+
self.scale2Weight.setEnabled(False)
1867+
self.scale2TareButton.setEnabled(False)
1868+
self.update_scale2_weight(None)
17691869

17701870
def updateScale2devices(self, devices:ScaleSpecs) -> None:
17711871
self.scale2_devices = devices
@@ -1785,6 +1885,11 @@ def scanScale2(self, _:bool = False) -> None:
17851885
else:
17861886
self.scale2NameComboBox.setEnabled(False)
17871887

1888+
@pyqtSlot(bool)
1889+
def tareScale2(self, _:bool = False) -> None:
1890+
scale = self.aw.scale_manager.get_scale2()
1891+
if scale is not None:
1892+
scale.tare_scale()
17881893

17891894
@pyqtSlot(bool)
17901895
def taskWebDisplayGreen(self, b:bool = False) -> None:

0 commit comments

Comments
 (0)