Skip to content

Commit 7a0027d

Browse files
committed
- updates acaia support
- adds button to manual update the roast schedule - increases maximum frequency of schedule updates - improves full redraw performance - fixes delayed closing of the Roast Properties dialog on some configurations
1 parent e2caa9b commit 7a0027d

File tree

15 files changed

+696
-397
lines changed

15 files changed

+696
-397
lines changed

src/artisanlib/acaia.py

Lines changed: 92 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,25 @@ class SCALE_CLASS(IntEnum):
4545
# split messages between several (mostly 2) payloads
4646
MODERN = 2 # eg. Lunar, Lunar 2021, Pearl, Pearl 2021, Pearl S
4747
# report weight in unit indicated by byte 2 of STATUS_A message
48-
RELAY = 3 # relaying scales without display eg. Umbra, Cosmo, ..
48+
RELAY = 3 # relaying scales without display eg. Umbra, ..
4949
# report weight always in g; byte 2 of STATUS_A message reports auto off timer
5050

5151
@unique
5252
class UNIT(IntEnum):
5353
KG = 1
5454
G = 2
55-
G_FIXED = 4
5655
OZ = 5
5756

5857
@unique
5958
class AUTO_OFF_TIMER(IntEnum):
6059
AUTO_SLEEP_OFF = 1
6160
AUTO_SLEEP_1MIN = 2
6261
AUTO_SLEEP_5MIN = 3
63-
AUTO_SLEEP_30MIN = 4
64-
AUTO_OFF_5MIN = 5
65-
AUTO_OFF_10MIN = 6
66-
AUTO_OFF_30MIN = 7
62+
AUTO_SLEEP_15MIN = 4
63+
AUTO_SLEEP_30MIN = 5
64+
AUTO_OFF_5MIN = 6
65+
AUTO_OFF_10MIN = 7
66+
AUTO_OFF_30MIN = 8
6767

6868
@unique
6969
class KEY_INFO(IntEnum):
@@ -192,7 +192,7 @@ class AcaiaBLE(QObject, ClientBLE): # pyright: ignore [reportGeneralTypeIssues]
192192
HEADER1:Final[bytes] = b'\xef'
193193
HEADER2:Final[bytes] = b'\xdd'
194194

195-
HEARTBEAT_FREQUENCY = 3 # every 3 sec send the heartbeat
195+
HEARTBEAT_FREQUENCY = 5 # every 5 sec send the heartbeat
196196

197197

198198
# NOTE: __slots__ are incompatible with multiple inheritance mixings in subclasses (as done below in class Acaia with QObject)
@@ -230,7 +230,7 @@ def __init__(self, connected_handler:Optional[Callable[[], None]] = None,
230230
###
231231

232232
# configure heartbeat
233-
self.set_heartbeat(self.HEARTBEAT_FREQUENCY) # send keep-alive heartbeat all 3-5sec; seems not to be needed any longer after sending ID on newer firmware versions!?
233+
self.set_heartbeat(self.HEARTBEAT_FREQUENCY) # send keep-alive heartbeat all 5sec; only for LEGACY scales
234234

235235
# register Acaia Legacy UUIDs
236236
for legacy_name in (ACAIA_LEGACY_LUNAR_NAME, ACAIA_LEGACY_PEARL_NAME):
@@ -277,12 +277,15 @@ def on_connect(self) -> None:
277277
if connected_service_UUID == ACAIA_LEGACY_SERVICE_UUID:
278278
_log.debug('connected to Acaia Legacy Scale')
279279
self.scale_class = SCALE_CLASS.LEGACY
280+
self.set_heartbeat(self.HEARTBEAT_FREQUENCY) # enable heartbeat
280281
elif connected_service_UUID == ACAIA_UMBRA_SERVICE_UUID:
281282
_log.debug('connected to Acaia Relay Scale')
282283
self.scale_class = SCALE_CLASS.RELAY
284+
self.set_heartbeat(0) # disable heartbeat
283285
else: #connected_service_UUID == ACAIA_SERVICE_UUID:
284286
_log.debug('connected to Acaia Scale')
285287
self.scale_class = SCALE_CLASS.MODERN
288+
self.set_heartbeat(0) # disable heartbeat
286289
if self._connected_handler is not None:
287290
self._connected_handler()
288291

@@ -298,22 +301,13 @@ def on_disconnect(self) -> None:
298301
def parse_info(self, data:bytes) -> None:
299302
_log.debug('INFO MSG')
300303

301-
# if len(data)>1:
302-
# print(data[1])
303-
# if len(data)>2:
304-
# print(data[2])
305-
306304
if len(data)>5:
307305
self.firmware = (data[3],data[4],data[5])
308306
_log.debug('firmware: %s.%s.%s', self.firmware[0], self.firmware[1], f'{self.firmware[2]:>03}')
309307

310-
# passwd_set
311-
# if len(data)>6:
312-
# print(data[6])
313-
314308
def decode_weight(self, payload:bytes) -> Optional[float]:
315309
try:
316-
#big_endian = (payload[5] & 0x08) == 0x08
310+
#big_endian = (payload[5] & 0x08) == 0x08 # bit 3 of byte 5 is set if weight is in big endian
317311
big_endian = self.scale_class == SCALE_CLASS.RELAY
318312

319313
value:float = 0
@@ -449,7 +443,6 @@ def parse_key_event(self, payload:bytes) -> int:
449443
if len(payload) < EVENT_LEN.KEY:
450444
return -1
451445
_log.debug('KEY EVENT')
452-
_log.debug('PRINT payload: %s', payload)
453446

454447
if payload[0] == KEY_INFO.TARE:
455448
_log.debug('tare key')
@@ -505,21 +498,28 @@ def parse_scale_events(self, payload:bytes) -> None:
505498
def parse_status(self, payload:bytes) -> None:
506499
_log.debug('STATUS')
507500

508-
# battery level (7 bits of second byte) + TIMER_START (1bit)
509-
if payload and len(payload) > 0:
501+
# byte 0: message len
502+
503+
# byte 1: battery level (7 bits of second byte) + TIMER_START (1bit)
504+
if payload and len(payload) > 1:
510505
self.battery = int(payload[1] & ~(1 << 7))
511506
self.battery_changed(self.battery)
512507
_log.debug('battery: %s%%', self.battery)
513-
# unit (7 bits of third byte) + CD_START (1bit)
514-
if payload and len(payload) > 1:
508+
509+
# byte 2:
510+
if payload and len(payload) > 2:
515511
if self.scale_class == SCALE_CLASS.RELAY:
512+
# relay scales: auto off setting for
516513
auto_off = int(payload[2] & 0xFF)
517514
if auto_off == 0:
518515
self.auto_off_timer = AUTO_OFF_TIMER.AUTO_SLEEP_OFF
519516
_log.debug('AUTO OFF TIMER: Auto Sleep Off')
520517
elif auto_off == 1:
521518
self.auto_off_timer = AUTO_OFF_TIMER.AUTO_SLEEP_5MIN
522519
_log.debug('AUTO OFF TIMER: AutoSleep 5 min')
520+
elif auto_off == 2:
521+
self.auto_off_timer = AUTO_OFF_TIMER.AUTO_SLEEP_15MIN
522+
_log.debug('AUTO OFF TIMER: AutoSleep 15 min')
523523
elif auto_off == 3:
524524
self.auto_off_timer = AUTO_OFF_TIMER.AUTO_SLEEP_30MIN
525525
_log.debug('AUTO OFF TIMER: AutoSleep 30 min')
@@ -536,18 +536,74 @@ def parse_status(self, payload:bytes) -> None:
536536
self.auto_off_timer = AUTO_OFF_TIMER.AUTO_SLEEP_1MIN
537537
_log.debug('AUTO OFF TIMER: AutoSleep 1 min')
538538
else:
539+
# display scales: weight unit (7 bits of third byte) + CD_START (1bit)
539540
self.unit = int(payload[2] & 0x7F)
540-
_log.debug('unit: %s', self.unit)
541-
542-
# mode (7 bits of third byte) + tare (1bit)
543-
# sleep (4th byte), 0:off, 1:5sec, 2:10sec, 3:20sec, 4:30sec, 5:60sec
544-
# key disabled (5th byte), touch key setting 0: off , 1: on
545-
# sound (6th byte), beep setting 0 : off 1: on
546-
# resolution (7th byte), 0 : default, 1 : high
547-
# max weight (8th byte)
541+
_log.debug('unit: %s (%s)', self.unit, ('g' if self.unit == UNIT.G else 'oz'))
542+
543+
# byte 3:
544+
if payload and len(payload) > 3:
545+
if self.scale_class == SCALE_CLASS.RELAY:
546+
# relay scales: beep setting (0:off, 1:on)
547+
# display scales: beep setting (0:off, 1:on)
548+
_log.debug('sound: %s', ('on' if payload[6] else 'off'))
549+
else:
550+
# display scales: mode (7 bits of third byte) + tare (1bit)
551+
mode = int(payload[3] & 0x7F)
552+
_log.debug('mode: %s', mode)
553+
_log.debug('tare: %s', (payload[3] & 0x80) == 0x80)
554+
# Lunar
555+
# 2: NodE_1 Weighing Mode
556+
# 4: NodE_2 Dual Display Mode
557+
# 3: NodE_3 Timer Starts with Flow Mode (drop)
558+
# 15: NodE_4 Auto-Tare Timer Starts with Flow Mode (drop/square)
559+
# 16: NodE_5 Auto-Tare Auto-Start Timer Mode (triangle/square)
560+
# 17: NodE_6 Auto-Tare Mode (square)
561+
562+
# byte 4:
563+
if payload and len(payload) > 4:
564+
if self.scale_class == SCALE_CLASS.RELAY:
565+
# relay scales: weight unit setting (0:g, 1:oz)
566+
self.unit = (UNIT.OZ if payload[4] == 1 else UNIT.G)
567+
_log.debug('unit: %s (%s)', self.unit, ('g' if self.unit == UNIT.G else 'oz'))
568+
else:
569+
sleep_modes = {0:'off', 1:'5sec', 2:'10sec', 3:'20sec', 4:'30sec', 5:'60sec'}
570+
_log.debug('sleep: %s%s', payload[4], (f' ({sleep_modes[payload[4]]})' if (payload[4] in sleep_modes) else ''))
571+
572+
# byte 5:
573+
if payload and len(payload) > 5:
574+
if self.scale_class == SCALE_CLASS.RELAY:
575+
# relay scales: resolution setting
576+
_log.debug('resolution: %s', ('0.01g' if payload[5] else '0.1g')) # resolution/readability: 0.1g / 0.01g
577+
else:
578+
# display scales: key disabled (0: off , 1: on)
579+
_log.debug('keys disabled: %s', ('on' if payload[5] else 'off'))
580+
581+
# byte 6:
582+
if payload and len(payload) > 6:
583+
if self.scale_class == SCALE_CLASS.RELAY:
584+
# relay scales: magic relay sensing (low/normal/high)
585+
_log.debug('magic relay sensing: %s', payload[6])
586+
# 0: low, 1: normal, 2: high
587+
else:
588+
# display scales: beep setting (0:off, 1:on)
589+
_log.debug('sound: %s', ('on' if payload[6] else 'off'))
590+
591+
# byte 7:
548592
if payload and len(payload) > 7:
549-
self.max_weight = (payload[7] + 1) * 1000
550-
_log.debug('max_weight: %s', self.max_weight)
593+
if self.scale_class == SCALE_CLASS.RELAY:
594+
# relay scales: magic relay beep
595+
_log.debug('magic relay beep: %s', ('on' if payload[7] else 'off'))
596+
else:
597+
# display scales: resolution (0:default, 1:high)
598+
_log.debug('resolution: %s', ('high' if payload[7] else 'default'))
599+
600+
# byte 8/8/10:
601+
if payload and len(payload) > 10 and self.scale_class == SCALE_CLASS.RELAY:
602+
# firmware version
603+
firmware = (payload[8],payload[9],payload[10])
604+
_log.debug('firmware: %s.%s.%s', firmware[0], firmware[1], firmware[2])
605+
606+
# bytes 11 & 12 reserved
551607

552608

553609
def parse_data(self, msg_type:int, data:bytes) -> None:
@@ -562,6 +618,7 @@ def parse_data(self, msg_type:int, data:bytes) -> None:
562618
if self.id_sent and not self.fast_notifications_sent:
563619
# we configure the scale to receive the initial
564620
# weight notification as fast as possible
621+
# Note: this event is needed to have the connected scale start to send weight messages even on relay scales which ignore the settings
565622
self.fast_notifications()
566623

567624
if not self.id_sent:
@@ -640,7 +697,7 @@ def send_ID(self) -> None:
640697
# self.send_message(MSG.IDENTIFY,b'\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x30\x31\x32\x33\x34')
641698
self.id_sent = True
642699

643-
# configure notifications
700+
# NOTE: notifications configuration not supported by Umbra and newer scales!
644701

645702
def slow_notifications(self) -> None:
646703
_log.debug('slow notifications')
@@ -703,9 +760,7 @@ async def reader(self, stream:IteratorReader) -> None:
703760
cmd = int.from_bytes(await stream.readexactly(1), 'big')
704761
if cmd in {CMD.SYSTEM_SA, CMD.INFO_A, CMD.STATUS_A, CMD.EVENT_SA}:
705762
dl = await stream.readexactly(1)
706-
data_len:int = min(20, int.from_bytes(dl, 'big'))
707-
# if cmd == CMD.STATUS_A: # all others are of variable length; STATUS_A with maxlen=16!?
708-
# data_len = min(data_len,16)
763+
data_len:int = int.from_bytes(dl, 'big')
709764
data = await stream.readexactly(data_len - 1)
710765
crc = await stream.readexactly(2)
711766
data = dl+data

src/artisanlib/ble_port.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ 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=4,
164+
scan_timeout:float=6,
165165
connect_timeout:float=6,
166166
address:Optional[str] = None # if given, connect only to the device with this ble address
167167
) -> Tuple[Optional[BleakClient], Optional[str]]:
@@ -243,7 +243,7 @@ def __init__(self) -> None:
243243
self._notifications:Dict[str, Callable[[BleakGATTCharacteristic, bytearray], None]] = {}
244244
self._writers:Dict[str, List[str]] = {} # associates a service UUID with a list of write characteristics
245245
self._readers:Dict[str, List[str]] = {} # associates a service UUID with a list of read characteristics
246-
self._heartbeat_frequency : float = 0 # heartbeat frequency in seconds; heartbeat ends if not positive and >0
246+
self._heartbeat_frequency : float = 0 # heartbeat frequency in seconds; heartbeat ends if not >0
247247
self._logging = False # if True device communication is logged
248248

249249

@@ -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=3, connect_timeout:float=6, address:Optional[str] = None) -> None:
287+
async def _connect(self, case_sensitive:bool=True, scan_timeout:float=6, connect_timeout:float=6, address:Optional[str] = None) -> None:
288288
blacklist:Set[str] = set()
289289
while self._running:
290290
# scan and connect
@@ -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=4, connect_timeout:float=6, address:Optional[str] = None) -> None:
397+
def start(self, case_sensitive:bool=True, scan_timeout:float=6, connect_timeout:float=6, address:Optional[str] = None) -> None:
398398
_log.debug('start')
399399
if self._running:
400400
_log.error('BLE client already running')

0 commit comments

Comments
 (0)