diff --git a/nordicsemi/__main__.py b/nordicsemi/__main__.py index c5a448b..09517ed 100644 --- a/nordicsemi/__main__.py +++ b/nordicsemi/__main__.py @@ -148,7 +148,10 @@ def convert(self, value, param, ctx): @click.option('-v', '--verbose', help='Show verbose information.', count=True) -def cli(verbose): +@click.option('-o', '--output', + help='Log output to file', + metavar='') +def cli(verbose, output): #click.echo('verbosity: %s' % verbose) if verbose == 0: log_level = logging.ERROR @@ -157,7 +160,14 @@ def cli(verbose): else: log_level = logging.DEBUG - logging.basicConfig(format='%(message)s', level=log_level) + logging.basicConfig(format='%(asctime)s %(message)s', level=log_level) + + if (output): + root = logging.getLogger('') + fh = logging.FileHandler(output) + fh.setLevel(log_level) + fh.setFormatter(logging.Formatter('%(asctime)s %(message)s')) + root.addHandler(fh) @cli.command() def version(): @@ -788,17 +798,14 @@ def convert_version_string_to_int(s): help='Serial port COM port to which the NCP is connected.', type=click.STRING) @click.option('-a', '--address', - help='Device IPv6 address. If address is not specified then perform DFU' - + 'on all capable devices.', + help='Device IPv6 address. If address is not specified then perform DFU ' + 'on all capable devices. If multicast address is specified (FF03::1), ' + 'perform multicast DFU.', type=click.STRING) @click.option('-sp', '--server_port', help='UDP port to which the DFU server binds. If not specified the 5683 is used.', type=click.INT, default=5683) -@click.option('--prefix', - help='URI prefix used added to DFU resources. Defaults to ''dfu''.', - type=click.STRING, - default='dfu') @click.option('--panid', help='802.15.4 PAN ID. If not specified then 1234 is used as PAN ID.', type=click.INT) @@ -816,13 +823,25 @@ def convert_version_string_to_int(s): help='Use software NCP and connect to the OT simulator.', type=click.BOOL, is_flag=True) -def thread(package, port, address, server_port, prefix, panid, channel, jlink_snr, flash_connectivity, sim): +@click.option('-r', '--rate', + help="Multicast upload rate in blocks per second.", + type=click.FLOAT) +@click.option('-rs', '--reset_suppress', + help='Suppress device reset after finishing DFU for a given number of milliseconds. ' + + 'If -1 is given then suppress indefinatelly.', + type = click.INT, + metavar = '') + +def thread(package, port, address, server_port, panid, channel, jlink_snr, flash_connectivity, + sim, rate, reset_suppress): ble_driver_init('NRF52') from nordicsemi.thread import tncp from nordicsemi.thread.dfu_thread import create_dfu_server from nordicsemi.thread.tncp import NCPTransport from nordicsemi.thread.ncp_flasher import NCPFlasher + mcast_dfu = False + """Perform a Device Firmware Update on a device with a bootloader that supports Thread DFU.""" if address is None: address = ipaddress.ip_address(u"ff03::1") @@ -830,26 +849,27 @@ def thread(package, port, address, server_port, prefix, panid, channel, jlink_sn else: try: address = ipaddress.ip_address(address) + mcast_dfu = address.is_multicast except: click.echo("Invalid IPv6 address") - return + return 1 if (not sim): if port is None and jlink_snr is None: click.echo("Please specify serial port or Jlink serial number.") - return + return 2 elif port is None: port = get_port_by_snr(jlink_snr) if port is None: click.echo("\nNo Segger USB CDC ports found, please connect your board.") - return + return 3 stream_descriptor = 'u:' + port - logger.info("Using connectivity board at serial port: {}".format(port)) + click.echo("Using connectivity board at serial port: {}".format(port)) else: stream_descriptor = 'p:' + Flasher.which('ot-ncp') + ' 30' - logger.info("Using ot-ncp binary: {}".format(stream_descriptor)) + click.echo("Using ot-ncp binary: {}".format(stream_descriptor)) if flash_connectivity: flasher = NCPFlasher(serial_port=port, snr = jlink_snr) @@ -873,24 +893,31 @@ def thread(package, port, address, server_port, prefix, panid, channel, jlink_sn if (flash_connectivity): config[tncp.NCPTransport.CFG_KEY_RESET] = False + opts = type('DFUServerOptions', (object,), {})() + opts.rate = rate + opts.reset_suppress = reset_suppress + opts.mcast_dfu = mcast_dfu + transport = NCPTransport(server_port, stream_descriptor, config) - dfu = create_dfu_server(transport, package, prefix) + dfu = create_dfu_server(transport, package, opts) try: - sighandler = lambda signum, frame : dfu.stop + sighandler = lambda signum, frame : transport.close() signal.signal(signal.SIGINT, sighandler) signal.signal(signal.SIGTERM, sighandler) - dfu.start() + transport.open() + # Delay DFU trigger until NCP promotes to a router (6 seconds by default) + time.sleep(6.0) dfu.trigger(address, 3) - click.echo("Press terminate") + click.echo("Press to terminate") pause() click.echo("Terminating") except Exception as e: logger.exception(e) finally: - dfu.stop() + transport.close() if __name__ == '__main__': cli() diff --git a/nordicsemi/thread/dfu_server.py b/nordicsemi/thread/dfu_server.py index f408228..a43fb3c 100644 --- a/nordicsemi/thread/dfu_server.py +++ b/nordicsemi/thread/dfu_server.py @@ -1,4 +1,3 @@ -# # Copyright (c) 2016 Nordic Semiconductor ASA # All rights reserved. # @@ -38,31 +37,51 @@ import binascii import struct import tqdm +import threading +import json +import sys + import piccata.core import piccata.block_transfer - -from ipaddress import ip_address +from piccata.block_transfer import extract_block from piccata.message import Message from piccata import constants - +from ipaddress import ip_address +from collections import namedtuple +import collections +import time logger = logging.getLogger(__name__) -def _make_trigger(init_data, image_data): +def _make_trigger(init_data, image_data, mcast_mode = False, reset_suppress = 0): '''Create a trigger payload from given init and image data''' - TRIGGER_VERSION = 0 - + TRIGGER_VERSION = 1 + MCAST_MODE_BIT = 0x08 + RESET_SUPPRESS_BIT = 0x04 + def crc(payload): return binascii.crc32(payload) & 0xffffffff # Structure format: # flags: uint8_t + # + # |V3|V2|V1|V0|M|R|R1|R0| + # + # V3-V0: version + # M: mcast mode + # R: reset suppress + # R1-R0: reserved bits + # # init size: uint32_t # init crc: uint32_t # image size: uint32_t # image crc: uint32_t - # prefix: var len max 16 bytes - flags = ((TRIGGER_VERSION << 4) & 0x03) + flags = (TRIGGER_VERSION << 4) + if (mcast_mode): + flags |= MCAST_MODE_BIT + if (reset_suppress != 0): + flags |= RESET_SUPPRESS_BIT + return struct.pack(">BIIII", flags, len(init_data), @@ -70,17 +89,20 @@ def crc(payload): len(image_data), crc(image_data)) -def _uri_string_to_list(uri): - '''Converts a URI string into a list of URI path elements. - - Example: '/dfu/image' -> ['dfu', 'image'] - ''' - return uri.lstrip('/').split('/') +def _make_bitmap(resource): + return [(resource, i) for i in range(0, _block_count(len(resource.data), ThreadDfuServer.BLOCK_SZX) + 1)] def _block_count(length, block_size): - '''Return number of blocks of a given size for total length of data''' + '''Return number of blocks of a given size for the total length of data.''' return int(length / (2 ** (block_size + 4)) + 0.5) +def _bmp_to_str(bitmap): + '''Convert binary data into a bit string''' + s = '' + for i in range(8): + s = s + '{:08b} '.format((bitmap >> 64 - 8*(i + 1)) & 0xff) + return s[:len(s) - 1] + def _get_block_opt(request): if (request.opt.block1): return request.opt.block1 @@ -89,100 +111,288 @@ def _get_block_opt(request): else: return (0, False, constants.DEFAULT_BLOCK_SIZE_EXP) +class ThreadDfuClient: + def __init__(self): + '''Stores a reference to a progress bar object.''' + self.progress_bar = None + '''The number of a block most recently requested by a node.''' + self.last_block = None + +Resource = namedtuple('Resource', ['path', 'data']) + class ThreadDfuServer(): - - def __init__(self, transport, init_data, image_data, uri_prefix): + REALM_LOCAL_ADDR = ip_address(u'FF03::1') + + SPBLK_SIZE = 64 # number of CoAP blocks of BLOCK_SZX size each + SPBLK_UPLOAD_RATE = 1 # in blocks / seconds + SPBLK_BMP_TIMEOUT = 2 # in seconds + BLOCK_SZX = 2 # 64 bytes + ERASE_DELAY = 0.5 # in seconds + SPBLK_FLUSH_DELAY = 1.0 # delay between superblocks + POST_UPLOAD_DELAY = 5.0 # delay after uploading the last block, in seconds + + IMAGE_URI = 'f' + INIT_URI = 'i' + TRIGGER_URI = 't' + BITMAP_URI = 'b' + + def __init__(self, protocol, init_data, image_data, opts): + assert(protocol != None) assert(init_data != None) assert(image_data != None) - - self.protocol = piccata.core.Coap(transport) + + self.opts = opts + if (not opts or not opts.rate): + self.opts.rate = self.SPBLK_UPLOAD_RATE + if (not opts or not opts.mcast_dfu): + self.opts.mcast_dfu = False + if (not opts or not opts.reset_suppress): + self.opts.reset_suppress = 0 + + self.protocol = protocol self.protocol.register_request_handler(self) - - self.transport = transport - self.transport.register_receiver(self.protocol) - - self.trigger_payload = _make_trigger(init_data, image_data) - self.init_data = init_data - self.image_data = image_data - - self.uri_prefix = uri_prefix - - self.progress = {} - - def _update_progress_bar(self, remote_address, block_num, block_szx): - block_count = _block_count(len(self.image_data), block_szx) - - if (remote_address not in self.progress): - pbar = tqdm.tqdm(desc = str(remote_address), - position = len(self.progress), - initial = block_num, - total = block_count) - last_block = None - self.progress[remote_address] = (pbar, block_num) - else: - pbar, last_block = self.progress[remote_address] - - if (last_block is not None and (block_num > last_block)): - pbar.update(block_num - last_block) - self.progress[remote_address] = (pbar, block_num) - - if (block_num == block_count): - pbar.close() - + + self.progress_bar = None + + self.missing_blocks = [] + self.bmp_received_event = threading.Event() + self.upload_done_event = threading.Event() + self.trig_done_event = threading.Event() + + self.init_resource = Resource((ThreadDfuServer.INIT_URI,), init_data) + self.image_resource = Resource((ThreadDfuServer.IMAGE_URI,), image_data) + + self.clients = {} + + def _draw_token(self): + return piccata.message.random_token(2) + + def _update_progress_bar(self, address, client, block_count, total_block_count): + # If node didn't request any blocks yet then create a new progress + # bar for it. Update otherwise. + if (client.progress_bar is None): + client.progress_bar = tqdm.tqdm(desc = str(address), + position = len(self.clients), + initial = block_count, + total = total_block_count) + elif (block_count > client.last_block): + client.progress_bar.update(block_count - client.last_block) + + if (block_count == total_block_count): + client.progress_bar.close() + client.progress_bar = None + def _handle_image_request(self, request): + if (request.remote not in self.clients): + self.clients[request.remote] = ThreadDfuClient() + block_num, _, block_szx = _get_block_opt(request) - self._update_progress_bar(request.remote.addr, block_num, block_szx) - return piccata.block_transfer.create_block_2_response(self.image_data, request) + + total_block_count = _block_count(len(self.image_resource.data), block_szx) + + + self._update_progress_bar(request.remote.addr, + self.clients[request.remote], + block_num, + total_block_count) + + self.clients[request.remote].last_block = block_num + + return piccata.block_transfer.create_block_2_response(self.image_resource.data, request) def _handle_init_request(self, request): - return piccata.block_transfer.create_block_2_response(self.init_data, request) - + # Add remote to the list of prospective DFU clients + if (request.remote not in self.clients): + self.clients[request.remote] = ThreadDfuClient() + logger.debug("Added {} to clients".format(request.remote.addr)) + + return piccata.block_transfer.create_block_2_response(self.init_resource.data, request) + def _handle_trigger_response(self, result, request, response, num_of_requests): - if (response == piccata.constants.RESULT_SUCCESS): - logger.info('Response Code: ' + piccata.constants.responses[response.code]) - logger.info('Payload: ' + response.payload) - elif (num_of_requests - 1 > 0): - request.timeout = 2*request.timeout - self.protocol.request(request, self._handle_trigger_response, (num_of_requests - 1,)) - else: - logger.info("All triggers sent") + assert (result == piccata.constants.RESULT_TIMEOUT) + + if (num_of_requests - 1 > 0): + self.protocol.request(request, + self._handle_trigger_response, + (num_of_requests - 1,)) + return + + self.trig_done_event.set() + + def _handle_trigger_request(self, request): + response = Message.AckMessage(request, + constants.CONTENT, + _make_trigger(self.init_resource.data, + self.image_resource.data, + self.opts.mcast_dfu, + self.opts.reset_suppress)) + return response + + def _send_block(self, remote, path, num, more, szx, payload): + ''' + Send a single block. + :param remote: An address of the remote endpoint. + :param path: An URI path to the block resource. + :param num: A block number. Part of the CoAP block option. + :param more: An information if more blocks are pending. Part of the CoAP block option. + :param szx: A block size, encoded in CoAP block option format. Part of the CoAP block option. + :param payload: A block payload. + ''' + logger.info('Sending block {} to {}'.format(num, remote.addr)) + + request = piccata.message.Message(mtype = piccata.constants.NON, + code = piccata.constants.PUT) + + request.opt.uri_path = path + request.opt.block1 = (num, more, szx) + request.remote = remote + request.payload = payload + + self.protocol.request(request) + + def _upload(self, remote, bitmap): + while True: + # Bitmap holds (resource, num) tuples. Sort them using path and block num. + bitmap.sort(key = lambda item : (item[0].path[0] == ThreadDfuServer.IMAGE_URI, item[1])) + resource, num = bitmap.pop(0) + + payload, more = extract_block(resource.data, + num, + ThreadDfuServer.BLOCK_SZX) + + logger.debug("Uploading resource {} block {} to {}".format(resource.path, num, remote.addr)) + + self._send_block(remote, + resource.path, + num, + more, + ThreadDfuServer.BLOCK_SZX, + payload) + + self.bmp_received_event.clear() + if len(bitmap): + if (num % ThreadDfuServer.SPBLK_SIZE == 0) or (((num + 1) % ThreadDfuServer.SPBLK_SIZE) == 0): + delay = ThreadDfuServer.ERASE_DELAY + else: + delay = 1.0/self.opts.rate + + time.sleep(delay) + + else: + self.upload_done_event.set() + self.bmp_received_event.wait() + + def _handle_reset_response(self, result, request, response, num_of_requests, delay): + assert (result == piccata.constants.RESULT_TIMEOUT) + + if (num_of_requests - 1 > 0): + self.protocol.request(request, + self._handle_reset_response, + (num_of_requests - 1, delay)) + + def _send_reset_request(self, remote, num_of_requests, delay): + logger.info('Sending reset request to {}'.format(remote)) + + request = piccata.message.Message(mtype = piccata.constants.NON, + code = piccata.constants.PUT, + token = self._draw_token()) + + request.opt.uri_path = ("r",) + request.remote = remote + request.timeout = ThreadDfuServer.SPBLK_BMP_TIMEOUT + request.payload = struct.pack(">I", delay) + + self.protocol.request(request, + self._handle_reset_response, + (num_of_requests, delay)) + + def _handle_bitmap_request(self, request): + payload = struct.unpack('!HQ', request.payload) + num = payload[0] + bmp = payload[1] + path = request.opt.uri_path[1] + logger.debug("Device {} returned path {} num {} bmp {}".format(request.remote.addr, + path, + num, + _bmp_to_str(bmp))) + + if (path == ThreadDfuServer.INIT_URI): + resource = self.init_resource + elif (path == ThreadDfuServer.IMAGE_URI): + resource = self.image_resource + + for i in range(ThreadDfuServer.SPBLK_SIZE): + item = (resource, num + i) + if (bmp & (1 << (ThreadDfuServer.SPBLK_SIZE - 1 - i)) and item not in self.missing_blocks): + self.missing_blocks.append(item) + logger.debug("Added {} block {} to missing list".format(item[0].path, item[1])) + + self.bmp_received_event.set() + return None def receive_request(self, request): '''Request callback called by the CoAP toolkit. Note that the function signature (name, args) is expected by the CoAP toolkit.''' - - # TODO: remove hardcoded URIs + + # TODO: consider a case where there is a mcast DFU in progress + # but a request is received. How this should be handled? + handlers = { - 'image' : self._handle_image_request, - 'init' : self._handle_init_request, - 'trig' : lambda request : Message.AckMessage(request, - constants.CONTENT, - self.trigger_payload) + ThreadDfuServer.IMAGE_URI : self._handle_image_request, + ThreadDfuServer.INIT_URI : self._handle_init_request, + ThreadDfuServer.TRIGGER_URI : self._handle_trigger_request, + ThreadDfuServer.BITMAP_URI : self._handle_bitmap_request, } - + for uri, handler in handlers.items(): - if request.opt.uri_path == _uri_string_to_list(self.uri_prefix + '/' + uri): - response = handler(request) - - return response - + if '/'.join(request.opt.uri_path).startswith(uri): + return handler(request) + + return piccata.message.Message.AckMessage(request, piccata.constants.NOT_FOUND) + + def _multicast_upload(self, remote, num_of_requests): + self.missing_blocks.extend(_make_bitmap(self.init_resource)) + self.missing_blocks.extend(_make_bitmap(self.image_resource)) + + self._send_trigger(remote, num_of_requests) + self.trig_done_event.wait() + + upload_thread = threading.Thread(target = self._upload, + name = "Upload thread", + args = (remote, self.missing_blocks)) + upload_thread.setDaemon(True) + upload_thread.start() + + time.sleep(15) + self.upload_done_event.wait() + + if (self.opts.reset_suppress > 0): + self._send_reset_request(remote, num_of_requests, self.opts.reset_suppress) + + def _send_trigger(self, remote, num_of_requests): + logger.info('Triggering DFU on {}'.format(remote)) + request = piccata.message.Message(mtype = piccata.constants.NON, + code = piccata.constants.POST, + token = self._draw_token()) + + request.opt.uri_path = (ThreadDfuServer.TRIGGER_URI,) + request.remote = remote + request.timeout = ThreadDfuServer.SPBLK_BMP_TIMEOUT + request.payload = _make_trigger(self.init_resource.data, + self.image_resource.data, + self.opts.mcast_dfu, + self.opts.reset_suppress) + self.protocol.request(request, + self._handle_trigger_response, + (num_of_requests,)) + def trigger(self, address, num_of_requests): - logger.info('Triggering DFU on {}\r'.format(address)) - - request = piccata.message.Message(mtype = piccata.constants.NON, code=piccata.constants.POST) - request.opt.uri_path = (self.uri_prefix, "trig",) - request.remote = (ip_address(address), piccata.constants.COAP_PORT) - request.timeout = piccata.constants.ACK_TIMEOUT - request.payload = self.trigger_payload - - self.protocol.request(request, self._handle_trigger_response, (num_of_requests,)) - - def start(self): - logger.debug("Starting DFU Server") - self.transport.open() - logger.debug("Server ready") - - def stop(self): - logger.debug("Stopping DFU Server") - self.transport.close() - logger.debug("Server stopped") + remote = piccata.types.Endpoint(address, piccata.constants.COAP_PORT) + + if (self.opts.mcast_dfu): + thread = threading.Thread(target = self._multicast_upload, + args = (remote, num_of_requests, )) + thread.setDaemon(True) + thread.start() + else: + self._send_trigger(remote, num_of_requests) diff --git a/nordicsemi/thread/dfu_thread.py b/nordicsemi/thread/dfu_thread.py index ff3b0e9..858a165 100644 --- a/nordicsemi/thread/dfu_thread.py +++ b/nordicsemi/thread/dfu_thread.py @@ -37,6 +37,7 @@ import tempfile import os.path import logging +import piccata from nordicsemi.dfu.package import Package from nordicsemi.thread.dfu_server import ThreadDfuServer @@ -68,16 +69,28 @@ def _get_file_names(manifest): logger.info("Image type {} found".format(data_attrs[0])) return firmware.dat_file, firmware.bin_file -def create_dfu_server(transport, zip_file_path, prefix): +def create_dfu_server(transport, zip_file_path, opts): + ''' + Create a DFU server instance. + :param transpoort: A transport to be used. + :param zip_file_path: A path to the firmware package. + :param opts: Optional paramters: + mcast_dfu: An information if multicast DFU is enabled. + rate: Multicast block transfer rate, in blocks per second + reset_suppress: A delay before sending multicast reset command (in milliseconds). -1 means that no reset will be sent. + ''' temp_dir = tempfile.mkdtemp(prefix="nrf_dfu_") unpacked_zip_path = os.path.join(temp_dir, 'unpacked_zip') manifest = Package.unpack_package(zip_file_path, unpacked_zip_path) + protocol = piccata.core.Coap(transport) + transport.register_receiver(protocol) + init_file, image_file = _get_file_names(manifest) - + with open(os.path.join(unpacked_zip_path, init_file), 'rb') as f: init_data = f.read() with open(os.path.join(unpacked_zip_path, image_file), 'rb') as f: image_data = f.read() - - return ThreadDfuServer(transport, init_data, image_data, prefix) + + return ThreadDfuServer(protocol, init_data, image_data, opts) diff --git a/nordicsemi/thread/tncp.py b/nordicsemi/thread/tncp.py index 168effe..2e9cc92 100644 --- a/nordicsemi/thread/tncp.py +++ b/nordicsemi/thread/tncp.py @@ -37,6 +37,7 @@ import time import logging import ipaddress +import struct import io import spinel.common @@ -54,13 +55,13 @@ class NCPTransport(): CFG_KEY_CHANNEL = 'channel' CFG_KEY_PANID = 'panid' CFG_KEY_RESET = 'reset' - + def __init__(self, port, stream_descriptor, config = None): self._port = port self._stream_descriptor = stream_descriptor.split(":") self._config = config if config is not None else self.get_default_config() self._attached = False - + self._udp6_parser = spinel.ipv6.IPv6PacketFactory( ulpf = { 17: spinel.ipv6.UDPDatagramFactory( @@ -70,9 +71,9 @@ def __init__(self, port, stream_descriptor, config = None): } ), }) - - self._receivers = [] - + + self._receivers = [] + @staticmethod def _propid_to_str(propid): for name, value in SPINEL.__dict__.iteritems(): @@ -94,6 +95,7 @@ def _get_property(self, *args): def _attach_to_network(self): props = [ (SPINEL.PROP_IPv6_ICMP_PING_OFFLOAD, 1, 'B'), + (SPINEL.PROP_THREAD_RLOC16_DEBUG_PASSTHRU, 1, 'B'), (SPINEL.PROP_PHY_CHAN, self._config[self.CFG_KEY_CHANNEL], 'H'), (SPINEL.PROP_MAC_15_4_PANID, self._config[self.CFG_KEY_PANID], 'H'), (SPINEL.PROP_NET_IF_UP, 1, 'B'), @@ -118,26 +120,28 @@ def _wpan_receive(self, prop, value, tid): try: pkt = self._udp6_parser.parse(io.BytesIO(value[2:]), spinel.common.MessageInfo()) - + endpoint = collections.namedtuple('endpoint', 'addr port') payload = str(pkt.upper_layer_protocol.payload.to_bytes()) src = endpoint(pkt.ipv6_header.source_address, pkt.upper_layer_protocol.header.src_port) dst = endpoint(pkt.ipv6_header.destination_address, pkt.upper_layer_protocol.header.dst_port) - + for receiver in self._receivers: receiver.receive(payload, src, dst) - + except RuntimeError: pass except Exception as e: logging.exception(e) + else: + logger.warn("Unexpected property received (PROP_ID: {})".format(prop)) return consumed def _build_udp_datagram(self, saddr, sport, daddr, dport, payload): - return spinel.ipv6.IPv6Packet(spinel.ipv6.IPv6Header(saddr, daddr), + return spinel.ipv6.IPv6Packet(spinel.ipv6.IPv6Header(saddr, daddr), spinel.ipv6.UDPDatagram( spinel.ipv6.UDPHeader(sport, dport), spinel.ipv6.UDPBytesPayload(payload))) @@ -164,25 +168,32 @@ def add_ip_address(self, ipaddr): self._wpan.prop_insert_value(SPINEL.PROP_IPV6_ADDRESS_TABLE, arr, str(len(arr)) + 's') logger.debug("Added") - + def print_addresses(self): for addr in self._wpan.get_ipaddrs(): logger.info(unicode(addr)) - + def send(self, payload, dest): - logger.debug("Sending datagram {} {} {} {}".format(self._src_addr, + if (dest.addr.is_multicast): + rloc16 = self._wpan.prop_get_value(SPINEL.PROP_THREAD_RLOC16) + # Create an IPv6 Thread RLOC address from mesh-local prefix and RLOC16 MAC address. + src_addr = ipaddress.ip_address(self._ml_prefix + '\x00\x00\x00\xff\xfe\x00' + struct.pack('>H', rloc16)) + else: + src_addr = self._ml_eid + + logger.debug("Sending datagram {} {} {} {}".format(src_addr, self._port, dest.addr, dest.port)) try: - datagram = self._build_udp_datagram(self._src_addr, + datagram = self._build_udp_datagram(src_addr, self._port, dest.addr, dest.port, payload) except Exception as e: - logging.exception(e) - + logging.exception(e) + self._wpan.ip_send(str(datagram.to_bytes())) def register_receiver(self, callback): @@ -197,12 +208,12 @@ def remove_receiver(self, callback): def open(self): '''Opens transport for communication.''' - self._stream = StreamOpen(self._stream_descriptor[0], self._stream_descriptor[1]) + self._stream = StreamOpen(self._stream_descriptor[0], self._stream_descriptor[1], False) # FIXME: remove node id from constructor after WpanAPI is refactored self._wpan = WpanApi(self._stream, 666) self._wpan.queue_register(SPINEL.HEADER_DEFAULT) self._wpan.queue_register(SPINEL.HEADER_ASYNC) - self._wpan.callback_register(SPINEL.PROP_STREAM_NET, self._wpan_receive) + self._wpan.callback_register(SPINEL.PROP_STREAM_NET, self._wpan_receive) if (self._config[NCPTransport.CFG_KEY_RESET]) and not self._wpan.cmd_reset(): raise Exception('Failed to reset NCP. Please flash connectvity firmware.') @@ -212,7 +223,8 @@ def open(self): logger.error("Failed to attach to the network") raise Exception('Unable to attach') - self._src_addr = ipaddress.ip_address(self._wpan.prop_get_value(SPINEL.PROP_IPV6_ML_ADDR)) + self._ml_eid = ipaddress.ip_address(self._wpan.prop_get_value(SPINEL.PROP_IPV6_ML_ADDR)) + self._ml_prefix = self._wpan.prop_get_value(SPINEL.PROP_IPV6_ML_PREFIX) logger.info("Done") diff --git a/requirements.txt b/requirements.txt index f8817e5..05724ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ protobuf pc_ble_driver_py >= 0.11.3 tqdm piccata -pyspinel +pyspinel == 1.0.0a1