class XBeeFrameBase(Block): """ Generate or interpret XBee frames Parameters: escaped (bool): True uses API mode 2 digimesh (bool): Use DigiMesh protocol rather than XBee (IEEE 802.15.4) """ version = VersionProperty(version='1.0.0') escaped = BoolProperty(title='Escaped characters? (API mode 2)', default=True) digimesh = BoolProperty(title='DigiMesh', default=False) def __init__(self): super().__init__() self._xbee = None self._serial = None self._protocol = xbee.XBee def configure(self, context): super().configure(context) if self.digimesh(): self._protocol = xbee.DigiMesh self._connect() def process_signals(self, signals): for signal in signals: pass def stop(self): try: self.logger.debug('Halting XBee callback thread') self._xbee.halt() self.logger.debug('XBee halted') except: self.logger.exception('Exception while halting xbee') super().stop() def _connect(self): ''' Establish XBee serial connection ''' try: self._serial = serial.Serial(None) self.logger.debug('Escaped is' ': {}'.format(self.escaped())) try: self._xbee = self._protocol(self._serial, escaped=self.escaped()) except: self.logger.exception('An exception occurred') except: self.logger.exception('An failure occurred') def _API_frame_packer(self, data): return xbee.frame.APIFrame(data, self.escaped()).output() def _API_frame_unpacker(self, data): frame = xbee.frame.APIFrame(escaped=self.escaped()) for byte in data: frame.fill(bytes([byte])) frame.parse() return self._xbee._split_response(frame.data)
class XBeeParseFrame(XBeeFrameBase): """Parse Frame. Take a XBee Frame as an input signal and output a signal composed of the individual frame components. Parameters: data: An XBee frame with a start byte and checksum *TODO: allow only the data packet* """ version = VersionProperty(version='1.0.0') data = Property(title="Data", default="{{ $ }}") def process_signals(self, signals): for signal in signals: if isinstance(self.data(signal), bytes): data_encoded = self.data(signal) else: data_encoded = "{}".format(self.data(signal)).encode() try: frame = self._API_frame_unpacker(data_encoded) self.notify_signals([Signal(frame)]) except: self.logger.exception("Failed to parse frame")
class XBeeDigiMeshTXFrame(XBeeFrameBase): """Generate TX_LONG_ADDR command frame This block generates an XBee 0x10 digimesh tx long frame. Parameters: dest_addr: 8 byte address of remote xbee to send AT command to. Default value when left blank is "FF FF" which sends a broadcast. data: Data to send, default is 100 bytes frame_id: Hidden property that can be used to order data. """ version = VersionProperty(version='1.0.0') data = Property(title="Data", default="{{ $.to_dict() }}") dest_addr = Property(title='Destination Address \ (8 bytes hex, ex: "01 23 45 67 89 AA 00 05")', default='', allow_none=True) frame_id = Property(title='Frame id', default="{{ $frame_id }}", hidden=True) def configure(self, context): super().configure(context) self._protocol = xbee.DigiMesh self._connect() def process_signals(self, signals): for signal in signals: data_encoded = "{}".format(self.data(signal)).encode() dest_addr = \ binascii.unhexlify(self.dest_addr(signal).replace(" ", "")) \ if self.dest_addr(signal) else None try: frame_id = binascii.unhexlify(self.frame_id(signal)) self.logger.debug("Frame ID = {}".format(frame_id)) except: frame_id = None self.logger.debug('Creating frame, data: {}'.format(data_encoded)) # tx_long_addr: 0x10 "Tx (Transmit) Request: 64-bit address" # frame_id: 0x01 # dest_addr: 0xFFFF is the broadcast address # data: RF data bytes to be transmitted # # frame_id is an arbitrary value, 1 hex byte, used to associate # sent packets with their responses. If set to 0 no response will # be sent. Could be a block property. packet = self._xbee._build_command( 'tx', id=b'\x10', frame_id=frame_id or b'\x01', dest_addr=dest_addr or b'\x00\x00\x00\x00\x00\x00\xFF\xFF', data=data_encoded, reserved=b'\xFF\xFE', broadcast_radius=b'\x00', options=b'\x00') self.notify_signals( [Signal({"frame": self._API_frame_packer(packet)})])
class XBeeTX(XBeeBase): """Execute TX Command. XBee sends the serialized version of each input signal to thie block. It is sent to the configured "Distnation Address" of the XBee. That destination XBee will receive that serialized signal. If that block is connected to nio then the block will notify the signal. Parameters: dest_addr: 2 or 8 byte address of remote xbee to send AT command to. must be 8 bytes when using digimesh. Default value when left blank is "FF FF" which sends a broadcast. """ version = VersionProperty(version='0.2.1') data = Property(title="Data", default="{{ $.to_dict() }}") dest_addr = Property(title='Destination Address \ (2 or 8 bytes hex, ex: "00 05")', default='', allow_none=True) def process_signals(self, signals): for signal in signals: data_encoded = "{}".format(self.data(signal)).encode() dest_addr = \ binascii.unhexlify(self.dest_addr(signal).replace(" ", "")) \ if self.dest_addr(signal) else None self.logger.debug('Sending data: {}'.format(data_encoded)) # tx: 0x01 "Tx (Transmit) Request: 16-bit address" # tx: 0x10 "Tx (Transmit) Request: 64-bit address", DigiMesh # frame_id: 0x01 # dest_addr: 0xFFFF appears to make it so that it sends to the # configured "Destination Address" on the XBee # data: RF data bytes to be transmitted # # frame_id is an arbitrary value, 1 hex byte, used to associate # sent packets with their responses. If set to 0 no response will # be sent. Could be a block property. if self.digimesh(): # pass all arguments to work around bug in # python-xbee/xbee/digimesh.py where default values are not # bytes self._xbee.send( 'tx', id=b'\x10', frame_id=b'\x01', dest_addr=dest_addr or b'\x00\x00\x00\x00\x00\x00\xFF\xFF', reserved=b'\xFF\xFE', broadcast_radius=b'\x00', options=b'\x00', data=data_encoded ) else: self._xbee.send('tx', frame_id=b'\x01', dest_addr=dest_addr or b'\xFF\xFF', data=data_encoded)
class XBeeATCommandFrame(XBeeFrameBase): """ Generate AT commands frames Parameters: command: The command to execute, ex. 'D0', WR' parameter: The command parameter, ex. '05' for 'D0' command to set pin high frame_id: Hidden parameter to specify frame_id """ version = VersionProperty(version='1.0.0') command = Property(title='AT Command (ascii)', default='ID') parameter = Property(title='Command Parameter (hex, ex: "05")', default='') frame_id = Property(title='Frame id', default="{{ $frame_id }}", hidden=True) def process_signals(self, signals): for signal in signals: try: command = self.command(signal) parameter = self.parameter(signal).replace(" ", "") try: frame_id = binascii.unhexlify(self.frame_id(signal)) self.logger.debug("Frame ID = {}".format(frame_id)) except: frame_id = None self._at(command, parameter, frame_id) except: self.logger.exception("Failed to execute at command") def _at(self, command, parameter, frame_id): command = command.encode('ascii') parameter = binascii.unhexlify(parameter) self.logger.debug( "Executing AT command: {}, with parameter: {}".format( command, parameter)) # at: 0x08 "AT Command" # frame_id: 0x01 # data: RF data bytes to be transmitted # command: The command to execute, ex. 'D0', WR' # parameter: The command parameter, ex. b'\x05' for 'D0' command # to set pin high # # frame_id is an arbitrary value, 1 hex byte, used to associate sent # packets with their responses. If set to 0 no response will be sent. # Could be a block property. packet = self._xbee._build_command('at', frame_id=frame_id or b'\x01', command=command, parameter=parameter) self.notify_signals( [Signal({"frame": self._API_frame_packer(packet)})])
class TrendAnalysis(Block): data = Property(title='Data Set', default='') version = VersionProperty('0.1.0') def process_signals(self, signals): # Create empty list of signals to notify signals_to_notify = [] for signal in signals: if (isinstance(self.data(signal), list) and len(self.data(signal)) > 1): # Make sure input is a list with at least two values dd = self.data(signal) # Plot trend line trend,trend_start = self.linreg(range(len(dd)),dd) trend_d = [ trend * index + trend_start for index in range(len(dd)) ] trend_end = trend_d[len(dd)-1] # Calculate standard error error = [abs(trend_d - dd) for trend_d, dd in zip(trend_d, dd)] std_error = statistics.stdev(error) # Create new signal attributes signal.trend = trend signal.trend_start = trend_start signal.trend_end = trend_end signal.std_error = std_error # Append signal to list signals_to_notify signals_to_notify.append(signal) else: # Raise exception, do not append signals_to_notify self.logger.error( "Data Set must be a list with length > 1" ) self.notify_signals(signals_to_notify) def linreg(self, X, Y): # Perform least-squares-fit of linear regression to a list of numeric # values N = len(X) Sx = Sy = Sxx = Syy = Sxy = 0.0 for x, y in zip(X, Y): Sx = Sx + x Sy = Sy + y Sxx = Sxx + x*x Syy = Syy + y*y Sxy = Sxy + x*y det = Sxx * N - Sx * Sx return (Sxy * N - Sy * Sx)/det, (Sxx * Sy - Sx * Sxy)/det
class XBeeTXFrame(XBeeFrameBase): """Generate TX command frame Parameters: dest_addr: 2 byte hex address of remote xbee to send AT command to. Default value when left blank is "FF FF" which sends a broadcast. data: Data to send, default maximum is 100 bytes frame_id: Hidden parameter to set frame_id """ version = VersionProperty(version='1.0.0') data = Property(title="Data", default="{{ $.to_dict() }}") dest_addr = Property(title='Destination Address \ (2 bytes hex, ex: "00 05")', default='', allow_none=True) frame_id = Property(title='Frame id', default="{{ $frame_id }}", hidden=True) def process_signals(self, signals): for signal in signals: data_encoded = "{}".format(self.data(signal)).encode() dest_addr = \ binascii.unhexlify(self.dest_addr(signal).replace(" ", "")) \ if self.dest_addr(signal) else None try: frame_id = binascii.unhexlify(self.frame_id(signal)) self.logger.debug("Frame ID = {}".format(frame_id)) except: frame_id = None self.logger.debug('Creating frame, data: {}'.format(data_encoded)) # tx: 0x01 "Tx (Transmit) Request: 16-bit address" # frame_id: 0x01 # dest_addr: 0xFFFF is the broadcast address # data: RF data bytes to be transmitted # # frame_id is an arbitrary value, 1 hex byte, used to associate # sent packets with their responses. If set to 0 no response will # be sent. Could be a block property. packet = self._xbee._build_command('tx', frame_id=frame_id or b'\x01', dest_addr=dest_addr or b'\xFF\xFF', data=data_encoded) self.notify_signals([Signal( { "frame" : self._API_frame_packer(packet) } )])
class XBeeATCommand(XBeeBase): """ Execute AT commands Parameters: command: The command to execute, ex. 'D0', WR' parameter: The command parameter, ex. '05' for 'D0' command to set pin high """ version = VersionProperty(version='0.1.0') command = Property(title='AT Command (ascii)', default='ID') parameter = Property(title='Command Parameter (hex, ex: "05")', default='') def process_signals(self, signals): for signal in signals: try: command = self.command(signal) parameter = self.parameter(signal).replace(" ", "") self._at(command, parameter) except: self.logger.exception("Failed to execute at command") def _at(self, command, parameter): command = command.encode('ascii') parameter = binascii.unhexlify(parameter) self.logger.debug( "Executing AT command: {}, with parameter: {}".format( command, parameter) ) # at: 0x08 "AT Command" # frame_id: 0x01 # data: RF data bytes to be transmitted # command: The command to execute, ex. 'D0', WR' # parameter: The command parameter, ex. b'\x05' for 'D0' command # to set pin high # # frame_id is an arbitrary value, 1 hex byte, used to associate sent # packets with their responses. If set to 0 no response will be sent. # Could be a block property. self._xbee.send('at', frame_id=b'\x01', command=command, parameter=parameter)
class IdentityIntervalSimulator(MultipleSignals, IdentityGenerator, IntervalTrigger, GeneratorBlock): version = VersionProperty('1.2.0')
class CounterSafeSimulator(CounterGenerator, SafeTrigger, GeneratorBlock): version = VersionProperty('1.1.0')
class StateBase(Persistence, GroupBy, Block): """ A base block mixin for keeping track of state """ state_expr = Property( title='State ', default='{{ $state }}', allow_none=True, order=0) initial_state = Property( title='Initial State', default='{{ None }}', allow_none=True, order=1) version = VersionProperty('0.1.0') def __init__(self): super().__init__() self._initial_state = None self._state_locks = defaultdict(Lock) self._safe_lock = Lock() self._states = {} def persisted_values(self): """Persist states using block mixin.""" return ["_states"] def configure(self, context): super().configure(context) # Store a cached copy of what a new state should look like self._initial_state = self.initial_state(Signal()) def get_state(self, group): """ Return the current state for a group. If the state has not been set yet, this function will return the initial state configured for the block. It will also set that as the state. """ if group not in self._states: self._states[group] = copy(self._initial_state) return self._states[group] def process_signals(self, signals, input_id=None): """ Process incoming signals. This block is a helper, it will just call _process_group and notify any signals that get appeneded to the to_notify list. Most likely, _process_group will be overridden in subclasses instead of this method. """ self.logger.debug( "Ready to process {} incoming signals".format(len(signals))) signals_to_notify = defaultdict(list) with self._safe_lock: group_result = self.for_each_group( self._process_group, signals, input_id=input_id, signals_to_notify=signals_to_notify) if group_result: signals_to_notify[None] = group_result for output_id in signals_to_notify: if output_id: self.notify_signals(signals_to_notify[output_id], output_id=output_id) else: self.notify_signals(signals_to_notify[output_id]) def _process_group(self, signals, group, input_id, signals_to_notify=None): """ Implement this method in subclasses to process signals in a group. Return: list or dict(list): The list of signals to be nofified. If notifying to a non-default input, return a dict with the key as the output id. """ pass def _process_state(self, signal, group): """ Changes state based on a signal and a group. If the signal cannot be processed, the state remains unchanged. Returns: Tuple: (prev_sate, new_state) if the state was changed None - if the state did not change """ with self._state_locks[group]: prev_state = self.get_state(group) new_state = self.state_expr(signal) if new_state != prev_state: # notify signal if there was a prev_state and # the state has changed. self.logger.debug( "Changing state from {} to {} for group {}".format( prev_state, new_state, group)) self._states[group] = new_state return (prev_state, new_state) def current_state(self, group): """ Command that returns the current state of a group """ if group is None: return_list = [] with self._safe_lock: for group in self._states: return_list.append({"group": group, "state": self._states[group]}) return return_list if group in self._states: return {"group": group, "state": self._states[group]} else: raise HTTPNotFound()
class HostSpecs(Block): version = VersionProperty("0.1.0") menu = ObjectProperty(Menu, title='Menu', default=Menu()) def __init__(self): super().__init__() self._retry_count = 0 def process_signals(self, signals): plat = self.platform() self.notify_signals([Signal(plat)]) def platform(self): '''Returns platform data ''' keys = [] out = {} if self.menu().machine(): keys.append('machine') if self.menu().os_version(): keys.append('version') if self.menu().platform(): keys.append('platform') if self.menu().dist(): keys.append('dist') if self.menu().system(): keys.append('system') if self.menu().node(): keys.append('node') if len(keys) > 0: out = {key: getattr(platform, key)() for key in tuple(keys)} if self.menu().python(): out['python'] = { key: getattr(platform, "python_" + key)() for key in ('implementation', 'compiler', 'version') } out['python']['architecture'] = \ int(ctypes.sizeof(ctypes.c_voidp) * 8) if self.menu().processor(): out['processor'] = self._get_processor() out['cores'] = len(psutil.cpu_percent(percpu=True)) if self.menu().mac(): out['MAC'] = hex(get_mac())[2:].upper() return out def _get_processor(self): '''Get type of processor http://stackoverflow.com/questions/4842448/ getting-processor-information-in-python ''' out = None if platform.system() == "Windows": out = platform.processor() elif platform.system() == "Darwin": path = os.environ['PATH'] os.environ['PATH'] = os.environ['PATH'] + os.pathsep + '/usr/sbin' try: command = "sysctl -n machdep.cpu.brand_string" out = subprocess.check_output(command, shell=True)\ .strip().decode() finally: os.environ['PATH'] = path elif platform.system() == "Linux": command = "cat /proc/cpuinfo" all_info = subprocess.check_output(command, shell=True)\ .strip().decode() for line in all_info.split("\n"): if "model name" in line: out = re.sub(".*model name.*:", "", line, 1) if "Hz" not in out: command_add = "lscpu | grep MHz" clock_info = subprocess.check_output(command_add, shell=True)\ .strip().decode() out = out + ' ' + \ [n for n in clock_info.split('\n')][0].split()[3] + "MHz" if out is None: return platform.processor() else: return out
class ControlBands(GroupBy, Persistence, Block): band_interval = TimeDeltaProperty(default={"days": 1}, title="Band Interval") value_expr = Property(default="{{ $value }}", title="Value") version = VersionProperty("1.0.2") def __init__(self): super().__init__() self._band_values = defaultdict(list) self._signals_lock = Lock() def process_signals(self, signals, input_id='default'): sigs_out = self.for_each_group(self.record_values, signals) if sigs_out: self.notify_signals(sigs_out) def persisted_values(self): """ Overridden from persistence mixin """ return ['_band_values'] def record_values(self, signals, group): """ Save the time and the list of signals for each group. This will return signals with the mean/band data included on them """ sigs_out = [] with self._signals_lock: ctime = _time() # First get rid of the old values self.trim_old_values(group, ctime) prev_values = self._get_current_values(group) self.logger.debug( "Previous values for group: {}".format(prev_values)) # Start off a new band data using the latest value from the # previous band data objects new_values = BandData(prev_values.last_val) for sig in signals: try: # the value must be a floating point value value = float(self.value_expr(sig)) # Add the moving range data to the signal and add it to # the list of signals to notify sigs_out.append( self._enrich_signal(sig, prev_values + new_values, value)) # Now account for the latest value in the moving range data new_values.register_value(value) except: self.logger.exception( "Unable to determine value for signal {}".format(sig)) # Archive the new values if new_values.count_items: self._band_values[group].append((ctime, new_values)) return sigs_out def _enrich_signal(self, signal, band_data, value): """ Add relevant band data to the signal. Args: signal: The signal that we should add data to band_data (BandData): A single BandData object containing the current moving range information value: The value this signal contributed to the band data. This is used to determine how many deviations from the mean it is. Returns: The signal with updated data """ range_mean = band_data.get_mean() range_deviation = band_data.get_range() if range_deviation != 0: deviations = (value - range_mean) / range_deviation else: deviations = 0 class BandSignalData(): def __init__(self, value, mean, deviation, deviations): self.value = value self.mean = mean self.deviation = deviation self.deviations = deviations def to_dict(self): """represent all BandSignalData attributes as a dict""" return self.__dict__ setattr( signal, 'band_data', BandSignalData(value, range_mean, range_deviation, deviations).to_dict()) return signal def _get_current_values(self, group): """ Returns a single BandData object for a group. This will make use of the __add__ function in the BandData class to sum together all of the current data points in the group. The result will be a single BandData object with all of the previously saved points accounted for. """ cur_values = self._band_values[group] if len(cur_values) > 1: # Sum every BandData (after the first), using the first one as # the starting point return sum([data[1] for data in cur_values[1:]], cur_values[0][1]) elif len(cur_values) == 1: return cur_values[0][1] else: return BandData() def trim_old_values(self, group, ctime): """ Remove any "old" saved values for a given group """ group_values = self._band_values[group] self.logger.debug("Trimming old values - had {} items".format( len(group_values))) group_values[:] = [ data for data in group_values if data[0] > ctime - self.band_interval().total_seconds() ] self.logger.debug("Now has {} items".format(len(group_values)))
class XBeeRemoteATFrame(XBeeFrameBase): """Generate Remote AT command frame Parameters: command: The command to execute, ex. 'D0', WR' parameter: The command parameter, ex. '05' for 'D0' command to set pin high dest_addr: 2 or 8 byte address of remote xbee to send AT command to. must be 8 bytes when using digimesh. Default value when left blank is "FF FF" which sends a broadcast. frame_id: Hidden parameter to set frame_id """ version = VersionProperty(version='1.0.0') command = Property(title='AT Command (ascii)', default='ID') parameter = Property(title='Command Parameter (hex, ex: "05")', default='') dest_addr = Property(title='Destination Address \ (2 or 8 bytes hex, ex: "00 05")', default='', allow_none=True) frame_id = Property(title='Frame id', default="{{ $frame_id }}", hidden=True) def process_signals(self, signals): for signal in signals: try: command = self.command(signal).encode('ascii') parameter = \ binascii.unhexlify(self.parameter(signal).replace(" ", "")) dest_addr = \ binascii.unhexlify( self.dest_addr(signal).replace(" ", "")) \ if self.dest_addr(signal) else None try: frame_id = binascii.unhexlify(self.frame_id(signal)) self.logger.debug("Frame ID = {}".format(frame_id)) except: frame_id = None self._remote_at(command, parameter, dest_addr, frame_id) except: self.logger.exception("Failed to execute remote at command") def _remote_at(self, command, parameter, dest_addr, frame_id): self.logger.debug( "Executing Remote AT command: {}, with parameter: {}".format( command, parameter)) # remote_at: 0x17 "Remote AT Command" # frame_id: 0x01 # dest_addr: 0xFFFF broadcasts to all XBees # data: RF data bytes to be transmitted # command: The command to execute, ex. 'D0', WR' # parameter: The command parameter, ex. b'\x05' for 'D0' command # to set pin high # # frame_id is an arbitrary value, 1 hex byte, used to associate sent # packets with their responses. If set to 0 no response will be sent. # Could be a block property. if dest_addr is not None and len(dest_addr) == 8: packet = self._xbee._build_command('remote_at', frame_id=frame_id or b'\x01', dest_addr_long=dest_addr or b'\x00\x00\x00\x00\x00\x00\xFF\xFF', command=command, parameter=parameter) else: packet = self._xbee._build_command('remote_at', frame_id=frame_id or b'\x01', dest_addr=dest_addr or b'\xFF\xFF', command=command, parameter=parameter) self.notify_signals([Signal( { "frame" : self._API_frame_packer(packet) } )])
class CounterIntervalSimulator(MultipleSignals, CounterGenerator, IntervalTrigger, GeneratorBlock): version = VersionProperty("0.2.0")
class FileIntervalSimulator(MultipleSignals, FileGenerator, IntervalTrigger, GeneratorBlock): version = VersionProperty('1.3.0')
class IdentityCronSimulator(MultipleSignals, IdentityGenerator, CronTrigger, GeneratorBlock): version = VersionProperty('0.1.0')
class XBeeRemoteAT(XBeeBase): """Execute Remote AT commands. Parameters: command: The command to execute, ex. 'D0', WR' parameter: The command parameter, ex. '05' for 'D0' command to set pin high dest_addr: 2 or 8 byte address of remote xbee to send AT command to. must be 8 bytes when using digimesh. Default value when left blank is "FF FF" which sends a broadcast. """ version = VersionProperty(version='0.1.0') command = Property(title='AT Command (ascii)', default='ID') parameter = Property(title='Command Parameter (hex, ex: "05")', default='') dest_addr = Property(title='Destination Address \ (2 or 8 bytes hex, ex: "00 05")', default='', allow_none=True) def process_signals(self, signals): for signal in signals: try: command = self.command(signal).encode('ascii') parameter = binascii.unhexlify( self.parameter(signal).replace(" ", "")) dest_addr = binascii.unhexlify( self.dest_addr(signal).replace(" ", "")) if \ self.dest_addr(signal) else None self._remote_at(command, parameter, dest_addr) except: self.logger.exception("Failed to execute remote at command") def _remote_at(self, command, parameter, dest_addr): self.logger.debug( "Executing Remote AT command: {}, with parameter: {}".format( command, parameter)) # remote_at: 0x17 "Remote AT Command" # frame_id: 0x01 # dest_addr: 0xFFFF broadcasts to all XBees # data: RF data bytes to be transmitted # command: The command to execute, ex. 'D0', WR' # parameter: The command parameter, ex. b'\x05' for 'D0' command # to set pin high # # frame_id is an arbitrary value, 1 hex byte, used to associate sent # packets with their responses. If set to 0 no response will be sent. # Could be a block property. if self.digimesh(): # pass all arguments to work around bug in # python-xbee/xbee/digimesh.py where default values are not bytes self._xbee.send('remote_at', id=b'\x17', frame_id=b'\x01', dest_addr_long=dest_addr or b'\x00\x00\x00\x00\x00\x00\xFF\xFF', reserved=b'\xFF\xFE', options=b'\x02', command=command, parameter=parameter) else: self._xbee.send('remote_at', frame_id=b'\x01', dest_addr=dest_addr or b'\xFF\xFF', command=command, parameter=parameter)