diff --git a/src/adapters/ftmsrower/ftmsrowerreader.py b/src/adapters/ftmsrower/ftmsrowerreader.py new file mode 100755 index 0000000..bab8224 --- /dev/null +++ b/src/adapters/ftmsrower/ftmsrowerreader.py @@ -0,0 +1,144 @@ +import gatt +import logging +import threading +from time import time + +logger = logging.getLogger(__name__) + +#This SDK requires you to create subclasses of gatt.DeviceManager and gatt.Device. The other two classes gatt.Service and gatt.Characteristic are not supposed to be subclassed. + +#The SDK entry point is the DeviceManager class. Check the following example to dicover any Bluetooth Low Energy device nearby. + + +class FTMS_Rower(gatt.Device): + + SERVICE_UUID_FTMSROWER = "00001826-0000-1000-8000-00805f9b34fb" + CHARACTERISTIC_UUID_ROWWRITE = "00002ad9-0000-1000-8000-00805f9b34fb" + CHARACTERISTIC_UUID_ROWDATA = "00002ad1-0000-1000-8000-00805f9b34fb" + + def __init__(self, mac_address, manager): + super().__init__(mac_address=mac_address, manager=manager) + self._callbacks = set() + self.lock = threading.Lock() + self.is_connected = False + + def ready(self): + with self.lock: #"Lock Acquired" + return self.is_connected + + def connect_succeeded(self): + super().connect_succeeded() + logger.info("Connected to [{}]".format(self.mac_address)) + + + def connect_failed(self, error): + super().connect_failed(error) + logger.info("Connection failed [{}]: {}".format(self.mac_address, error)) + + def disconnect_succeeded(self): + super().disconnect_succeeded() + logger.info("Disconnected [{}]".format(self.mac_address)) + #self.connect() + + def find_service(self, uuid): + for service in self.services: + if service.uuid == uuid: + return service + + return None + + def find_characteristic(self, service, uuid): + for chrstc in service.characteristics: + if chrstc.uuid == uuid: + return chrstc + + return None + + def services_resolved(self): + super().services_resolved() + + logger.info("Resolved services [{}]".format(self.mac_address)) + for service in self.services: + logger.info("\t[{}] Service [{}]".format(self.mac_address, service.uuid)) + for characteristic in service.characteristics: + logger.info("\t\tCharacteristic [{}]".format(characteristic.uuid)) + + self.serviceFTMS_Rower = self.find_service(self.SERVICE_UUID_FTMSROWER) + self.chrstcRowData = self.find_characteristic(self.serviceFTMS_Rower, self.CHARACTERISTIC_UUID_ROWDATA) + self.chrstcRowData.enable_notifications() + + self.chrstcRowWrite = self.find_characteristic(self.serviceFTMS_Rower, self.CHARACTERISTIC_UUID_ROWWRITE) + with self.lock: #"Lock Acquired" + self.is_connected = True + + def characteristic_value_updated(self, characteristic, value): + super().characteristic_value_updated(characteristic, value) + #self.buffer = value.decode() + self.notify_callbacks(value) + + + def characteristic_write_value(self, value): + self.writing = value + #print(value) + self.chrstcRowWrite.write_value(value) + + def register_callback(self, cb): + self._callbacks.add(cb) + + def remove_callback(self, cb): + self._callbacks.remove(cb) + + def notify_callbacks(self, event): + for cb in self._callbacks: + cb(event) + +class FTMS_Row_Manager(gatt.DeviceManager): + def __init__(self,*args,**kwargs): + gatt.DeviceManager.__init__(self, *args, **kwargs) + self.lock = threading.Lock() + self.discovered=False + + def ready(self): + with self.lock: + return self.discovered + + def device_discovered(self, device): + # Better to allow an argument for the FTMS device alias? + if device.alias() == "CR 71": + logging.info("found FTMS Rower") + logging.info(device.mac_address) + self.ftmsrowmac = device.mac_address + self.stop() + with self.lock: #"Lock Acquired" + self.discovered=True + + +def connecttoftmsrow(): + manager = FTMS_Row_Manager(adapter_name='hci0') + logger.info("starting discovery") + manager.start_discovery() # from the DeviceManager class call the methode start_discorvery + manager.run() + while not manager.ready(): # hold the thread locked a checks if FTMS Rower has been found. Then gives other process 0.2 sec time to work + time.sleep(0.2) + logger.info("found FTMS Row macaddress") + macaddressftmsrower = manager.ftmsrowmac + return macaddressftmsrower + + +if __name__ == '__main__': + + manager = gatt.DeviceManager(adapter_name='hci0') + device = FTMS_Rower(mac_address="", manager=manager) + device.connect() + + manager.run() + + # manager = FTMS_Row_ManagerRowManager(adapter_name='hci0') + # manager.start_discovery() + # try: + # manager.run() + # except KeyboardInterrupt: + # for device in manager.devices(): + # if device.is_connected(): + # device.disconnect() + # manager.stop() diff --git a/src/adapters/ftmsrower/ftmsrowtobleant.py b/src/adapters/ftmsrower/ftmsrowtobleant.py new file mode 100755 index 0000000..626295a --- /dev/null +++ b/src/adapters/ftmsrower/ftmsrowtobleant.py @@ -0,0 +1,184 @@ +import logging +import struct + +import gatt +import threading +from time import sleep +import time +from copy import deepcopy + +from . import ftmsrowerreader + +logger = logging.getLogger(__name__) + + +class DataLogger(): + # INDEXES are [index, length] + # referencing later by [index:index + length] notation + # All little endian? + # +- Strokes per 2 seconds + # | +--+- Total Strokes + # | | | +--+--+- Total Distance + # | | | | | | +--+- Instantaneous Pace + # | | | | | | | | +--+- Instantaneous Power (watts) + # | | | | | | | | | | +--+- Total Energy + # | | | | | | | | | | | | +--+- Energy_per_hour + # | | | | | | | | | | | | | | +- Energy Per Minute + # | | | | | | | | | | | | | | | +--+- Elapsed time + # | | | | | | | | | | | | | | | | | + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Index + # 2c-09-32-03-00-2b-00-00-95-00-83-00-09-00-f1-02-00-12-00 + INDEXES = { + "stroke_rate": [2, 1], # /2 for StrokesPerSecond + "total_strokes": [3, 2], # Total, 02 00 = 00 02 = 2 + "total_distance_m": [5, 3], # Total, Meters 1a 00 00 = 00 00 1a = 26 + "instantaneous pace": [8, 2], # Gauge, c4 00 = 00 c4 = 196 seconds per 500 meters + "watts": [10, 2], # Gauge, 50 00 = 00 50 = 80 watts (instantaneous_power) + "total_kcal": [12, 2], # Total, kCal 07 00 = 00 07 = 7 kCal + "total_kcal_hour": [14, 2], # Gauge? Total? Total if we continue for 1 hr? + "total_kcal_minute": [16, 1], # Gauge/Avg, once at least a minute is done + #"elapsed_time": [17, 3], # Total reported, but apparently we calculate it? + } + FTMS_EVENT = 44 + + def __init__(self, rower_interface): + self._rower_interface = rower_interface + self._rower_interface.register_callback(self.on_row_event) + + self.WRValues_rst = None + self.WRValues = None + self.WRValues_standstill = None + self.starttime = None + self.fullstop = None + self.ftmsHalt = None + + self._reset_state() + + def _reset_state(self): + self.WRValues_rst = { + 'stroke_rate': 0, + 'total_strokes': 0, + 'total_distance_m': 0, + 'instantaneous pace': 0, + 'speed': 0, + 'watts': 0, + 'total_kcal': 0, + 'total_kcal_hour': 0, + 'total_kcal_min': 0, + 'heart_rate': 0, + 'elapsedtime': 0.0, + 'work': 0, + 'stroke_length': 0, + 'force': 0, + 'watts_avg':0, + 'pace_avg':0 + } + self.WRValues = deepcopy(self.WRValues_rst) + self.WRValues_standstill = deepcopy(self.WRValues_rst) + self.starttime = None # time.time() # was None + self.fullstop = True + self.ftmsHalt = False + self.Initial_reset = False + + + def elapsedtime(self): + print(self.fullstop) + if self.fullstop == False: + elaspedtimecalc = int(time.time() - self.starttime) + self.WRValues.update({'elapsedtime':elaspedtimecalc}) + elif self.fullstop == True and self.WRValues.get('total_distance_m') !=0 and self.Initial_reset == True: + if not self.starttime: + self.starttime = time.time() + elaspedtimecalc = int(time.time() - self.starttime) + self.WRValues.update({'elapsedtime':elaspedtimecalc}) + else: + self.WRValues.update({'elapsedtime': 0}) + + def on_row_event(self, event): + if int(event[0]) == self.FTMS_EVENT: + oldWRValues = deepcopy(self.WRValues) + for message_type, indexes in self.INDEXES.items(): + i, l = indexes + # Get the data at the index(es), reverse the byte order + # Convert the bytestring to a hex string, then to a decimal integer + integer_value = int(event[i:i+l][::-1].hex(),16) + #if message_type = "stroke_rate": + # integer_value = int(integer_value / 2) # Handled in antfe.py + self.WRValues.update({message_type: integer_value}) + if message_type == "instantaneous pace" and integer_value > 0: + # pace is seconds per 500m + # We want cm/s? I guess? So 500 (meters) * 100 gives us CM + # Then divide by pace for cm/s + speed = 500 * 100 / integer_value + self.WRValues.update({'speed': speed}) + new_strokes = self.WRValues['total_strokes'] - oldWRValues['total_strokes'] + new_distance = self.WRValues['total_distance_m'] - oldWRValues['total_distance_m'] + if new_strokes > 0: + self.WRValues.update({'stroke_length': new_distance / new_strokes}) + self.elapsedtime() + + print(self.WRValues) + #print("| H|R| TS| TD| IP| Wa| TE| TH|M|time") + #print(event.hex()) + + +def connectFTMS(manager,ftms): + ftms.connect() + manager.run() + +def reset(ftms): + pass + ftms.characteristic_write_value(struct.pack("