diff --git a/MetaWear-SDK-Cpp b/MetaWear-SDK-Cpp index 5bbf7be..d679878 160000 --- a/MetaWear-SDK-Cpp +++ b/MetaWear-SDK-Cpp @@ -1 +1 @@ -Subproject commit 5bbf7be238dd16a8a04a9fb9f9c3010e8268c23d +Subproject commit d679878b8f7f88471697491f352469c391c7f27f diff --git a/examples/anonymous_datasignals.py b/examples/anonymous_datasignals.py index f15c595..c191b53 100644 --- a/examples/anonymous_datasignals.py +++ b/examples/anonymous_datasignals.py @@ -15,27 +15,24 @@ metawear.connect() print("Connected") -sync_event = Event() +e = Event() result = {} -def handler(board, signals, len): +def handler(ctx, board, signals, len): result['length'] = len result['signals'] = cast(signals, POINTER(c_void_p * len)) if signals is not None else None - sync_event.set() -handler_fn = FnVoid_VoidP_VoidP_UInt(handler) + e.set() +handler_fn = FnVoid_VoidP_VoidP_VoidP_UInt(handler) class DataHandler: def __init__(self, signal): - raw = libmetawear.mbl_mw_anonymous_datasignal_get_identifier(signal) - self.identifier = cast(raw, c_char_p).value.decode("ascii") - self.data_handler_fn = FnVoid_DataP(lambda ptr: print({"identifier": self.identifier, "epoch": ptr.contents.epoch, "value": parse_value(ptr)})) - - libmetawear.mbl_mw_memory_free(raw) + self.identifier = libmetawear.mbl_mw_anonymous_datasignal_get_identifier(signal) + self.data_handler_fn = FnVoid_VoidP_DataP(lambda ctx, ptr: print({"identifier": self.identifier, "epoch": ptr.contents.epoch, "value": parse_value(ptr)})) print("Creating anonymous signals") libmetawear.mbl_mw_settings_set_connection_parameters(metawear.board, 7.5, 7.5, 0, 6000) sleep(1.0) -libmetawear.mbl_mw_metawearboard_create_anonymous_datasignals(metawear.board, handler_fn) -sync_event.wait() +libmetawear.mbl_mw_metawearboard_create_anonymous_datasignals(metawear.board, None, handler_fn) +e.wait() if (result['signals'] == None): if (result['length'] != 0): @@ -43,34 +40,37 @@ def __init__(self, signal): else: print("No active loggers detected") else: - dl_event = Event() + e.clear() libmetawear.mbl_mw_logging_stop(metawear.board) print(str(result['length']) + " active loggers discovered") handlers = [] for x in range(0, result['length']): wrapper = DataHandler(result['signals'].contents[x]) - libmetawear.mbl_mw_anonymous_datasignal_subscribe(result['signals'].contents[x], wrapper.data_handler_fn) + libmetawear.mbl_mw_anonymous_datasignal_subscribe(result['signals'].contents[x], None, wrapper.data_handler_fn) handlers.append(wrapper) - def progress_update_handler(left, total): + def progress_update_handler(ctx, left, total): if (left == 0): - dl_event.set() + e.set() - def unknown_entry_handler(id, epoch, data, length): + def unknown_entry_handler(ctx, id, epoch, data, length): print("unknown entry = " + str(id)) print("Downloading log") - progress_update_fn = FnVoid_UInt_UInt(progress_update_handler) - unknown_entry_fn = FnVoid_UByte_Long_UByteP_UByte(unknown_entry_handler) - download_handler= LogDownloadHandler(received_progress_update = progress_update_fn, - received_unknown_entry = unknown_entry_fn, received_unhandled_entry = cast(None, FnVoid_DataP)) + progress_update_fn = FnVoid_VoidP_UInt_UInt(progress_update_handler) + unknown_entry_fn = FnVoid_VoidP_UByte_Long_UByteP_UByte(unknown_entry_handler) + download_handler= LogDownloadHandler(context = None, received_progress_update = progress_update_fn, + received_unknown_entry = unknown_entry_fn, received_unhandled_entry = cast(None, FnVoid_VoidP_DataP)) libmetawear.mbl_mw_logging_download(metawear.board, 10, byref(download_handler)) - dl_event.wait() + e.wait() print("Download completed") libmetawear.mbl_mw_macro_erase_all(metawear.board) libmetawear.mbl_mw_debug_reset_after_gc(metawear.board) - libmetawear.mbl_mw_debug_disconnect(metawear.board) - sleep(1.0) + e.clear() + metawear.on_disconnect = lambda status: e.set() + + libmetawear.mbl_mw_debug_disconnect(metawear.board) + e.wait() diff --git a/examples/multi_device.py b/examples/multi_device.py index 32bf2c8..ce8d3d5 100644 --- a/examples/multi_device.py +++ b/examples/multi_device.py @@ -14,9 +14,9 @@ class State: def __init__(self, device): self.device = device self.samples = 0 - self.callback = FnVoid_DataP(self.data_handler) + self.callback = FnVoid_VoidP_DataP(self.data_handler) - def data_handler(self, data): + def data_handler(self, ctx, data): print("%s -> %s" % (self.device.address, parse_value(data))) self.samples+= 1 @@ -30,15 +30,15 @@ def data_handler(self, data): for s in states: print("configuring device") libmetawear.mbl_mw_settings_set_connection_parameters(s.device.board, 7.5, 7.5, 0, 6000) - libmetawear.mbl_mw_acc_set_odr(s.device.board, 25.0); - libmetawear.mbl_mw_acc_set_range(s.device.board, 16.0); - libmetawear.mbl_mw_acc_write_acceleration_config(s.device.board); + libmetawear.mbl_mw_acc_set_odr(s.device.board, 25.0) + libmetawear.mbl_mw_acc_set_range(s.device.board, 16.0) + libmetawear.mbl_mw_acc_write_acceleration_config(s.device.board) signal = libmetawear.mbl_mw_acc_get_acceleration_data_signal(s.device.board) - libmetawear.mbl_mw_datasignal_subscribe(signal, s.callback) + libmetawear.mbl_mw_datasignal_subscribe(signal, None, s.callback) - libmetawear.mbl_mw_acc_enable_acceleration_sampling(s.device.board); - libmetawear.mbl_mw_acc_start(s.device.board); + libmetawear.mbl_mw_acc_enable_acceleration_sampling(s.device.board) + libmetawear.mbl_mw_acc_start(s.device.board) sleep(30.0) diff --git a/examples/scan_connect.py b/examples/scan_connect.py index ad7b0fe..54c7ff8 100644 --- a/examples/scan_connect.py +++ b/examples/scan_connect.py @@ -1,21 +1,30 @@ # usage: python scan_connect.py from mbientlab.metawear import MetaWear from mbientlab.metawear.cbindings import * -from gattlib import DiscoveryService +from mbientlab.warble import * from time import sleep import platform +import six selection = -1 devices = None while selection == -1: - service = DiscoveryService("hci0") - devices = service.discover(2) + print("scanning for devices...") + devices = {} + def handler(result): + devices[result.mac] = result.name + + BleScanner.set_handler(handler) + BleScanner.start() + + sleep(10.0) + BleScanner.stop() i = 0 - for address, attr in devices.items(): - print("[%d] %s (%s)" % (i, address, attr['name'])) + for address, name in six.iteritems(devices): + print("[%d] %s (%s)" % (i, address, name)) i+= 1 msg = "Select your device (-1 to rescan): " diff --git a/examples/update_firmware.py b/examples/update_firmware.py new file mode 100644 index 0000000..3c5c2ad --- /dev/null +++ b/examples/update_firmware.py @@ -0,0 +1,27 @@ +# usage: python update_firmware.py [mac] [version](optional) +from mbientlab.metawear import MetaWear, libmetawear +from threading import Event + +import sys + +device = MetaWear(sys.argv[1]) +device.connect() +print("Connected") + +args = { + 'progress_handler': lambda p: print("upload: %d%%" % (p)), +} +if (len(sys.argv) >= 3): + args['version'] = sys.argv[2] + +e = Event() +result = [] +def dfu_handler(err): + result.append(err) + e.set() + +device.update_firmware_async(dfu_handler, **args) +e.wait() + +if (result[0] != None): + raise result[0] \ No newline at end of file diff --git a/mbientlab/metawear/__init__.py b/mbientlab/metawear/__init__.py index c389fe4..686a600 100644 --- a/mbientlab/metawear/__init__.py +++ b/mbientlab/metawear/__init__.py @@ -1,43 +1,20 @@ -from ctypes import * -from distutils.version import LooseVersion -from gattlib import GATTRequester, GATTResponse -from threading import Event from .cbindings import * +from ctypes import CDLL -import copy -import errno -import json import os -import requests -import sys -import time -import uuid +import platform -if sys.version_info[0] == 2: - range = xrange +if (platform.system() == 'Windows'): + _so_path = os.path.join(os.path.dirname(__file__), 'MetaWear.Win32.dll') +elif (platform.system() == 'Linux'): + _so_path = os.path.join(os.path.dirname(__file__), 'libmetawear.so') +else: + raise RuntimeError("MetaWear Python SDK is not supported for '%s'" % platform.system()) -so_path = os.path.join(os.path.dirname(__file__), 'libmetawear.so') -libmetawear= CDLL(so_path) +libmetawear= CDLL(_so_path) init_libmetawear(libmetawear) -def _gattchar_to_string(gattchar): - return str(uuid.UUID(int = ((gattchar.uuid_high << 64) | gattchar.uuid_low))) - -def _lookup_path(path): - return path if path is not None else ".metawear" - -def _download_file(url, dest): - try: - os.makedirs(os.path.dirname(dest)) - except OSError as exception: - if exception.errno != errno.EEXIST: - raise - finally: - r = requests.get(url, stream=True) - content = r.content - with open(dest, "wb") as f: - f.write(content) - return content +from .metawear import MetaWear def parse_value(p_data): """ @@ -75,282 +52,3 @@ def parse_value(p_data): return cast(p_data.contents.value, POINTER(OverflowState)).contents else: raise RuntimeError('Unrecognized data type id: ' + str(p_data.contents.type_id)) - -class _PyBlueZGatt(GATTRequester): - def __init__(self, address, device): - GATTRequester.__init__(self, address, False, device) - - self.dc_handler = None - self.notify_handlers = {} - - def on_notification(self, handle, data): - stripped = data[3:len(data)] - - buffer = create_string_buffer(stripped, len(stripped)) - handler = self.notify_handlers[handle]; - handler[1](handler[0], cast(buffer, POINTER(c_ubyte)), len(buffer.raw)) - -class MetaWear(object): - _METABOOT_SERVICE = uuid.UUID("00001530-1212-efde-1523-785feabcd123") - - @staticmethod - def _convert(value): - return value if sys.version_info[0] == 2 else value.encode('utf8') - - def __init__(self, address, **kwargs): - """ - Creates a MetaWear object - @params: - address - Required : MAC address of the board to connect to e.g. E8:C9:8F:52:7B:07 - cache_path - Optional : Path the SDK uses for cached data, defaults to '.metawear' in the local directory - device - Optional : hci device to use, defaults to 'hci0' - deserialize - Optional : Deserialize the cached C++ SDK state if available, defaults to true - """ - self.info = {} - - self.address = address - self.cache = kwargs['cache_path'] if ('cache_path' in kwargs) else ".metawear" - self.gatt = _PyBlueZGatt(address, "hci0" if 'device' not in kwargs else kwargs['device']) - self.response = GATTResponse() - self.on_notification = self.gatt.on_notification - - self._write_fn= FnVoid_VoidP_GattCharWriteType_GattCharP_UByteP_UByte(self._write_gatt_char) - self._read_fn= FnVoid_VoidP_GattCharP_FnIntVoidPtrArray(self._read_gatt_char) - self._notify_fn = FnVoid_VoidP_GattCharP_FnIntVoidPtrArray_FnVoidVoidPtrInt(self._enable_notifications) - self._disconnect_fn = FnVoid_VoidP_FnVoidVoidPtrInt(self._on_disconnect) - self._btle_connection= BtleConnection(write_gatt_char = self._write_fn, read_gatt_char = self._read_fn, - enable_notifications = self._notify_fn, on_disconnect = self._disconnect_fn) - - self.board = libmetawear.mbl_mw_metawearboard_create(byref(self._btle_connection)) - - if 'deserialize' not in kwargs or kwargs['deserialize']: - self.deserialize() - - try: - os.makedirs(self.cache) - except OSError as exception: - if exception.errno != errno.EEXIST: - raise - - @property - def in_metaboot_mode(self): - """ - True if the board is in MetaBoot mode. The only permitted operation for MetaBoot boards is to update the firmware - """ - return str(MetaWear._METABOOT_SERVICE) in self.services - - def disconnect(self): - """ - Disconnects from the MetaWear board - """ - self.gatt.disconnect() - - def connect(self, **kwargs): - """ - Connects to the MetaWear board and initializes the SDK. You must first connect to the board before using - any of the SDK functions - @params: - serialize - Optional : Serialize and cached C++ SDK state after initializaion, defaults to true - """ - try: - self.gatt.connect(True, channel_type='random') - except RuntimeError as e: - # gattlib.connect's `wait=True` requires elevated permission - # or modified capabilities. - # It still connects, but a RuntimeError is raised. Check if - # `self.gatt` is connected, and rethrow exception otherwise. - if not self.gatt.is_connected(): - raise e - - - self.services = set() - for s in self.gatt.discover_primary(): - self.services.add(s['uuid']) - - self.characteristics = {} - for c in self.gatt.discover_characteristics(): - self.characteristics[c['uuid']] = c['value_handle'] - - if ('hardware' not in self.info): - self.info['hardware'] = self.gatt.read_by_uuid("00002a27-0000-1000-8000-00805f9b34fb")[0] - - if ('manufacturer' not in self.info): - self.info['manufacturer'] = self.gatt.read_by_uuid("00002a29-0000-1000-8000-00805f9b34fb")[0] - - if ('serial' not in self.info): - self.info['serial'] = self.gatt.read_by_uuid("00002a25-0000-1000-8000-00805f9b34fb")[0] - - if ('model' not in self.info): - self.info['model'] = self.gatt.read_by_uuid("00002a24-0000-1000-8000-00805f9b34fb")[0] - - if not self.in_metaboot_mode: - init_event = Event() - def init_handler(device, status): - self.init_status = status - init_event.set() - - init_handler_fn = FnVoid_VoidP_Int(init_handler) - libmetawear.mbl_mw_metawearboard_initialize(self.board, init_handler_fn) - init_event.wait() - - if self.init_status != Const.STATUS_OK: - self.disconnect() - raise RuntimeError("Error initializing the API (%d)" % (self.init_status)) - - if 'serialize' not in kwargs or kwargs['serialize']: - self.serialize() - else: - self.info['firmware'] = self.gatt.read_by_uuid("00002a26-0000-1000-8000-00805f9b34fb")[0] - - def _read_gatt_char(self, caller, ptr_gattchar, handler): - uuid = _gattchar_to_string(ptr_gattchar.contents) - raw = self.gatt.read_by_uuid(uuid)[0] - - if (('model' not in self.info) and uuid == "00002a24-0000-1000-8000-00805f9b34fb"): - self.info['model'] = raw - elif (uuid == "00002a26-0000-1000-8000-00805f9b34fb"): - self.info['firmware'] = raw - - value = MetaWear._convert(raw) - buffer = create_string_buffer(value, len(value)) - handler(caller, cast(buffer, POINTER(c_ubyte)), len(buffer.raw)) - - def _write_gatt_char(self, caller, write_type, ptr_gattchar, value, length): - buffer= [] - for i in range(0, length): - buffer.append(value[i]) - - handle = self.characteristics[_gattchar_to_string(ptr_gattchar.contents)] - if (write_type == GattCharWriteType.WITH_RESPONSE): - self.gatt.write_by_handle_async(handle, bytes(bytearray(buffer)), self.response) - else: - self.gatt.write_cmd_by_handle(handle, bytes(bytearray(buffer))) - - def _enable_notifications(self, caller, ptr_gattchar, handler, ready): - handle = self.characteristics[_gattchar_to_string(ptr_gattchar.contents)] - self.gatt.write_by_handle(handle + 1, b'\x01\x00') - self.gatt.notify_handlers[handle] = [caller, handler]; - ready(caller, 0) - - def _on_disconnect(self, caller, handler): - pass - - def _download_firmware(self, version=None): - firmware_root = os.path.join(self.cache, "firmware") - - info1 = os.path.join(firmware_root, "info1.json") - if not os.path.isfile(info1) or (time.time() - os.path.getmtime(info1)) > 1800.0: - info1_content = json.loads(_download_file("https://releases.mbientlab.com/metawear/info1.json", info1)) - else: - with open(info1, "rb") as f: - info1_content = json.load(f) - - if version is None: - versions = [] - for k in info1_content[self.info['hardware']][self.info['model']]["vanilla"].keys(): - versions.append(LooseVersion(k)) - versions.sort() - target = str(versions[-1]) - else: - if version not in info1_content[self.info['hardware']][self.info['model']]["vanilla"]: - raise ValueError("Firmware '%s' not available for this board" % (version)) - target = version - - filename = info1_content[self.info['hardware']][self.info['model']]["vanilla"][target]["filename"] - local_path = os.path.join(firmware_root, self.info['hardware'], self.info['model'], "vanilla", target, filename) - - if not os.path.isfile(local_path): - url = "https://releases.mbientlab.com/metawear/{}/{}/{}/{}/{}".format( - self.info['hardware'], self.info['model'], "vanilla", target, filename - ) - _download_file(url, local_path) - return local_path - - def serialize(self): - """ - Serialize and cache the SDK state - """ - mac_str = self.address.replace(':','') - path = os.path.join(self.cache, '%s.json' % (mac_str)) - - state = { "info": copy.deepcopy(self.info) } - - size = c_uint(0) - cpp_state = cast(libmetawear.mbl_mw_metawearboard_serialize(self.board, byref(size)), POINTER(c_ubyte * size.value)) - state["cpp_state"] = [cpp_state.contents[i] for i in range(0, size.value)] - libmetawear.mbl_mw_memory_free(cpp_state) - - with open(path, "w") as f: - f.write(json.dumps(state, indent=2)) - - def deserialize(self): - """ - Deserialize the cached SDK state - """ - mac_str = self.address.replace(':','') - - # See if old serialized state exists, if it does, read that then remove it - path = os.path.join(self.cache, '%s.bin' % (mac_str)) - if os.path.isfile(path): - with(open(path, "rb")) as f: - content = f.read() - raw = (c_ubyte * len(content)).from_buffer_copy(content) - libmetawear.mbl_mw_metawearboard_deserialize(self.board, raw, len(content)) - - os.remove(path) - return True - - path = os.path.join(self.cache, '%s.json' % (mac_str)) - if os.path.isfile(path): - with(open(path, "r")) as f: - content = json.loads(f.read()) - self.info = content["info"] - raw = (c_ubyte * len(content)).from_buffer_copy(bytearray(content["cpp_state"])) - libmetawear.mbl_mw_metawearboard_deserialize(self.board, raw, len(content)) - return True - - return False - - def update_firmware_async(self, handler, **kwargs): - """ - Updates the firmware on the device. The function is asynchronous and will update the caller - with the result of the task via a two parameter callback function. If the first parameter is set - with a BaseException object, then the task failed. - @params: - handler - Required : Callback function to handle the result of the task - progress_handler - Optional : Callback function to handle progress updates - version - Optional : Specific firmware version to update to, defaults to latest available version - """ - if not self.in_metaboot_mode: - libmetawear.mbl_mw_debug_jump_to_bootloader(self.board) - time.sleep(10) - - self.disconnect() - self.connect() - if not self.in_metaboot_mode: - raise RuntimeError("DFU service not found") - - self._dfu_handler = handler - self._progress_handler = None if 'progress_handler' not in kwargs else kwargs['progress_handler'] - - def print_dfu_start(): - pass - - self._on_successful = FnVoid(lambda: self._dfu_handler(None, None)) - self._on_transfer = FnVoid_Int(self._dfu_progress) - self._on_started = FnVoid(print_dfu_start) - self._on_cancelled = FnVoid(lambda: self._dfu_error("DFU operation cancelled")) - self._on_error = FnVoid_charP(self._dfu_error) - - path = self._download_firmware() if 'version' not in kwargs else self._download_firmware(version = kwargs['version']) - buffer = create_string_buffer(path.encode('ascii')) - self._dfu_delegate = DfuDelegate(on_dfu_started = self._on_started, on_dfu_cancelled = self._on_cancelled, - on_transfer_percentage = self._on_transfer, on_successful_file_transferred = self._on_successful, on_error = self._on_error) - libmetawear.mbl_mw_metawearboard_perform_dfu(self.board, byref(self._dfu_delegate), buffer.raw) - - def _dfu_error(self, msg): - self._dfu_handler(RuntimeError(msg), None) - - def _dfu_progress(self, p): - if self._progress_handler != None: - self._progress_handler(p) diff --git a/mbientlab/metawear/metawear.py b/mbientlab/metawear/metawear.py new file mode 100644 index 0000000..92cab3d --- /dev/null +++ b/mbientlab/metawear/metawear.py @@ -0,0 +1,382 @@ +from . import libmetawear +from .cbindings import * +from collections import deque +from ctypes import * +from distutils.version import LooseVersion +from mbientlab.warble import Gatt +from threading import Event + +import copy +import errno +import json +import os +import platform +import requests +import sys +import time +import uuid + +_is_linux = platform.system() == 'Linux' + +if sys.version_info[0] == 2: + range = xrange + + def _array_to_buffer(value): + return create_string_buffer(str(bytearray(value)), len(value)) + +elif sys.version_info[0] == 3: + def _array_to_buffer(value): + return create_string_buffer(bytes(value), len(value)) + +def _gattchar_to_string(gattchar): + return str(uuid.UUID(int = ((gattchar.uuid_high << 64) | gattchar.uuid_low))) + +def _lookup_path(path): + return path if path is not None else ".metawear" + +def _download_file(url, dest): + try: + os.makedirs(os.path.dirname(dest)) + except OSError as exception: + if exception.errno != errno.EEXIST: + raise + finally: + r = requests.get(url, stream=True) + content = r.content + with open(dest, "wb") as f: + f.write(content) + return content + +class MetaWear(object): + _METABOOT_SERVICE = "00001530-1212-efde-1523-785feabcd123" + _DEV_INFO = { + "00002a27-0000-1000-8000-00805f9b34fb": "hardware", + "00002a29-0000-1000-8000-00805f9b34fb": "manufacturer", + "00002a25-0000-1000-8000-00805f9b34fb": "serial", + "00002a24-0000-1000-8000-00805f9b34fb": "model", + "00002a26-0000-1000-8000-00805f9b34fb": "firmware" + } + + @staticmethod + def _convert(value): + return value if sys.version_info[0] == 2 else value.encode('utf8') + + def __init__(self, address, **kwargs): + """ + Creates a MetaWear object + @params: + address - Required : Mac address of the board to connect to e.g. E8:C9:8F:52:7B:07 + cache_path - Optional : Path the SDK uses for cached data, defaults to '.metawear' in the local directory + hci_mac - Optional : Mac address of the hci device to uses, Warble will pick one if not set + deserialize - Optional : Deserialize the cached C++ SDK state if available, defaults to true + """ + args = {} + if (_is_linux and 'hci_mac' in kwargs): + args['hci'] = kwargs['hci_mac'] + self.warble = Gatt(address.upper(), **args) + + self.info = {} + self.write_queue = deque([]) + self.on_disconnect = None + self.address = address.upper() + self.cache = kwargs['cache_path'] if ('cache_path' in kwargs) else ".metawear" + + self._write_fn= FnVoid_VoidP_VoidP_GattCharWriteType_GattCharP_UByteP_UByte(self._write_gatt_char) + self._read_fn= FnVoid_VoidP_VoidP_GattCharP_FnIntVoidPtrArray(self._read_gatt_char) + self._notify_fn = FnVoid_VoidP_VoidP_GattCharP_FnIntVoidPtrArray_FnVoidVoidPtrInt(self._enable_notifications) + self._disconnect_fn = FnVoid_VoidP_VoidP_FnVoidVoidPtrInt(self._on_disconnect) + self._btle_connection= BtleConnection(write_gatt_char = self._write_fn, read_gatt_char = self._read_fn, + enable_notifications = self._notify_fn, on_disconnect = self._disconnect_fn) + + self.board = libmetawear.mbl_mw_metawearboard_create(byref(self._btle_connection)) + libmetawear.mbl_mw_metawearboard_set_time_for_response(self.board, 1000) + + if 'deserialize' not in kwargs or kwargs['deserialize']: + self.deserialize() + dev_info = libmetawear.mbl_mw_metawearboard_get_device_information(self.board) + self.info = { + 'hardware': dev_info.contents.hardware_revision.decode('utf8'), + 'manufacturer': dev_info.contents.manufacturer.decode('utf8'), + 'serial': dev_info.contents.serial_number.decode('utf8'), + 'firmware': dev_info.contents.firmware_revision.decode('utf8'), + 'model': dev_info.contents.model_number.decode('utf8') + } + + try: + os.makedirs(self.cache) + except OSError as exception: + if exception.errno != errno.EEXIST: + raise + + @property + def is_connected(self): + return self.warble.is_connected + + @property + def in_metaboot_mode(self): + """ + True if the board is in MetaBoot mode. The only permitted operation for MetaBoot boards is to update the firmware + """ + return self.warble.service_exists(MetaWear._METABOOT_SERVICE) + + def disconnect(self): + """ + Disconnects from the MetaWear board + """ + self.warble.disconnect() + + def connect(self, **kwargs): + """ + Connects to the MetaWear board and initializes the SDK. You must first connect to the board before using + any of the SDK functions + @params: + serialize - Optional : Serialize and cached C++ SDK state after initializaion, defaults to true + """ + e = Event() + result = None + def completed(err): + if (err != None): + result = err + e.set() + + self.warble.connect_async(completed) + e.wait() + + if (result != None): + raise result + + if not self.in_metaboot_mode: + init_event = Event() + def init_handler(context, device, status): + self._init_status = status + init_event.set() + + init_handler_fn = FnVoid_VoidP_VoidP_Int(init_handler) + libmetawear.mbl_mw_metawearboard_initialize(self.board, None, init_handler_fn) + init_event.wait() + + if self._init_status != Const.STATUS_OK: + self.disconnect() + raise RuntimeError("Error initializing the API (%d)" % (self._init_status)) + + if 'serialize' not in kwargs or kwargs['serialize']: + self.serialize() + else: + uuids = deque(MetaWear._DEV_INFO.keys()) + while(len(uuids)): + next = uuids.popleft() + if (MetaWear._DEV_INFO[next] not in self.info): + gatt_char = self.warble.find_characteristic(next) + if (gatt_char == None): + raise RuntimeError("Missing gatt char '%s'" % (next)) + + e.clear() + results = [] + def completed(value, error): + results.append(value) + results.append(error) + e.set() + + gatt_char.read_value_async(completed) + e.wait() + + if results[1] == None: + self.info[MetaWear._DEV_INFO[next]] = bytearray(results[0]).decode('utf8') + else: + raise results[1] + + def _read_gatt_char(self, context, caller, ptr_gattchar, handler): + uuid = _gattchar_to_string(ptr_gattchar.contents) + gatt_char = self.warble.find_characteristic(uuid) + + if (gatt_char == None): + print("gatt char '%s' does not exist" % (uuid)) + + def completed(value, error): + if error == None: + read_value = bytearray(value) + self.info[MetaWear._DEV_INFO[uuid]] = read_value.decode('utf8') + + handler(caller, cast(_array_to_buffer(value), POINTER(c_ubyte)), len(value)) + else: + print("%s: Error reading gatt char (%s)" % (gatt_char.uuid, error)) + + gatt_char.read_value_async(completed) + + def _write_char_async(self, force): + count = len(self.write_queue) + if (count > 0 and (force or count == 1)): + next = self.write_queue[0] + + def completed(err): + if (err != None): + print(str(err)) + temp = self.write_queue.popleft() + self._write_char_async(True) + + if (next[2] == GattCharWriteType.WITH_RESPONSE): + next[0].write_async(next[1], completed) + else: + next[0].write_without_resp_async(next[1], completed) + + def _write_gatt_char(self, context, caller, write_type, ptr_gattchar, value, length): + gatt_char = self.warble.find_characteristic(_gattchar_to_string(ptr_gattchar.contents)) + buffer = [value[i] for i in range(0, length)] + + self.write_queue.append([gatt_char, buffer, write_type]) + + self._write_char_async(False) + + def _enable_notifications(self, context, caller, ptr_gattchar, handler, ready): + uuid = _gattchar_to_string(ptr_gattchar.contents) + gatt_char = self.warble.find_characteristic(uuid) + + if (gatt_char == None): + ready(caller, Const.STATUS_ERROR_ENABLE_NOTIFY) + else: + def completed(err): + if err != None: + print(str(err)) + ready(caller, Const.STATUS_ERROR_ENABLE_NOTIFY) + else: + gatt_char.on_notification_received(lambda value: handler(caller, cast(_array_to_buffer(value), POINTER(c_ubyte)), len(value))) + ready(caller, Const.STATUS_OK) + + gatt_char.enable_notifications_async(completed) + + def _on_disconnect(self, context, caller, handler): + def event_handler(status): + if (self.on_disconnect != None): + self.on_disconnect(status) + handler(caller, status) + self.warble.on_disconnect(event_handler) + + def _download_firmware(self, version=None): + firmware_root = os.path.join(self.cache, "firmware") + + info1 = os.path.join(firmware_root, "info1.json") + if not os.path.isfile(info1) or (time.time() - os.path.getmtime(info1)) > 1800.0: + info1_content = json.loads(_download_file("https://releases.mbientlab.com/metawear/info1.json", info1)) + else: + with open(info1, "rb") as f: + info1_content = json.load(f) + + if version is None: + versions = [] + for k in info1_content[self.info['hardware']][self.info['model']]["vanilla"].keys(): + versions.append(LooseVersion(k)) + versions.sort() + target = str(versions[-1]) + else: + if version not in info1_content[self.info['hardware']][self.info['model']]["vanilla"]: + raise ValueError("Firmware '%s' not available for this board" % (version)) + target = version + + filename = info1_content[self.info['hardware']][self.info['model']]["vanilla"][target]["filename"] + local_path = os.path.join(firmware_root, self.info['hardware'], self.info['model'], "vanilla", target, filename) + + if not os.path.isfile(local_path): + url = "https://releases.mbientlab.com/metawear/{}/{}/{}/{}/{}".format( + self.info['hardware'], self.info['model'], "vanilla", target, filename + ) + _download_file(url, local_path) + return local_path + + def serialize(self): + """ + Serialize and cache the SDK state + """ + mac_str = self.address.replace(':','') + path = os.path.join(self.cache, '%s.json' % (mac_str)) + + state = { "info": copy.deepcopy(self.info) } + + size = c_uint(0) + cpp_state = cast(libmetawear.mbl_mw_metawearboard_serialize(self.board, byref(size)), POINTER(c_ubyte * size.value)) + state["cpp_state"] = [cpp_state.contents[i] for i in range(0, size.value)] + libmetawear.mbl_mw_memory_free(cpp_state) + + with open(path, "w") as f: + f.write(json.dumps(state, indent=2)) + + def deserialize(self): + """ + Deserialize the cached SDK state + """ + mac_str = self.address.replace(':','') + + # See if old serialized state exists, if it does, read that then remove it + path = os.path.join(self.cache, '%s.bin' % (mac_str)) + if os.path.isfile(path): + with(open(path, "rb")) as f: + content = f.read() + raw = (c_ubyte * len(content)).from_buffer_copy(content) + libmetawear.mbl_mw_metawearboard_deserialize(self.board, raw, len(content)) + + os.remove(path) + return True + + path = os.path.join(self.cache, '%s.json' % (mac_str)) + if os.path.isfile(path): + with(open(path, "r")) as f: + content = json.loads(f.read()) + self.info = content["info"] + raw = (c_ubyte * len(content["cpp_state"])).from_buffer_copy(bytearray(content["cpp_state"])) + libmetawear.mbl_mw_metawearboard_deserialize(self.board, raw, len(content)) + return True + + return False + + def update_firmware_async(self, handler, **kwargs): + """ + Updates the firmware on the device. The function is asynchronous and will update the caller + with the result of the task via a one parameter callback function. If the parameter is set + with a BaseException object, then the task failed. + @params: + handler - Required : `(BaseException) -> void` function to handle the result of the task + progress_handler - Optional : `(int) -> void` function to handle progress updates + version - Optional : Specific firmware version to update to, defaults to latest available version + """ + if not self.in_metaboot_mode: + dc_copy = self.on_disconnect + + e = Event() + self.on_disconnect = lambda status: e.set() + + libmetawear.mbl_mw_debug_jump_to_bootloader(self.board) + e.wait() + + self.on_disconnect = dc_copy + + try: + self.connect() + if not self.in_metaboot_mode: + handler(RuntimeError("DFU service not found")) + except BaseException as err: + handler(err) + return + + self._progress_handler = None if 'progress_handler' not in kwargs else kwargs['progress_handler'] + + def completed(ctx): + time.sleep(5.0) + handler(None) + + self._on_successful = FnVoid_VoidP(completed) + self._on_transfer = FnVoid_VoidP_Int(self._dfu_progress) + self._on_started = FnVoid_VoidP(lambda ctx: None) + self._on_cancelled = FnVoid_VoidP(lambda ctx: self._dfu_error("DFU operation cancelled")) + self._on_error = FnVoid_VoidP_charP(lambda ctx, msg: self._dfu_handler(RuntimeError(msg))) + + try: + path = self._download_firmware() if 'version' not in kwargs else self._download_firmware(version = kwargs['version']) + buffer = create_string_buffer(path.encode('ascii')) + self._dfu_delegate = DfuDelegate(context = None, on_dfu_started = self._on_started, on_dfu_cancelled = self._on_cancelled, + on_transfer_percentage = self._on_transfer, on_successful_file_transferred = self._on_successful, on_error = self._on_error) + libmetawear.mbl_mw_metawearboard_perform_dfu(self.board, byref(self._dfu_delegate), buffer.raw) + except ValueError as e: + handler(e) + + def _dfu_progress(self, ctx, p): + if self._progress_handler != None: + self._progress_handler(p) diff --git a/setup.py b/setup.py index 522b20f..51d9bd6 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ -from distutils.dir_util import copy_tree from multiprocessing import cpu_count -from shutil import copy2 +from shutil import copy2, move from subprocess import call, STDOUT from setuptools import setup from setuptools.command.build_py import build_py @@ -12,45 +11,59 @@ machine = "arm" if "arm" in platform.machine() else ("x64" if sys.maxsize > 2**32 else "x86") class MetaWearBuild(build_py): + @staticmethod + def _move(src, dest, basename): + for f in os.listdir(src): + if (f.startswith(basename)): + move(os.path.join(src, f), dest) + def run(self): root = os.path.dirname(os.path.abspath(__file__)) + dest = os.path.join("mbientlab", "metawear") + cpp_sdk = os.path.join(root, 'MetaWear-SDK-Cpp') + dist_dir = os.path.join(cpp_sdk, 'dist', 'release', 'lib', machine) if os.path.exists(os.path.join(root, '.git')): status = call(["git", "submodule", "update", "--init"], cwd=root, stderr=STDOUT) if (status != 0): raise RuntimeError("Could not init git submodule") - status = call(["make", "-C", "MetaWear-SDK-Cpp", "OPT_FLAGS=-Wno-strict-aliasing", "-j%d" % (cpu_count())], cwd=root, stderr=STDOUT) - if (status != 0): - raise RuntimeError("Failed to compile C++ SDK") + if (platform.system() == 'Windows'): + if (call(["MSBuild.exe", "MetaWear.Win32.vcxproj", "/p:Platform=%s" % machine, "/p:Configuration=Release"], cwd=cpp_sdk, stderr=STDOUT) != 0): + raise RuntimeError("Failed to compile MetaWear.dll") + + move(os.path.join(dist_dir, "MetaWear.Win32.dll"), dest) + elif (platform.system() == 'Linux'): + status = call(["make", "-C", "MetaWear-SDK-Cpp", "OPT_FLAGS=-Wno-strict-aliasing", "-j%d" % (cpu_count())], cwd=root, stderr=STDOUT) + if (status != 0): + raise RuntimeError("Failed to compile C++ SDK") - copy_tree('MetaWear-SDK-Cpp/dist/release/lib/%s/' % (machine), "mbientlab/metawear") - copy2('MetaWear-SDK-Cpp/bindings/python/mbientlab/metawear/cbindings.py', "mbientlab/metawear") + MetaWearBuild._move(dist_dir, dest, 'libmetawear.so') + else: + raise RuntimeError("MetaWear Python SDK not supported for '%s'" % platform.system()) + copy2(os.path.join(cpp_sdk, 'bindings', 'python', 'mbientlab', 'metawear', 'cbindings.py'), dest) build_py.run(self) +so_pkg_data = ['libmetawear.so'] if platform.system() == 'Linux' else ['MetaWear.Win32.dll'] setup( name='metawear', packages=['mbientlab', 'mbientlab.metawear'], - version='0.3.1', + version='0.4.0', description='Python bindings for the MetaWear C++ SDK by MbientLab', long_description=open(os.path.join(os.path.dirname(__file__), "README.rst")).read(), - package_data={'mbientlab.metawear': ['libmetawear.so*']}, + package_data={'mbientlab.metawear': so_pkg_data}, include_package_data=True, url='https://github.com/mbientlab/MetaWear-SDK-Python', author='MbientLab', author_email="hello@mbientlab.com", install_requires=[ - 'gattlib==0.20171002', + 'warble >= 1.0, < 2.0', 'requests' ], cmdclass={ 'build_py': MetaWearBuild, }, - dependency_links=[ - 'git+https://github.com/mbientlab/pygattlib.git/@master#egg=gattlib-0.20171002', - 'git+https://github.com/mbientlab/pygattlib.git@master#egg=gattlib-0.20171002' - ], keywords = ['sensors', 'mbientlab', 'metawear', 'bluetooth le', 'native'], python_requires='>=2.7', classifiers=[