24
24
from bleak .backends .characteristic import BleakGATTCharacteristic # pylint: disable=unused-import
25
25
26
26
try :
27
- from PyQt6 .QtCore import QObject # @UnusedImport @Reimport @UnresolvedImport
27
+ from PyQt6 .QtCore import pyqtSignal , pyqtSlot # @UnusedImport @Reimport @UnresolvedImport
28
28
except ImportError :
29
- from PyQt5 .QtCore import QObject # type: ignore # @UnusedImport @Reimport @UnresolvedImport
29
+ from PyQt5 .QtCore import pyqtSignal , pyqtSlot # type: ignore # @UnusedImport @Reimport @UnresolvedImport
30
30
31
31
from artisanlib .ble_port import ClientBLE
32
32
from artisanlib .async_comm import AsyncIterable , IteratorReader
33
- from artisanlib .scale import Scale
33
+ from artisanlib .scale import Scale , ScaleSpecs
34
34
from artisanlib .util import float2float
35
35
36
36
@@ -185,8 +185,12 @@ class ACAIA_TIMER(IntEnum):
185
185
186
186
187
187
188
- class AcaiaBLE (QObject , ClientBLE ): # pyright: ignore [reportGeneralTypeIssues] # Argument to class must be a base class
188
+ class AcaiaBLE (ClientBLE ): # pyright: ignore [reportGeneralTypeIssues] # Argument to class must be a base class
189
189
190
+ weight_changed_signal = pyqtSignal (float ) # delivers new weight in g with decimals for accurate conversion
191
+ battery_changed_signal = pyqtSignal (int ) # delivers new batter level in %
192
+ connected_signal = pyqtSignal () # issued on connect
193
+ disconnected_signal = pyqtSignal () # issued on disconnect
190
194
191
195
# Acaia message constants
192
196
HEADER1 :Final [bytes ] = b'\xef '
@@ -212,8 +216,7 @@ def __init__(self, connected_handler:Optional[Callable[[], None]] = None,
212
216
self .scale_class :SCALE_CLASS = SCALE_CLASS .MODERN
213
217
214
218
# Protocol parser variables
215
- self ._read_queue : asyncio .Queue [bytes ] = asyncio .Queue (maxsize = 200 )
216
- self ._input_stream = IteratorReader (AsyncIterable (self ._read_queue ))
219
+ self ._read_queue : Optional [asyncio .Queue [bytes ]] = None
217
220
218
221
self .id_sent :bool = False # ID is sent once after first data is received from scale
219
222
self .fast_notifications_sent :bool = False # after connect we switch fast notification on to receive first reading fast
@@ -250,13 +253,6 @@ def __init__(self, connected_handler:Optional[Callable[[], None]] = None,
250
253
self .add_notify (ACAIA_UMBRA_NOTIFY_UUID , self .notify_callback )
251
254
self .add_write (ACAIA_UMBRA_SERVICE_UUID , ACAIA_UMBRA_WRITE_UUID )
252
255
253
- def set_connected_handler (self , connected_handler :Optional [Callable [[], None ]]) -> None :
254
- self ._connected_handler = connected_handler
255
-
256
- def set_disconnected_handler (self , disconnected_handler :Optional [Callable [[], None ]]) -> None :
257
- self ._disconnected_handler = disconnected_handler
258
-
259
-
260
256
# protocol parser
261
257
262
258
@@ -288,11 +284,13 @@ def on_connect(self) -> None:
288
284
self .set_heartbeat (0 ) # disable heartbeat
289
285
if self ._connected_handler is not None :
290
286
self ._connected_handler ()
287
+ self .connected_signal .emit ()
291
288
292
289
def on_disconnect (self ) -> None :
293
290
_log .debug ('disconnected' )
294
291
if self ._disconnected_handler is not None :
295
292
self ._disconnected_handler ()
293
+ self .disconnected_signal .emit ()
296
294
297
295
298
296
##
@@ -357,7 +355,7 @@ def update_weight(self, value:Optional[float]) -> None:
357
355
# if value is fresh and reading is stable
358
356
if value_rounded != self .weight : # and stable:
359
357
self .weight = value_rounded
360
- self .weight_changed (self .weight )
358
+ self .weight_changed_signal . emit (self .weight )
361
359
_log .debug ('new weight: %s' , self .weight )
362
360
363
361
# returns length of consumed data or -1 on error
@@ -373,7 +371,7 @@ def parse_battery_event(self, payload:bytes) -> int:
373
371
b = payload [0 ]
374
372
if 0 <= b <= 100 :
375
373
self .battery = int (payload [0 ])
376
- self .battery_changed (self .battery )
374
+ self .battery_changed_signal . emit (self .battery )
377
375
_log .debug ('battery: %s' , self .battery )
378
376
return EVENT_LEN .BATTERY
379
377
@@ -503,7 +501,7 @@ def parse_status(self, payload:bytes) -> None:
503
501
# byte 1: battery level (7 bits of second byte) + TIMER_START (1bit)
504
502
if payload and len (payload ) > 1 :
505
503
self .battery = int (payload [1 ] & ~ (1 << 7 ))
506
- self .battery_changed (self .battery )
504
+ self .battery_changed_signal . emit (self .battery )
507
505
_log .debug ('battery: %s%%' , self .battery )
508
506
509
507
# byte 2:
@@ -737,7 +735,7 @@ def fast_notifications(self) -> None:
737
735
###
738
736
739
737
def notify_callback (self , _sender :'BleakGATTCharacteristic' , data :bytearray ) -> None :
740
- if hasattr (self , '_async_loop_thread' ) and self ._async_loop_thread is not None :
738
+ if hasattr (self , '_async_loop_thread' ) and self ._async_loop_thread is not None and self . _read_queue is not None :
741
739
asyncio .run_coroutine_threadsafe (
742
740
self ._read_queue .put (bytes (data )),
743
741
self ._async_loop_thread .loop )
@@ -752,7 +750,9 @@ def notify_callback(self, _sender:'BleakGATTCharacteristic', data:bytearray) ->
752
750
# 6 data: d[4:10] = b'\x02\x14\x02<\x14\x00'
753
751
# 2 crc: d[10:12] = b'\x00W\x18' # calculated over "data_len+data"
754
752
755
- async def reader (self , stream :IteratorReader ) -> None :
753
+ async def reader (self ) -> None :
754
+ self ._read_queue = asyncio .Queue (maxsize = 200 ) # queue needs to be started in the current async event loop!
755
+ stream = IteratorReader (AsyncIterable (self ._read_queue ))
756
756
while True :
757
757
try :
758
758
await stream .readuntil (self .HEADER1 )
@@ -769,50 +769,68 @@ async def reader(self, stream:IteratorReader) -> None:
769
769
else :
770
770
_log .debug ('CRC error: %s <- %s' ,self .crc (data ),data )
771
771
except Exception as e : # pylint: disable=broad-except
772
- _log .exception (e )
772
+ _log .error (e )
773
773
774
774
775
775
def on_start (self ) -> None :
776
776
if hasattr (self , '_async_loop_thread' ) and self ._async_loop_thread is not None :
777
777
# start the reader
778
778
asyncio .run_coroutine_threadsafe (
779
- self .reader (self . _input_stream ),
779
+ self .reader (),
780
780
self ._async_loop_thread .loop )
781
781
782
782
783
- def weight_changed (self , new_value :float ) -> None : # pylint: disable=no-self-use
784
- del new_value
785
-
786
-
787
- def battery_changed (self , new_value :int ) -> None : # pylint: disable=no-self-use
788
- del new_value
789
-
790
-
791
-
792
- # AcaiaBLE and its super class are not allowed to hold __slots__
793
- class Acaia (AcaiaBLE , Scale ): # pyright: ignore [reportGeneralTypeIssues] # Argument to class must be a base class
783
+ class Acaia (Scale ): # pyright: ignore [reportGeneralTypeIssues] # Argument to class must be a base class
794
784
795
785
def __init__ (self , model :int , ident :Optional [str ], name :Optional [str ], connected_handler :Optional [Callable [[], None ]] = None ,
796
786
disconnected_handler :Optional [Callable [[], None ]] = None ):
797
- Scale .__init__ (self , model , ident , name )
798
- AcaiaBLE .__init__ (self , connected_handler = connected_handler , disconnected_handler = disconnected_handler )
787
+ super ().__init__ (model , ident , name )
788
+ self .acaia = AcaiaBLE (connected_handler = connected_handler , disconnected_handler = disconnected_handler )
789
+ self .acaia .weight_changed_signal .connect (self .weight_changed )
790
+ self .acaia .connected_signal .connect (self .on_connect )
791
+ self .acaia .disconnected_signal .connect (self .on_disconnect )
792
+
793
+ self .scale_connected = False
794
+
795
+
796
+ def scan (self ) -> None :
797
+ devices = self .acaia .scan ()
798
+ acaia_devices :ScaleSpecs = []
799
+ # for Acaia scales we filter by name
800
+ for d , a in devices :
801
+ name = (a .local_name or d .name )
802
+ if name :
803
+ match = next ((f'{ product_name } ({ name } )' for (name_prefix , product_name ) in ACAIA_SCALE_NAMES
804
+ if name and name .startswith (name_prefix )), None )
805
+ if match is not None :
806
+ acaia_devices .append ((match , d .address ))
807
+ self .scanned_signal .emit (acaia_devices )
808
+
809
+ def is_connected (self ) -> bool :
810
+ return self .scale_connected
799
811
800
812
def connect_scale (self ) -> None :
801
- _log .debug ('connect_scale' )
802
- self .start (address = self .ident )
813
+ self .acaia .start (address = self .ident )
803
814
804
815
def disconnect_scale (self ) -> None :
805
- self .stop ()
816
+ self .acaia . stop ()
806
817
818
+ @pyqtSlot (float )
807
819
def weight_changed (self , new_value :float ) -> None :
808
820
self .weight_changed_signal .emit (new_value )
809
821
810
822
def battery_changed (self , new_value :int ) -> None :
811
823
self .battery_changed_signal .emit (new_value )
812
824
825
+ @pyqtSlot ()
826
+ def on_connect (self ) -> None :
827
+ self .scale_connected = True
828
+ self .connected_signal .emit ()
829
+
830
+ @pyqtSlot ()
813
831
def on_disconnect (self ) -> None :
832
+ self .scale_connected = False
814
833
self .disconnected_signal .emit ()
815
- AcaiaBLE .on_disconnect (self )
816
834
817
835
def tare_scale (self ) -> None :
818
- self .send_tare ()
836
+ self .acaia . send_tare ()
0 commit comments