Skip to content

Commit

Permalink
- adds ColorTrack connect/disconnect messages
Browse files Browse the repository at this point in the history
- reworks the ColorTrack data mapping
- removes ColorTrack debug code
- matches BLE on advertised service UUID if advertisements local_name is (still) unknown
- package updates
  • Loading branch information
MAKOMO committed Nov 3, 2024
1 parent 89432dc commit e8f37f2
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 68 deletions.
11 changes: 7 additions & 4 deletions src/artisanlib/ble_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
Expand Down
82 changes: 23 additions & 59 deletions src/artisanlib/colortrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions src/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit e8f37f2

Please sign in to comment.