diff --git a/src/artisanlib/ble_port.py b/src/artisanlib/ble_port.py index 17349e3e6..c515979e2 100644 --- a/src/artisanlib/ble_port.py +++ b/src/artisanlib/ble_port.py @@ -54,19 +54,22 @@ def terminate_scan(self) -> None: self._terminate_scan_event.set() # returns True if the given device name matches with the devices name or the local_name of the advertisement + # returns True also in case the local_name is None, but the given service_uuid is within ad.service_uuids @staticmethod - def name_match(bd:'BLEDevice', ad:'AdvertisementData', device_name:str, case_sensitive:bool) -> bool: + def name_match(bd:'BLEDevice', ad:'AdvertisementData', device_name:str, case_sensitive:bool, service_uuid:Optional[str]) -> bool: return ((bd.name is not None and (bd.name.startswith(device_name) if case_sensitive else bd.name.casefold().startswith(device_name.casefold()))) or (ad.local_name is not None and (ad.local_name.startswith(device_name) if - case_sensitive else ad.local_name.casefold().startswith(device_name.casefold())))) + case_sensitive else ad.local_name.casefold().startswith(device_name.casefold()))) or + (ad.local_name is None and service_uuid is not None and + service_uuid.casefold() in (uuid.casefold() for uuid in ad.service_uuids))) # matches the discovered BLEDevice and AdvertisementData # returns True on success and False otherwise, as well as the service_uuid str of the matching device_description def description_match(self, bd:'BLEDevice', ad:'AdvertisementData', device_descriptions:Dict[Optional[str],Optional[Set[str]]], case_sensitive:bool) -> Tuple[bool, Optional[str]]: for service_uuid, device_names in device_descriptions.items(): - if device_names is None or any(self.name_match(bd,ad,device_name,case_sensitive) for device_name in device_names): + if device_names is None or any(self.name_match(bd,ad,device_name,case_sensitive, service_uuid) for device_name in device_names): return True, service_uuid return False, None @@ -84,7 +87,7 @@ async def _scan(self, async for bd, ad in scanner.advertisement_data(): if self._terminate_scan_event.is_set(): return None, None - #_log.debug("device %s, (%s): %s", bd.name, ad.local_name, ad.service_uuids) +# _log.debug("device %s, (%s): %s", bd.name, ad.local_name, ad.service_uuids) if bd.address not in blacklist: res:bool res_service_uuid:Optional[str] diff --git a/src/artisanlib/colortrack.py b/src/artisanlib/colortrack.py index 29ba2e481..17293f46c 100644 --- a/src/artisanlib/colortrack.py +++ b/src/artisanlib/colortrack.py @@ -95,6 +95,7 @@ async def read_msg(self, stream: asyncio.StreamReader) -> None: _log.error(e) + class ColorTrackBLE(ClientBLE): # ColorTrack RT service and characteristics UUIDs @@ -103,32 +104,9 @@ class ColorTrackBLE(ClientBLE): COLORTRACK_READ_NOTIFY_LASER_UUID:Final[str] = '713D0002-503E-4C75-BA94-3148F18D9410' # Laser Measurements COLORTRACK_TEMP_HUM_READ_NOTIFY_UUID:Final[str] = '713D0004-503E-4C75-BA94-3148F18D9410' # Temperature and Humidity - # maps bytes received via BLE to color values [100-0] - COLOR_MAP = [ 0,0,0.01,0.02,0.04,0.06,0.08,0.11,0.15,0.19,0.23, - 0.28,0.33,0.39,0.45,0.51,0.58,0.66,0.73,0.82,0.9, - 1.0,1.09,1.19,1.3,1.41,1.52,1.64,1.77,1.89,2.03, - 2.16,2.3,2.45,2.6,2.75,2.91,3.08,3.25,3.42,3.59, - 3.78,3.96,4.15,4.35,4.55,4.75,4.96,5.17,5.39,5.61, - 5.83,6.07,6.3,6.54,6.78,7.03,7.28,7.54,7.8,8.07, - 8.34,8.61,8.89,9.18,9.47,9.76,10.06,10.36,10.66,10.98, - 11.29,11.61,11.93,12.26,12.6,12.93,13.28,13.62,13.97,14.33, - 14.69,15.05,15.42,15.79,16.17,16.55,16.94,17.33,17.73,18.13, - 18.53,18.94,19.35,19.77,20.19,20.62,21.05,21.49,21.93,22.37, - 22.82,23.27,23.73,24.19,24.66,25.31,25.61,26.09,26.57,27.06, - 27.56,28.05,28.56,29.06,29.58,30.09,30.61,31.14,31.67,32.2, - 32.74,33.28,33.83,34.38,34.94,35.5,36.06,36.63,37.21,37.78, - 38.37,38.95,39.55,40.14,40.74,41.35,41.96,42.57,43.19,43.81, - 44.44,45.07,45.71,46.35,47,47.65,48.3,48.96,49.62,50.29, - 50.96,51.64,52.32,53.0,53.69,54.39,55.09,55.79,56.5,57.21, - 57.93,58.65,59.38,60.11,60.84,61.58,62.32,63.07,63.82,64.58, - 65.34,66.11,66.88,67.65,68.43,69.22,70,70.8,71.59,72.39, - 73.2,74.01,74.83,75.65,76.47,77.3,78.13,78.97,79.82,80.65, - 81.51,82.36,83.22,84.08,84.95,85.83,86.7,87.58,88.47,89.36, - 90.26,91.16,92.06,92.97,93.88,94.8,95.72,96.65,97.58,98.51, - 99.45,100 ] def __init__(self, connected_handler:Optional[Callable[[], None]] = None, - disconnected_handler:Optional[Callable[[], None]] = None): + disconnected_handler:Optional[Callable[[], None]] = None): super().__init__() # handlers @@ -138,68 +116,54 @@ def __init__(self, connected_handler:Optional[Callable[[], None]] = None, self.add_device_description(self.COLORTRACK_SERVICE_UUID, self.COLORTRACK_NAME) self.add_notify(self.COLORTRACK_READ_NOTIFY_LASER_UUID, self.notify_laser_callback) self.add_read(self.COLORTRACK_SERVICE_UUID, self.COLORTRACK_READ_NOTIFY_LASER_UUID) - self.add_notify(self.COLORTRACK_TEMP_HUM_READ_NOTIFY_UUID, self.notify_temp_hum_callback) +# self.add_notify(self.COLORTRACK_TEMP_HUM_READ_NOTIFY_UUID, self.notify_temp_hum_callback) # no callbacks received!? - # weights for averaging (length 40) # ColorTrack sends about 65 laser readings per second - self._weights:Final[npt.NDArray[np.float64]] = np.array(range(1, 40)) + # weights for averaging (length 20) # ColorTrack sends about 65 laser readings per second + self._weights:Final[npt.NDArray[np.float64]] = np.array(range(1, 20)) # received but not yet consumed readings (mapped) self._received_readings:npt.NDArray[np.float64] = np.array([]) - self._received_raw_readings:npt.NDArray[np.float64] = np.array([]) - def map_reading(self, r:int) -> float: - if 0 <= r < 213: - return self.COLOR_MAP[r] - return 0 + + # maps received byte decimal value to ColorTrack readings in range 0-100 + @staticmethod + def map_reading(x:int) -> float: + return min(100, max(0, 3.402e-8 *x*x + 1.028e-6 * x - 0.0069)) + + # converts pairs of bytes of paloads to readings + def register_readings(self, payload:bytearray) -> None: + for i in range(0, len(payload), 2): + if len(payload)>i+1: + self.register_reading(self.map_reading(payload[i]*256 + payload[i+1])) def register_reading(self, value:float) -> None: self._received_readings = np.append(self._received_readings, value) if self._logging: _log.info('register_reading: %s',value) - def register_raw_reading(self, value:float) -> None: - self._received_raw_readings = np.append(self._received_raw_readings, value) + # second result is the raw average def getColor(self) -> Tuple[float, float]: - read_res = self.read(self.COLORTRACK_READ_NOTIFY_LASER_UUID) - _log.info('PRINT getLaser: %s',read_res) +# read_res = self.read(self.COLORTRACK_READ_NOTIFY_LASER_UUID) # returns 20 bytes (same format as via the notifications) +# _log.info('PRINT getLaser: %s',read_res) + try: number_of_readings = len(self._received_readings) - number_of_raw_readings = len(self._received_raw_readings) if number_of_readings == 1: - return float(self._received_readings[0]), (float(self._received_raw_readings[0]) if number_of_raw_readings==1 else -1) + return float(self._received_readings[0]), float(self._received_readings[0]) if number_of_readings > 1: # consume and average the readings l = min(len(self._weights), number_of_readings) res:float = float(np.average(self._received_readings[-l:], weights=self._weights[-l:])) self._received_readings = np.array([]) - # raw - l_raw = min(len(self._weights), number_of_raw_readings) - res_raw:float = float(np.average(self._received_raw_readings[-l_raw:], weights=self._weights[-l_raw:])) - self._received_raw_readings = np.array([]) - return res, res_raw + return res, res except Exception as e: # pylint: disable=broad-except _log.exception(e) return -1, -1 - # every second reading is the sync character 36 - # returns readings as bytes extracted from the given payload. If the payload is not valid the result is empty. - @staticmethod - def validate_data(payload:bytearray) -> bytearray: - if len(payload) % 2 == 0: - payload_even = payload[::2] - payload_odd = payload[1::2] - if all(d == 36 for d in payload_even): - return payload_odd - if all(d == 36 for d in payload_odd): - return payload_even - return bytearray() - def notify_laser_callback(self, _sender:'BleakGATTCharacteristic', payload:bytearray) -> None: - for r in self.validate_data(payload): - self.register_reading(self.map_reading(r)) - self.register_raw_reading(r) + self.register_readings(payload) @staticmethod def notify_temp_hum_callback(_sender:'BleakGATTCharacteristic', payload:bytearray) -> None: diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index 0d3438c16..6c59eca51 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -1,7 +1,7 @@ types-openpyxl>=3.1.5.20241025 types-Pillow>=10.2.0.20240822 types-protobuf>=5.28.3.20241030 -types-psutil>=6.1.0.20241022 +types-psutil>=6.1.0.20241102 types-pyserial>=3.5.0.20240826 types-python-dateutil==2.9.0.20241003 types-pytz>=2024.2.0.20241003 @@ -13,7 +13,7 @@ types-docutils>=0.21.0.20241005 lxml-stubs>=0.5.1 mypy==1.13.0 pyright==1.1.387 -ruff>=0.7.1 +ruff>=0.7.2 pylint==3.3.1 pre-commit>=4.0.1 pytest>=8.3.3 @@ -25,7 +25,7 @@ pytest-cov==5.0.0 #pytest-bdd==6.1.1 #pytest-benchmark==4.0.0 #pytest-mock==3.11.1 -hypothesis>=6.115.6 +hypothesis>=6.116.0 coverage>=7.6.4 coverage-badge==1.1.2 codespell==2.3.0 diff --git a/src/requirements.txt b/src/requirements.txt index c723d6c91..b23bed0f4 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -32,7 +32,7 @@ pyserial==3.5 pymodbus==3.6.9; python_version < '3.9' # last Python 3.8 release pymodbus==3.7.4; python_version >= '3.9' python-snap7==1.3; python_version < '3.10' # last Python 3.9 release -python-snap7==2.0.0; python_version >= '3.10' +python-snap7==2.0.2; python_version >= '3.10' Phidget22==1.20.20240911 Unidecode==1.3.8 qrcode==7.4.2; python_version < '3.9' # last Python 3.8 release @@ -49,7 +49,7 @@ psutil==6.1.0 typing-extensions==4.10.0; python_version < '3.8' # required for supporting Final and TypeDict on Python <3.8 protobuf==5.28.3 numpy==1.24.3; python_version < '3.9' # last Python 3.8 release -numpy==2.1.2; python_version >= '3.9' +numpy==2.1.3; python_version >= '3.9' scipy==1.10.1; python_version < '3.9' # last Python 3.8 release scipy==1.14.1; python_version >= '3.9' wquantiles==0.6