コード例 #1
0
    def __init__(self, adapter, port):
        """
        :param adapter: The adapter that will serve as an interface for interacting with the broker
        """
        self._port = port
        # A list of items that we will need to discover for.
        # The base protocol will use this dictionary to feed items to
        # the UI
        self.items = []
        self._error_codes = []

        # The list of connected item IDs found in the initial sweep in
        # connectionMade()
        self._item_ids = []
        BaseProtocol.__init__(self)

        # Set the LineReceiver to line mode. This causes lineReceived to be called
        # when data is sent to the serial port. We will get a line whenever the END_BYTE
        # appears in the buffer
        self.setLineMode()
        self.delimiter = END_BYTE_STR

        # The buffer that we will be storing the data that arrives via the serial connection
        self._binary_buffer = bytearray()

        self.adapter = adapter

        # Event IDs are 16-bit (2 byte) numbers so we need a radix
        # of 65535 or 0xFFFF in hex
        # NOTE: The number of bits in an event ID is subject to change,
        # the constant NUM_EVENT_ID_BITS can easily be changed to accommodate this.
        self._event_id_generator = message_id_generator((2**self.NUM_EVENT_ID_BITS))

        # From parlay.utils, calls _message_queue_handler() whenever
        # a new message is added to the MessageQueue object
        self._message_queue = MessageQueue(self._message_queue_handler)
        self._attached_item_d = None

        # Dictionary that maps ID # to Deferred object
        self._discovery_msg_ids = {}

        # Sequence number is a nibble as of now, so the domain should be
        # 0 <= seq number <= 15
        # which means the radix will be 16, but to be safe I'll do
        # 2^SEQ_BITS where SEQ_BITS is a member constant that can easily be changed
        self._seq_num = message_id_generator((2**self.SEQ_BITS))

        # ACKs should be deferred objects because you want to catch them on the way
        # back via asynchronous communication.
        self._ack_deferred = defer.Deferred()

        # Store discovered item IDs so that we do not push duplicates to the
        # item requesting the discovery

        self._in_progress = False
        self._discovery_deferred = defer.Deferred()

        self._ack_table = {seq_num : defer.Deferred() for seq_num in range(2**self.SEQ_BITS)}

        self._ack_window = SlidingACKWindow(self.WINDOW_SIZE, self.NUM_RETRIES)
コード例 #2
0
ファイル: threaded_item.py プロジェクト: jedgentry/Parlay
    def __init__(self,
                 item_id,
                 name,
                 reactor=None,
                 adapter=None,
                 parents=None):
        BaseItem.__init__(self,
                          item_id,
                          name,
                          adapter=adapter,
                          parents=parents)
        self._reactor = self._adapter.reactor if reactor is None else reactor
        self._msg_listeners = []
        self._system_errors = []
        self._system_events = []
        self._timer = None

        self._auto_update_discovery = True  #: If True auto update discovery with broadcast discovery messages
        self.discovery = {}  #: The current discovery information to pull from

        self._message_id_generator = message_id_generator(65535, 100)

        # Add this listener so it will be first in the list to pickup errors, warnings and events.
        self.add_listener(self._system_listener)
        self.add_listener(self._discovery_request_listener)

        self._adapter.subscribe(self._discovery_broadcast_listener,
                                type='DISCOVERY_BROADCAST')
コード例 #3
0
    def __init__(self, item_id, name, reactor=None, adapter=None):
        BaseItem.__init__(self, item_id, name, adapter=adapter)
        self._reactor = self._adapter.reactor if reactor is None else reactor
        self._msg_listeners = []
        self._system_errors = []
        self._system_events = []
        self._timer = None

        self._auto_update_discovery = True  #: If True auto update discovery with broadcast discovery messages
        self.discovery = {}  #: The current discovery information to pull from

        self._message_id_generator = message_id_generator(65535, 100)

        # Add this listener so it will be first in the list to pickup errors, warnings and events.
        self.add_listener(self._system_listener)
        self.add_listener(self._discovery_request_listener)

        self._adapter.subscribe(self._discovery_broadcast_listener, type='DISCOVERY_BROADCAST')
コード例 #4
0
class ParlayCommandItem(ParlayStandardItem):
    """
    This is a Parlay Item that defines functions that serve as commands,
    with arguments.

    This class enables you to use the :func:`~parlay_standard.parlay_command` decorator over your
    command functions.  Then, those functions will be available as commands
    that can be called from the user interface, scripts, or by other items.

    **Example: How to define a class as a ParlayCommandItem**::

        from parlay import local_item, ParlayCommandItem, parlay_command

        @local_item()
        class MotorSimulator(ParlayCommandItem):

            def __init__(self, item_id, item_name):
                self.coord = 0
                ParlayCommandItem.__init__(self, item_id, item_name)

            @parlay_command
            def move_to_coordinate(self, coordinate)
                self.coord = coordinate

    **Example: How to instantiate an item from the above definition**::

        import parlay
        from motor_sim import MotorSimulator

        MotorSimulator("motor1", "motor 1")  # motor1 will be discoverable
        parlay.start()


    **Example: How to interact with the instantiated item from a Parlay script**::

        # script_move_motor.py

        setup()
        discover()
        motor_sim = get_item_by_name("motor 1")
        motor_sim.move_to_coordinate(500)

    """

    #! change this to have a custom subsystem ID for the entire Python subsystem
    SUBSYSTEM_ID = "python"

    # id generator for auto numbering class instances
    __ID_GEN = message_id_generator(2**32, 1)

    def __init__(self, item_id=None, name=None, reactor=None, adapter=None, parents=None):
        """
        :param item_id : The id of the Item (Must be unique in this system)
        :type item_id str | int
        :param name : the human readible name of this item. (Advised to be unique, but not required)
        :type name str
        :rtype : object
        """
        if item_id is None:
            item_id = ParlayCommandItem.SUBSYSTEM_ID + "." + self.__class__.__name__ + "." + str(ParlayCommandItem.__ID_GEN.next())

        if name is None:
            name = self.__class__.__name__

        ParlayStandardItem.__init__(self, item_id, name, reactor, adapter, parents)
        self._commands = {}  # dict with command name -> callback function

        # ease of use deferred for wait* functions
        self._wait_for_next_sent_message = defer.Deferred()
        self._wait_for_next_recv_message = defer.Deferred()

        self.subscribe(self._wait_for_next_recv_msg_subscriber, TO=self.item_id)
        self.subscribe(self._wait_for_next_sent_msg_subscriber, FROM=self.item_id)

        # add any function that have been decorated
        for member_name in [x for x in dir(self) if not x.startswith("__")]:
            member = self.__class__.__dict__.get(member_name, None)
            # are we a method? and do we have the flag, and is it true?
            if callable(member) and hasattr(member, "_parlay_command") and member._parlay_command:
                # get a reference to this OBJECTs bound functiion now, now the class's unbound function
                member = getattr(self, member_name, {})
                self._commands[member_name] = member
                # build the sub-field based on their signature
                arg_names = member._parlay_fn.func_code.co_varnames[1:member._parlay_fn.func_code.co_argcount]   # remove self

                # get a list of the default arguments
                # (don't use argspec because it is needlesly strict and fails on perfectly valid Cython functions)
                defaults = member._parlay_fn.func_defaults if member._parlay_fn.func_defaults is not None else []
                # cut params to only the last x (defaults are always at the end of the signature)
                params = arg_names
                params = params[len(params) - len(defaults):]
                default_lookup = dict(zip(params, defaults))

                # add the sub_fields, trying to best guess their discovery types. If not possible then default to STRING
                member.__func__._parlay_sub_fields = [self.create_field(x,
                                                                        member._parlay_arg_discovery.get(x, INPUT_TYPES.STRING),
                                                                        default=default_lookup.get(x, None))
                                                      for x in arg_names]

        # run discovery to init everything for a first time
        self.get_discovery()



    def get_discovery(self):
        """
        Will auto-populate the UI with inputs for commands
        """
        # start fresh
        self.clear_fields()
        self._add_commands_to_discovery()
        self._add_properties_to_discovery()
        self._add_datastreams_to_discovery()

        # call parent
        return ParlayStandardItem.get_discovery(self)

    def _add_commands_to_discovery(self):
        """
        Add commands to the discovery for user input
        """

        if len(self._commands) == 0:
            return  # nothing to do here

        else:  # more than 1 option
            command_names = self._commands.keys()
            command_names.sort()  # pretty-sort

            # add the command selection dropdown
            self.add_field("COMMAND", INPUT_TYPES.DROPDOWN, label='command', default=command_names[0],
                           dropdown_options=[(x, x) for x in command_names],
                           dropdown_sub_fields=[self._commands[x]._parlay_sub_fields for x in command_names])

    def _add_properties_to_discovery(self):
        """
        Add properties to discovery
        """
        # clear properties
        self._properties = {}
        for member_name in [x for x in dir(self.__class__) if not x.startswith("__")]:
            member = self.__class__.__dict__.get(member_name, None)
            if isinstance(member, ParlayProperty):
                self.add_property(member_name, member_name,  # lookup type name based on type func (e.g. int())
                                  INPUT_TYPE_DISCOVERY_LOOKUP.get(member._val_type.__name__, "STRING"),
                                  read_only=member._read_only, write_only=member._write_only)

    def _add_datastreams_to_discovery(self):
        """
        Add properties to discovery
        """
        # clear properties
        self._datastreams = {}
        for member_name in sorted([x for x in dir(self.__class__) if not x.startswith("__")]):
            member = self.__class__.__dict__.get(member_name, None)
            if isinstance(member, ParlayDatastream) or isinstance(member, ParlayProperty):
                self.add_datastream(member_name, member_name, "")

    def _send_parlay_message(self, msg):
        self.publish(msg)

    def on_message(self, msg):
        """
        Will handle command messages automatically.
        Returns True if the message was handled, False otherwise
        """
        # run it through the listeners for processing

        self._runListeners(msg)

        topics, contents = msg["TOPICS"], msg["CONTENTS"]
        msg_type = topics.get("MSG_TYPE", "")
        # handle property messages
        if msg_type == "PROPERTY":
            action = contents.get('ACTION', "")
            property_id = str(contents.get('PROPERTY', ""))
            try:
                if action == 'SET':
                    assert 'VALUE' in contents  # we need a value to set!
                    setattr(self, self._properties[property_id]["ATTR_NAME"], contents['VALUE'])
                    self.send_response(msg, {"PROPERTY": property_id, "ACTION": "RESPONSE"})
                    return True
                elif action == "GET":
                    def on_get(val):
                        self.send_response(msg, {"PROPERTY": property_id, "ACTION": "RESPONSE", "VALUE": val})

                    defer.maybeDeferred(lambda: getattr(self, self._properties[property_id]["ATTR_NAME"]))\
                        .addCallback(on_get)
                    return True
            except Exception as e:
                self.send_response(msg, {"PROPERTY": property_id, "ACTION": "RESPONSE", "DESCRIPTION": str(e)},
                                   msg_status=MSG_STATUS.ERROR)

        # handle data stream messages (that aren't value messages)
        if msg_type == "STREAM" and 'VALUE' not in contents:
            try:
                stream_id = str(contents["STREAM"])
                remove = contents.get("STOP", False)
                requester = topics["FROM"]

                def sample(stream_value):
                    self.send_message(to=requester, msg_type=MSG_TYPES.STREAM, contents={'VALUE': stream_value},
                                      extra_topics={"STREAM": stream_id})

                if remove:
                    # if we've been asked to unsubscribe
                    # access the stream object through the class's __dict__ so we don't just end up calling the __get__()
                    self.__class__.__dict__[stream_id].stop(self, requester)
                else:
                    #listen in if we're subscribing
                    # access the stream object through the class's __dict__ so we don't just end up calling the __get__()
                    self.__class__.__dict__[stream_id].listen(self, sample, requester)


            except Exception as e:
                self.send_response(msg, {"STREAM": contents.get("STREAM", "__UNKNOWN_STREAM__"), "ACTION": "RESPONSE",
                                         "DESCRIPTION": str(e)},
                                   msg_status=MSG_STATUS.ERROR)

        # handle 'command' messages
        command = contents.get("COMMAND", "")
        if command in self._commands:
            arg_names = ()
            try:
                method = self._commands[command]
                arg_names = method._parlay_fn.func_code.co_varnames[1: method._parlay_fn.func_code.co_argcount]  # remove 'self'
                # add the defaults to the msg if they're not overwritten
                defaults = method._parlay_fn.func_defaults if method._parlay_fn.func_defaults is not None else []
                # cut params to only the last x (defaults are always at the end of the signature)
                params = arg_names
                params = params[len(params) - len(defaults):]
                default_lookup = dict(zip(params, defaults))
                for k,v in default_lookup.iteritems():
                    if k not in msg["CONTENTS"] or msg["CONTENTS"][k] is None:
                        msg["CONTENTS"][k] = v

                kws = {k: msg["CONTENTS"][k] for k in arg_names}
                try:
                    # do any type conversions (default to whatever we were sent if no conversions)
                    kws = {k: method._parlay_arg_conversions[k](v) if k in method._parlay_arg_conversions
                                                                   else v for k, v in kws.iteritems()}
                except (ValueError, TypeError) as e:
                    self.send_response(msg, contents={"DESCRIPTION": e.message, "ERROR": "BAD TYPE"},
                                       msg_status=MSG_STATUS.ERROR)
                    return None

                # try to run the method, return the data and say status ok
                def run_command():
                    return method(**kws)

                self.send_response(msg, msg_status=MSG_STATUS.PROGRESS)

                result = defer.maybeDeferred(run_command)
                result = log_stack_on_error(result)
                result.addCallback(lambda r: self.send_response(msg, {"RESULT": r}))

                def bad_status_errback(f):
                    # is this an explicitly bad status?
                    if isinstance(f.value, BadStatusError):
                        error = f.value
                        self.send_response(msg, contents={"DESCRIPTION": error.description, "ERROR": error.error},
                                           msg_status=MSG_STATUS.ERROR)

                    # or is it unknown generic exception?
                    else:
                        self.send_response(msg, contents={"DESCRIPTION": f.getErrorMessage(),
                                                          "TRACEBACK": f.getTraceback()}, msg_status=MSG_STATUS.ERROR)

                # if we get an error, then return it
                result.addErrback(bad_status_errback)

            except KeyError as e:
                self.send_response(msg, contents={"DESCRIPTION": "Missing Argument '%s' to command '%s'" %
                                                                 (e.args[0], command),
                                                  "TRACEBACK": ""}, msg_status=MSG_STATUS.ERROR)


            return True

        else:
            return False

    def _wait_for_next_sent_msg_subscriber(self, msg):
        d = self._wait_for_next_sent_message
        # set up new one before calling callback in case things triggered by the callback need to wait for the next sent
        self._wait_for_next_sent_message = defer.Deferred()
        d.callback(msg)

    def _wait_for_next_recv_msg_subscriber(self, msg):
        d = self._wait_for_next_recv_message
        # set up new one before calling callback in case things triggered by the callback need to wait for the next sent
        self._wait_for_next_recv_message = defer.Deferred()
        d.callback(msg)

    def wait_for_next_sent_msg(self):
        """
        Returns a deferred that will callback on the next message we SEND
        """
        return self._wait_for_next_sent_message

    def wait_for_next_recv_msg(self):
        """
        Returns a deferred that will callback on the next message we RECEIVE
        """
        return self._wait_for_next_recv_message

    def send_response(self, msg, contents=None, msg_status=MSG_STATUS.OK):
        if contents is None:
            contents = {}

        # swap to and from
        to, from_ = msg["TOPICS"]["FROM"], msg["TOPICS"]['TO']

        self.send_message(to, from_, tx_type=TX_TYPES.DIRECT, msg_type=MSG_TYPES.RESPONSE,
                          msg_id=msg["TOPICS"]["MSG_ID"], msg_status=msg_status, response_req=False,
                          contents=contents)
コード例 #5
0
class PCOMMessage(object):

    _item_lookup_map = {}

    # If we get a string ID , we need to assign a item ID. Start at 0xfc00 and go to 0xffff
    _item_id_generator = message_id_generator(0xffff, 0xfc00)

    VALID_JSON_MESSAGE_TYPES = [
        "COMMAND", "EVENT", "RESPONSE", "PROPERTY", "STREAM"
    ]

    GLOBAL_ERROR_CODE_ID = 0xff

    def __init__(self,
                 to=None,
                 from_=None,
                 msg_id=0,
                 tx_type=None,
                 msg_type=None,
                 attributes=0,
                 response_code=None,
                 response_req=None,
                 msg_status=None,
                 contents=None,
                 data=None,
                 data_fmt=None,
                 topics=None,
                 description=''):

        # TODO: Change response_req to response_code

        # private variables only accessed through @property functions
        self._attributes = None
        self._format_string = ''
        self._data = []

        self.description = description

        self.to = to
        self.from_ = from_
        self.msg_id = msg_id
        self.tx_type = tx_type
        self.msg_type = msg_type
        self.response_req = response_req
        self.msg_status = msg_status
        self.contents = contents
        self.priority = 0
        self.attributes = attributes
        self.format_string = data_fmt
        self.data = data
        self.response_code = response_code
        self.topics = topics

    @classmethod
    def _get_item_id(cls, name):
        """
        Gets a item ID from an item name
        """

        # if we're an int we're good
        if type(name) == int:
            return name

        if name in cls._item_lookup_map:
            return cls._item_lookup_map[name]

        else:
            # if the item ID wasn't in our map, generate an int for it
            # and add it to the map
            item_id = cls._item_id_generator.next()
            cls._item_lookup_map[name] = item_id
            cls._item_lookup_map[item_id] = name
            return item_id

    @classmethod
    def _get_name_from_id(cls, item_id):
        """
        Gets a item name from an item ID
        """

        if item_id in cls._item_lookup_map:
            return cls._item_lookup_map[item_id]

        return item_id

    @staticmethod
    def _look_up_id(map, destination_id, name):
        if isinstance(name, basestring):
            # TODO: use .get() to avoid key error
            return map[destination_id].get(name, None)
        else:
            return name

    @classmethod
    def _get_data_format(cls, msg):
        """
        Takes a msg and does the appropriate table lookup to obtain the
        format data for the command/property/stream.

        Returns a tuple in the form of (data, format)
        where data is a list and format is a format string.

        :param msg:
        :return:
        """

        data = []
        fmt = ''
        if msg.msg_type == "COMMAND":
            # If the message type is "COMMAND" there should be an
            # entry in the 'CONTENTS' table for the command ID
            if msg.to in pcom_serial.PCOM_COMMAND_MAP:
                # command will be a CommandInfo object that has a list of parameters and format string
                command_id = msg.contents.get("COMMAND", INVALID_ID)
                command_int_id = cls._look_up_id(
                    pcom_serial.PCOM_COMMAND_NAME_MAP, msg.to, command_id)
                if command_int_id is None:
                    logger.error(
                        "Could not find integer command ID for command name: {0}"
                        .format(command_id))
                    return
                # TODO: check for KeyError
                command = pcom_serial.PCOM_COMMAND_MAP[msg.to].get(
                    command_int_id, None)
                if command is None:
                    return data, fmt

                fmt = str(msg.contents.get('__format__', command["format"]))
                for param in command["input params"]:
                    # TODO: May need to change default value to error out
                    data.append(msg.contents.get(str(param), 0))

        elif msg.msg_type == "PROPERTY":
            # If the message type is a "PROPERTY" there should be
            # a "PROPERTY" entry in the "CONTENTS" that has the property ID

            action = msg.contents.get('ACTION', None)

            if action == "GET":
                data = []
                fmt = ''
            elif action == "SET":
                if msg.to in pcom_serial.PCOM_PROPERTY_MAP:
                    property_id = msg.contents.get("PROPERTY", INVALID_ID)
                    property = cls._look_up_id(
                        pcom_serial.PCOM_PROPERTY_NAME_MAP, msg.to,
                        property_id)
                    if property is None:
                        logger.error(
                            "Could not find integer property ID for property name: {0}"
                            .format(property))
                        return data, fmt
                    prop = pcom_serial.PCOM_PROPERTY_MAP[msg.to][property]
                    fmt = prop["format"]
                    content_data = msg.contents.get('VALUE', 0)
                    if type(content_data
                            ) == list:  # we have a variable property list
                        data = content_data
                    else:
                        data.append(content_data)
                    data = serial_encoding.cast_data(fmt, data)

        elif msg.msg_type == "STREAM":
            # no data or format string for stream messages
            rate = msg.contents.get("RATE", None)
            data = [rate] if rate else []
            fmt = 'f' if rate else ''

        return data, fmt

    @classmethod
    def from_json_msg(cls, json_msg):
        """
        Converts a dictionary message to a PCOM message object

        :param json_msg: JSON message
        :return: PCOM message object
        """

        msg_id = json_msg['TOPICS']['MSG_ID']

        to = cls._get_item_id(json_msg['TOPICS']['TO'])
        from_ = cls._get_item_id(json_msg['TOPICS']['FROM'])

        msg_type = json_msg['TOPICS']['MSG_TYPE']

        response_req = json_msg['TOPICS'].get("RESPONSE_REQ", False)

        msg_status = 0  # TODO: FIX THIS
        tx_type = json_msg['TOPICS'].get('TX_TYPE', "DIRECT")

        contents = json_msg['CONTENTS']
        topics = json_msg['TOPICS']

        msg = cls(to=to,
                  from_=from_,
                  msg_id=msg_id,
                  response_req=response_req,
                  msg_type=msg_type,
                  msg_status=msg_status,
                  tx_type=tx_type,
                  contents=contents,
                  topics=topics)

        # Set data and format using class function
        msg.data, msg.format_string = cls._get_data_format(msg)
        return msg

    def _is_response_req(self):
        """
        If the msg is an order a response is expected.
        :return:
        """

        return (self.category()) == MessageCategory.Order

    @staticmethod
    def get_subsystem(id):
        """"
        Gets the subsystem of the message.
        """
        return (id & SUBSYSTEM_MASK) >> SUBSYSTEM_SHIFT

    def _get_data(self):
        """
        Helper function for returning the data of the PCOM Message. Returns an error message if there
        wasn't any data to get.
        :param index:
        :return:
        """
        if len(self.data) == 1:
            return self.data[0]
        if len(self.data) > 1:
            return self.data
        return None

    def get_tx_type_from_id(self, id):
        """
        Given an ID, returns the msg['TOPICS']['TX_TYPE'] that should be assigned
        :param id: destination item ID
        :return:
        """
        subsystem_id = self.get_subsystem(id)
        return "BROADCAST" if subsystem_id == BROADCAST_ID else "DIRECT"

    @staticmethod
    def get_name_from_id(item_id,
                         map,
                         id_to_find,
                         default_val="No name found"):
        """
        Gets name from item ID. Assuming name is the KEY and ID is the value in <map> dictionary

        :param item_id:
        :param map:
        :param default_val:
        :return:
        """

        item_name_map = map.get(item_id, None)

        if not item_name_map:
            return default_val

        for name in item_name_map:
            if item_name_map[name] == id_to_find:
                return name

        return default_val

    def _build_parlay_command_message(self, msg):
        """
        Adds fields to Parlay message <msg> in accordance with command spec (3/15/2017).

        Command messages have:
        1) TOPICS -> MSG_TYPE -> COMMAND
        2) CONTENTS -> COMMAND -> COMMAND ID
        3) CONTENTS -> COMMAND NAME (optional)

        :param msg: Parlay message to be modified
        :return: None
        """

        destination_integer_id = self.to
        msg['TOPICS']['MSG_TYPE'] = "COMMAND"
        msg['CONTENTS']['COMMAND'] = self.response_code
        msg['CONTENTS']['COMMAND_NAME'] = self.get_name_from_id(
            destination_integer_id, pcom_serial.PCOM_COMMAND_NAME_MAP,
            self.response_code)

    def _build_parlay_property_get_msg(self, msg):
        """
        Adds fields to Parlay message <msg> in accordance with property GET spec (3/15/2017)

        Property GET messages have:
        1) TOPICS -> MSG_TYPE -> PROPERTY
        2) CONTENTS -> PROPERTY -> PROPERTY ID
        3) CONTENTS -> ACTION -> GET

        :param msg: Parlay message that will be modified
        :return: None
        """

        msg['TOPICS']['MSG_TYPE'] = "PROPERTY"
        msg['CONTENTS']['PROPERTY'] = self.response_code
        msg['CONTENTS']['ACTION'] = "GET"

    def _build_parlay_property_set_msg(self, msg):
        """
        Adds fields to Parlay message <msg> in accordance with property SET spec (3/15/2017)

        Property SET messages have:
        1) TOPICS -> MSG_TYPE -> PROPERTY
        2) CONTENTS -> PROPERTY -> PROPERTY ID
        3) CONTENTS -> ACTION -> SET
        4) CONTENTS -> VALUE -> PROPERTY VALUE

        :param msg: Parlay message (dictionary) that will be modified.
        :return: None
        """

        msg['TOPICS']['MSG_TYPE'] = "PROPERTY"
        msg['CONTENTS']['PROPERTY'] = self.response_code
        msg['CONTENTS']['ACTION'] = "SET"
        msg['CONTENTS']['VALUE'] = self._get_data()

    def _build_parlay_error_response_msg(self, msg):
        """
        Adds fields to Parlay message <msg> in accordance with Parlay error response spec. (3/15/2017)

        Parlay error responses have:
        1) CONTENTS -> ERROR_CODE -> ERROR STATUS # (from embedded board)
        2) CONTENTS -> DESCRIPTION -> ERROR DESCRIPTION
        3) TOPICS -> MSG_STATUS -> ERROR
        4) TOPICS -> RESPONSE_REQ -> False

        :param msg: Parlay message (dictionary) that will be modified.
        :return: None
        """
        def _get_id_name_from_error_code(error_code):
            if error_code >> pcom_serial.PCOMSerial.SUBSYSTEM_SHIFT == self.GLOBAL_ERROR_CODE_ID:
                return self.from_
            return (self.from_ & pcom_serial.PCOMSerial.SUBSYSTEM_ID_MASK) | (
                error_code >> pcom_serial.PCOMSerial.SUBSYSTEM_SHIFT)

        def _get_specific_code(error_code):
            if error_code >> pcom_serial.PCOMSerial.SUBSYSTEM_SHIFT != self.GLOBAL_ERROR_CODE_ID:
                return error_code & pcom_serial.PCOMSerial.ITEM_ID_MASK
            return error_code

        msg['TOPICS']['MSG_STATUS'] = "ERROR"
        msg['TOPICS']['RESPONSE_REQ'] = False

        msg['CONTENTS']['ERROR_CODE'] = self.msg_status
        msg['CONTENTS']['DESCRIPTION'] = pcom_serial.PCOM_ERROR_CODE_MAP.get(
            self.msg_status, self.description)
        msg['CONTENTS']['ERROR ORIGIN'] = pcom_serial.PCOM_ITEM_NAME_MAP.get(
            _get_id_name_from_error_code(self.msg_status), "REACTOR")
        msg['CONTENTS']['ITEM SPECIFIC ERROR CODE'] = _get_specific_code(
            self.msg_status)
        msg['CONTENTS']['INFO'] = self.data

    def _build_parlay_command_response(self, msg):
        """
        Adds fields to Parlay message <msg> in accordance with Parlay command response spec. (3/15/2017)

        Parlay command responses have:
        1) TOPICS -> MSG_STATUS -> PROGRESS/OK
        2) CONTENTS -> DATA depending on result

        :param msg: Parlay message (dictionary) that will be modified.
        :return: None
        """
        msg_option = self.option()
        item = pcom_serial.PCOM_COMMAND_MAP.get(self.from_, None)

        if item or self.response_code == 0:
            if msg_option == ResponseCommandOption.Complete:
                msg['TOPICS']['MSG_STATUS'] = "OK"
            elif msg_option == ResponseCommandOption.Inprogress:
                msg['TOPICS']['MSG_STATUS'] = "PROGRESS"

        else:
            msg["TOPICS"]["MSG_STATUS"] = "ERROR"
            msg["CONTENTS"][
                "DESCRIPTION"] = "PCOM ERROR: Could not find item:", self.from_
            return

        if self.response_code == 0:
            self._build_contents_map(["Subsystems"], msg["CONTENTS"])
            return

        cmd = item.get(self.response_code,
                       pcom_serial.PCOMSerial.build_command_info("", [], []))
        self._build_contents_map(cmd["output params"], msg["CONTENTS"])

    def _build_parlay_property_response(self, msg):
        """
        Adds fields to Parlay message <msg> in accordance with Parlay property response spec. (3/15/2017)

        :param msg: Parlay message (dictionary) that will be modified.
        :return: None
        """
        sender_integer_id = self.from_
        msg_option = self.option()

        msg['TOPICS']['MSG_STATUS'] = "OK"
        if msg_option == ResponsePropertyOption.Get_Response:
            msg['CONTENTS']['ACTION'] = "RESPONSE"
            id = self.response_code

            msg['CONTENTS']['PROPERTY'] = self.response_code
            msg['CONTENTS']['VALUE'] = self._get_data()
        elif msg_option == ResponsePropertyOption.Set_Response:
            msg['CONTENTS']['ACTION'] = "RESPONSE"
            msg['CONTENTS']['PROPERTY'] = self.response_code
            pass  # NOTE: set responses do not have a 'value' field
        elif msg_option == ResponsePropertyOption.Stream_Response:
            msg['TOPICS']['MSG_TYPE'] = "STREAM"
            id = self.response_code
            if type(id) == int:
                # convert to stream name ID
                id = self.get_name_from_id(sender_integer_id,
                                           pcom_serial.PCOM_STREAM_NAME_MAP,
                                           self.response_code,
                                           default_val=self.response_code)
            # TODO: remove stream ID in topics when other platforms conform to spec.
            msg['TOPICS']['STREAM'] = id
            msg['CONTENTS']['STREAM'] = id
            msg['CONTENTS']['VALUE'] = self._get_data()

    def _build_parlay_notification(self, msg):
        """
        Adds fields to Parlay message <msg> in accordance with Parlay notification spec. (3/15/2017)

        :param msg: Parlay message (dictionary) that will be modified.
        :return: None
        """

        msg['TOPICS']["MSG_TYPE"] = "EVENT"
        msg['CONTENTS']['EVENT'] = self.response_code
        msg['CONTENTS']['ERROR_CODE'] = self.msg_status
        msg['CONTENTS']["INFO"] = self.data
        msg['CONTENTS']['DESCRIPTION'] = pcom_serial.PCOM_ERROR_CODE_MAP.get(
            self.msg_status, self.description)
        msg['TOPICS']['RESPONSE_REQ'] = False

    def _build_broadcast(self, msg):
        """
        Changes message to type BROADCAST.

        :param msg: Parlay message (dictionary) that will be modified.
        :return: None
        """

        msg_option = self.option()
        if msg_option == BroadcastNotificationOptions.External:
            msg['TOPICS']['TX_TYPE'] = "BROADCAST"
            if "TO" in msg['TOPICS']:
                del msg['TOPICS']['TO']
        else:
            raise Exception("Received internal broadcast message")

    def _add_notification_msg_status(self, msg):
        """
        Adds status fields to Parlay message <msg>

        :param msg: Parlay message (dictionary) that will be modified.
        :return: None
        """

        if self.msg_status == 0:
            msg['TOPICS']['MSG_STATUS'] = "INFO"
        elif self.msg_status > 0:
            msg['TOPICS']['MSG_STATUS'] = "ERROR"
        else:
            msg['TOPICS']['MSG_STATUS'] = "WARNING"

    def _build_parlay_stream_msg(self, msg, is_on):
        """
        Adds fields to Parlay message <msg> for stream commands.
        NOTE: The stream ID is put in contents and topics because some Parlay implementations check for it in
        TOPICS and some check for it in CONTENTS.

        :param msg: Parlay dictionary message that will be modified.
        :param is_on: Whether the stream command is turning the stream on or off.
        :return:
        """

        sender_integer_id = self.from_

        msg["TOPICS"]["MSG_TYPE"] = "STREAM"
        if type(id) == int:
            # convert to stream name ID
            id = self.get_name_from_id(sender_integer_id,
                                       pcom_serial.PCOM_STREAM_NAME_MAP,
                                       self.response_code,
                                       default_val=self.response_code)

        msg["TOPICS"]["STREAM"] = id
        msg["CONTENTS"]["STREAM"] = id
        msg["CONTENTS"]["STOP"] = is_on
        # TODO: Find out from Frances what the data looks like for stream commands

    def to_json_msg(self):
        """
        Converts from PCOMMessage type to Parlay JSON message. Returns message when translation is complete.
        :return: Parlay JSON message equivalent of this object
        """

        # Initialize our potential JSON message
        msg = {'TOPICS': {}, 'CONTENTS': {}}

        msg['TOPICS']['TO'] = self._get_name_from_id(self.to)
        msg['TOPICS']['FROM'] = self._get_name_from_id(self.from_)
        msg['TOPICS']['FROM_NAME'] = pcom_serial.PCOM_ITEM_NAME_MAP.get(
            self.from_, "")
        msg['TOPICS']['MSG_ID'] = self.msg_id

        # Default the message transmission type to "DIRECt".
        # This may be changed later if the message is a broadcast notification.
        msg['TOPICS']['TX_TYPE'] = "DIRECT"

        # Retrieve our message components
        msg_category = self.category()
        msg_sub_type = self.sub_type()
        msg_option = self.option()

        msg['TOPICS']['RESPONSE_REQ'] = self._is_response_req()

        # Handle Parlay command messages
        if msg_category == MessageCategory.Order:
            if msg_sub_type == OrderSubType.Command:
                if msg_option == OrderCommandOption.Normal:
                    self._build_parlay_command_message(msg)

            elif msg_sub_type == OrderSubType.Property:
                if msg_option == OrderPropertyOption.Get_Property:
                    self._build_parlay_property_get_msg(msg)

                elif msg_option == OrderPropertyOption.Set_Property:
                    self._build_parlay_property_set_msg(msg)

                elif msg_option == OrderPropertyOption.Stream_On:
                    self._build_parlay_stream_msg(msg, is_on=True)
                elif msg_option == OrderPropertyOption.Stream_Off:
                    self._build_parlay_stream_msg(msg, is_on=False)

            else:
                raise Exception("Unhandled message subtype {}", msg_sub_type)

        # Handle Parlay responses
        elif msg_category == MessageCategory.Order_Response:
            msg['TOPICS']['MSG_TYPE'] = "RESPONSE"

            if self.msg_status != STATUS_SUCCESS:
                self._build_parlay_error_response_msg(msg)
                return msg

            if msg_sub_type == ResponseSubType.Command:
                self._build_parlay_command_response(msg)

            elif msg_sub_type == ResponseSubType.Property:
                self._build_parlay_property_response(msg)

        # Handle Parlay notifications
        elif msg_category == MessageCategory.Notification:
            self._build_parlay_notification(msg)

            if msg_sub_type == NotificationSubType.Broadcast:
                self._build_broadcast(msg)

            elif msg_option == NotificationSubType.Direct:
                msg['TOPICS']['TX_TYPE'] = "DIRECT"

            else:
                raise Exception("Unhandled notification type")

            self._add_notification_msg_status(msg)

        return msg

    def _build_contents_map(self, output_param_names, contents_map):
        """
        Given the output names of a command and a data list, modifies <contents_map> accordingly.

        :param output_param_names: The output names of a command found during discovery.
        :param contents_map: The map that will be produced as part of a Parlay JSON message.
        :return: None
        """

        # If we don't have any data we do not need add anything to the contents
        # dictionary.
        if len(self.data) == 0:
            return

        # If there is only one output parameter we need to add a "RESULT" key to the
        # contents dictionary and fill it with our data as per Parlay spec 3/15/2017.
        if len(output_param_names) == 1:
            contents_map["RESULT"] = self.data[0] if len(
                self.data) == 1 else self.data
            return

        # If the number of output parameters does not match the number of data pieces
        # we can not reliably add { output_name -> data } pairs to the CONTENTS dictionary
        # so we should report an error and not add anything to CONTENTS.
        if len(output_param_names) != len(self.data):
            logger.error(
                "[PCOM] ERROR: Could not produce contents dictionary for data: {0} and"
                " output parameters: {1}".format(self.data,
                                                 output_param_names))
            return

        # If we got here we know that the number of output parameters is the same
        # as the number of data pieces so we can match our output parameters to data pieces
        # 1:1. Simply zip them together and throw them in the CONTENTS dictionary.
        contents_map.update(dict(zip(output_param_names, self.data)))

    def category(self):
        return (self.msg_type & CATEGORY_MASK) >> CATEGORY_SHIFT

    def sub_type(self):
        return (self.msg_type & SUB_TYPE_MASK) >> SUB_TYPE_SHIFT

    def option(self):
        return (self.msg_type & OPTION_MASK) >> OPTION_SHIFT

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, value):
        if hasattr(value, '__iter__'):
            self._data = list(value)
        elif value is None:
            self._data = []
        else:
            self._data = [value]

    @property
    def attributes(self):
        return self._attributes

    @attributes.setter
    def attributes(self, value):
        self._attributes = value
        if value is not None:
            self.priority = value & 0x01