Skip to content

Commit 28a9364

Browse files
committed
adds support for newer Santoker BLE BT/ET
1 parent 4561603 commit 28a9364

File tree

10 files changed

+399
-46
lines changed

10 files changed

+399
-46
lines changed

src/artisanlib/acaia.py

+29-13
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,12 @@
2424
from bleak.backends.characteristic import BleakGATTCharacteristic # pylint: disable=unused-import
2525

2626

27-
try:
28-
from PyQt6.QtCore import QObject, pyqtSignal # @UnusedImport @Reimport @UnresolvedImport
29-
except ImportError:
30-
from PyQt5.QtCore import QObject, pyqtSignal # type: ignore # @UnusedImport @Reimport @UnresolvedImport
31-
32-
3327
from artisanlib.ble_port import ClientBLE
3428
from artisanlib.async_comm import AsyncIterable, IteratorReader
29+
from artisanlib.scale import Scale
3530

3631

37-
_log = logging.getLogger(__name__)
32+
_log: Final[logging.Logger] = logging.getLogger(__name__)
3833

3934

4035
####
@@ -113,6 +108,19 @@ class ACAIA_TIMER(IntEnum):
113108
ACAIA_CINCO_NAME:Final[str] = 'CINCO' # Acaia Cinco
114109
ACAIA_PYXIS_NAME:Final[str] = 'PYXIS' # Acaia Pyxis
115110

111+
112+
# Acaia scale device name prefixes and product names
113+
ACAIA_SCALE_NAMES = [
114+
(ACAIA_LEGACY_LUNAR_NAME, 'Lunar'), # original Lunar
115+
(ACAIA_LUNAR_NAME, 'Lunar'), # Lunar 2021 and later
116+
(ACAIA_LEGACY_PEARL_NAME, 'Pearl'), # original Pearl
117+
(ACAIA_PEARLS_NAME, 'Pearl S'),
118+
(ACAIA_PEARL_NAME, 'Pearl'), # Pearl 2021
119+
(ACAIA_CINCO_NAME, 'Cinco'),
120+
(ACAIA_PYXIS_NAME, 'Pyxis')]
121+
122+
123+
116124
class AcaiaBLE(ClientBLE):
117125

118126

@@ -510,17 +518,25 @@ def battery_changed(self, new_value:int) -> None: # pylint: disable=no-self-use
510518

511519

512520
# QObject needs to go first in this mixing and AcaiaBLE and its super class are not allowed to hold __slots__
513-
class Acaia(QObject, AcaiaBLE): # pyright: ignore [reportGeneralTypeIssues] # Argument to class must be a base class
514-
515-
weight_changed_signal = pyqtSignal(int) # delivers new weight in g
516-
battery_changed_signal = pyqtSignal(int) # delivers new batter level in %
517-
disconnected_signal = pyqtSignal() # issued on disconnect
521+
class Acaia(Scale, AcaiaBLE): # pyright: ignore [reportGeneralTypeIssues] # Argument to class must be a base class
518522

519523
def __init__(self, connected_handler:Optional[Callable[[], None]] = None,
520524
disconnected_handler:Optional[Callable[[], None]] = None):
521-
QObject.__init__(self)
525+
Scale.__init__(self)
522526
AcaiaBLE.__init__(self, connected_handler = connected_handler, disconnected_handler=disconnected_handler)
527+
self.address:Optional[str] = None # if set, connects are restricted to the device with this BLE address
528+
529+
def set_address(self, address:Optional[str]) -> None:
530+
self.address = address
531+
532+
def get_address(self) -> Optional[str]:
533+
return self.address
534+
535+
def connect(self) -> None:
536+
self.start(address=self.address)
523537

538+
def disconnect(self) -> None:
539+
self.stop()
524540

525541
def weight_changed(self, new_value:int) -> None:
526542
self.weight_changed_signal.emit(new_value)

src/artisanlib/ble_port.py

+38-14
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ async def _scan(self,
7979
device_descriptions:Dict[Optional[str],Optional[Set[str]]],
8080
blacklist:Set[str],
8181
case_sensitive:bool,
82-
scan_timeout:float) -> 'Tuple[Optional[BLEDevice], Optional[str]]':
82+
scan_timeout:float,
83+
address:Optional[str]) -> 'Tuple[Optional[BLEDevice], Optional[str]]':
8384
try:
8485
async with asyncio.timeout(scan_timeout): # type:ignore[attr-defined]
8586
async with BleakScanner() as scanner:
@@ -88,7 +89,7 @@ async def _scan(self,
8889
if self._terminate_scan_event.is_set():
8990
return None, None
9091
# _log.debug("device %s, (%s): %s", bd.name, ad.local_name, ad.service_uuids)
91-
if bd.address not in blacklist:
92+
if bd.address not in blacklist and (address is None or bd.address == address):
9293
res:bool
9394
res_service_uuid:Optional[str]
9495
res, res_service_uuid = self.description_match(bd,ad,device_descriptions,case_sensitive)
@@ -106,14 +107,16 @@ async def _scan_and_connect(self,
106107
blacklist:Set[str], # client addresses to ignore
107108
case_sensitive:bool,
108109
disconnected_callback:Optional[Callable[[BleakClient], None]],
109-
scan_timeout:float, connect_timeout:float) -> Tuple[Optional[BleakClient], Optional[str]]:
110+
scan_timeout:float, connect_timeout:float,
111+
address:Optional[str] = None # if given, connect only to the device with this ble address
112+
) -> Tuple[Optional[BleakClient], Optional[str]]:
110113
async with self._scan_and_connect_lock:
111114
# the lock ensures that only one scan/connect operation is running at any time
112115
# as trying to establish a connection to two devices at the same time
113116
# can cause errors
114117
discovered_bd:Optional[BLEDevice] = None
115118
service_uuid:Optional[str] = None
116-
discovered_bd, service_uuid = await self._scan(device_descriptions, blacklist, case_sensitive, scan_timeout)
119+
discovered_bd, service_uuid = await self._scan(device_descriptions, blacklist, case_sensitive, scan_timeout, address)
117120
if discovered_bd is None:
118121
return None, None
119122
client = BleakClient(
@@ -159,7 +162,9 @@ def scan_and_connect(self,
159162
case_sensitive:bool=True,
160163
disconnected_callback:Optional[Callable[[BleakClient], None]] = None,
161164
scan_timeout:float=10,
162-
connect_timeout:float=3) -> Tuple[Optional[BleakClient], Optional[str]]:
165+
connect_timeout:float=3,
166+
address:Optional[str] = None # if given, connect only to the device with this ble address
167+
) -> Tuple[Optional[BleakClient], Optional[str]]:
163168
if hasattr(self, '_asyncLoopThread') and self._asyncLoopThread is None:
164169
self._asyncLoopThread = AsyncLoopThread()
165170
assert self._asyncLoopThread is not None
@@ -170,7 +175,8 @@ def scan_and_connect(self,
170175
case_sensitive,
171176
disconnected_callback,
172177
scan_timeout,
173-
connect_timeout),
178+
connect_timeout,
179+
address),
174180
self._asyncLoopThread.loop)
175181
try:
176182
return fut.result()
@@ -250,8 +256,9 @@ def start_notifications(self) -> None:
250256
try:
251257
ble.start_notify(self._ble_client, notify_uuid, callback)
252258
self._active_notification_uuids.add(notify_uuid)
259+
_log.debug('notification on characteristic %s started', notify_uuid)
253260
except BleakCharacteristicNotFoundError:
254-
_log.debug('start_notifications: characteristic {notify_uuid} not found')
261+
_log.debug('start_notifications: characteristic %s not found', notify_uuid)
255262

256263
# Notifications are stopped automatically on disconnect, so this method does not need to be called
257264
# unless notifications need to be stopped before the device disconnects
@@ -262,7 +269,7 @@ def stop_notifications(self) -> None:
262269
ble.stop_notify(self._ble_client, notify_uuid)
263270
_log.debug('notifications on %s stopped', notify_uuid)
264271
except BleakCharacteristicNotFoundError:
265-
_log.debug('start_notifications: characteristic {notify_uuid} not found')
272+
_log.debug('start_notifications: characteristic %s not found', notify_uuid)
266273
self._active_notification_uuids = set()
267274

268275
def _disconnect(self) -> None:
@@ -277,7 +284,7 @@ def connected(self) -> Optional[str]:
277284

278285

279286
# connect and re-connect while self._running to BLE
280-
async def _connect(self, case_sensitive:bool=True, scan_timeout:float=10, connect_timeout:float=4) -> None:
287+
async def _connect(self, case_sensitive:bool=True, scan_timeout:float=10, connect_timeout:float=4, address:Optional[str] = None) -> None:
281288
blacklist:Set[str] = set()
282289
while self._running:
283290
# scan and connect
@@ -290,7 +297,8 @@ async def _connect(self, case_sensitive:bool=True, scan_timeout:float=10, connec
290297
case_sensitive,
291298
self.disconnected_callback,
292299
scan_timeout,
293-
connect_timeout)
300+
connect_timeout,
301+
address)
294302
if service_uuid is not None and self._ble_client is not None and self._ble_client.is_connected:
295303
# validate correct service
296304
try:
@@ -380,13 +388,13 @@ async def _keep_alive(self) -> None:
380388
await asyncio.sleep(self._heartbeat_frequency)
381389
self.heartbeat()
382390

383-
async def _connect_and_keep_alive(self,case_sensitive:bool,scan_timeout:float, connect_timeout:float) -> None:
391+
async def _connect_and_keep_alive(self,case_sensitive:bool,scan_timeout:float, connect_timeout:float, address:Optional[str] = None) -> None:
384392
await asyncio.gather(
385-
self._connect(case_sensitive,scan_timeout,connect_timeout),
393+
self._connect(case_sensitive,scan_timeout,connect_timeout, address),
386394
self._keep_alive())
387395

388396

389-
def start(self, case_sensitive:bool=True, scan_timeout:float=10, connect_timeout:float=4) -> None:
397+
def start(self, case_sensitive:bool=True, scan_timeout:float=10, connect_timeout:float=4, address:Optional[str] = None) -> None:
390398
_log.debug('start')
391399
if self._running:
392400
_log.error('BLE client already running')
@@ -397,7 +405,7 @@ def start(self, case_sensitive:bool=True, scan_timeout:float=10, connect_timeout
397405
self._async_loop_thread = AsyncLoopThread()
398406
# run _connect in async loop
399407
asyncio.run_coroutine_threadsafe(
400-
self._connect_and_keep_alive(case_sensitive, scan_timeout, connect_timeout),
408+
self._connect_and_keep_alive(case_sensitive, scan_timeout, connect_timeout, address),
401409
self._async_loop_thread.loop)
402410
_log.debug('BLE client started')
403411
self.on_start()
@@ -480,3 +488,19 @@ def on_stop(self) -> None: # pylint: disable=no-self-use
480488
...
481489
def heartbeat(self) -> None: # pylint: disable=no-self-use
482490
...
491+
492+
493+
##
494+
495+
# scans for named BLE devices providing any of the provided servie_uuids
496+
# returns a list of triples (name, address, Optional[BLEDevice])
497+
def scan_ble(timeout: float = 3.0) -> 'List[Tuple[BLEDevice, AdvertisementData]]':
498+
coro = BleakScanner.discover(
499+
timeout=timeout,
500+
return_adv=True)
501+
try:
502+
loop = asyncio.get_running_loop()
503+
res = asyncio.run_coroutine_threadsafe(coro, loop).result()
504+
except RuntimeError:
505+
res = asyncio.run(coro)
506+
return list(res.values())

src/artisanlib/canvas.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5993,7 +5993,7 @@ def playbackevent(self) -> None:
59935993
# temperatures which are assumed to increase
59945994
end_reached[event_type] = True
59955995

5996-
if (event_type not in slider_events and # only if the is no slider event of the corresponding type
5996+
if (event_type not in slider_events and # only if there is no slider event of the corresponding type
59975997
self.specialeventplayback[event_type] and # only replay event types activated for replay
59985998
(str(self.etypesf(event_type) == str(self.Betypesf(event_type)))) and
59995999
#self.aw.eventslidervisibilities[event_type] and

0 commit comments

Comments
 (0)