class BoardInterface: bus = ThreadSafeBus() listener = CanListener() notifier = can.Notifier(bus, [listener]) @staticmethod def write_valve(boardID, valveID, angle): arbId = (boardID << 6) | (valveID) #TODO: angle should be converted to standard 16 bit representation for float message = can.Message(arbitration_id=arbId, data=[angle]) #TODO: use this? @staticmethod def set_message_handler(self, messageHandler): BoardInterface.listener.message_handler = messageHandler
class CanConnector(Connector, Thread): CMD_REGEX = r"^(\d{1,2}):(\d{1,2}):?(big|little)?:(\d+)$" VALUE_REGEX = r"^(\d{1,2}):(\d{1,2}):?(big|little)?:(bool|boolean|int|long|float|double|string):?([0-9A-Za-z-_]+)?$" NO_CMD_ID = "no_cmd" UNKNOWN_ARBITRATION_ID = -1 DEFAULT_RECONNECT_PERIOD = 30.0 DEFAULT_POLL_PERIOD = 1.0 DEFAULT_SEND_IF_CHANGED = False DEFAULT_RECONNECT_STATE = True DEFAULT_EXTENDED_ID_FLAG = False DEFAULT_FD_FLAG = False DEFAULT_BITRATE_SWITCH_FLAG = False DEFAULT_BYTEORDER = "big" DEFAULT_ENCODING = "ascii" DEFAULT_ENABLE_UNKNOWN_RPC = False DEFAULT_OVERRIDE_RPC_PARAMS = False DEFAULT_STRICT_EVAL_FLAG = True DEFAULT_SIGNED_FLAG = False DEFAULT_RPC_RESPONSE_SEND_FLAG = False def __init__(self, gateway, config, connector_type): self.statistics = {'MessagesReceived': 0, 'MessagesSent': 0} super().__init__() self.setName( config.get( "name", 'CAN Connector ' + ''.join(choice(ascii_lowercase) for _ in range(5)))) self.__gateway = gateway self.__connector_type = connector_type self.__bus_conf = {} self.__bus = None self.__reconnect_count = 0 self.__reconnect_conf = {} self.__devices = {} self.__nodes = {} self.__commands = {} self.__polling_messages = [] self.__rpc_calls = {} self.__shared_attributes = {} self.__converters = {} self.__bus_error = None self.__connected = False self.__stopped = True self.daemon = True self.__parse_config(config) def open(self): log.info("[%s] Starting...", self.get_name()) self.__stopped = False self.start() def close(self): if not self.__stopped: self.__stopped = True log.debug("[%s] Stopping", self.get_name()) def get_name(self): return self.name def is_connected(self): return self.__connected def on_attributes_update(self, content): for attr_name, attr_value in content["data"].items(): attr_config = self.__shared_attributes.get(content["device"], {}).get(attr_name) if attr_config is None: log.warning( "[%s] No configuration for '%s' attribute, ignore its update", self.get_name(), attr_name) return log.debug( "[%s] Processing attribute update for '%s' device: attribute=%s,value=%s", self.get_name(), content["device"], attr_name, attr_value) # Converter expects dictionary as the second parameter so pack an attribute value to a dictionary data = self.__converters[content["device"]]["downlink"].convert( attr_config, {"value": attr_value}) if data is None: log.error( "[%s] Failed to update '%s' attribute for '%s' device: data conversion failure", self.get_name(), attr_name, content["device"]) return done = self.send_data_to_bus(data, attr_config, data_check=True) if done: log.debug("[%s] Updated '%s' attribute for '%s' device", self.get_name(), attr_name, content["device"]) else: log.error( "[%s] Failed to update '%s' attribute for '%s' device", self.get_name(), attr_name, content["device"]) def server_side_rpc_handler(self, content): rpc_config = self.__rpc_calls.get(content["device"], {}).get(content["data"]["method"]) if rpc_config is None: if not self.__devices[content["device"]]["enableUnknownRpc"]: log.warning( "[%s] No configuration for '%s' RPC request (id=%s), ignore it", self.get_name(), content["data"]["method"], content["data"]["id"]) return else: rpc_config = {} log.debug( "[%s] Processing %s '%s' RPC request (id=%s) for '%s' device: params=%s", self.get_name(), "pre-configured" if rpc_config else "UNKNOWN", content["data"]["method"], content["data"]["id"], content["device"], content["data"].get("params")) if self.__devices[content["device"]]["overrideRpcConfig"]: if rpc_config: conversion_config = self.__merge_rpc_configs( content["data"].get("params", {}), rpc_config) log.debug( "[%s] RPC request (id=%s) params and connector config merged to conversion config %s", self.get_name(), content["data"]["id"], conversion_config) else: log.debug( "[%s] RPC request (id=%s) will use its params as conversion config", self.get_name(), content["data"]["id"]) conversion_config = content["data"].get("params", {}) else: conversion_config = rpc_config data = self.__converters[content["device"]]["downlink"].convert( conversion_config, content["data"].get("params", {})) if data is not None: done = self.send_data_to_bus(data, conversion_config, data_check=True) if done: log.debug( "[%s] Processed '%s' RPC request (id=%s) for '%s' device", self.get_name(), content["data"]["method"], content["data"]["id"], content["device"]) else: log.error( "[%s] Failed to process '%s' RPC request (id=%s) for '%s' device", self.get_name(), content["data"]["method"], content["data"]["id"], content["device"]) else: done = False log.error( "[%s] Failed to process '%s' RPC request (id=%s) for '%s' device: data conversion failure", self.get_name(), content["data"]["method"], content["data"]["id"], content["device"]) if conversion_config.get("response", self.DEFAULT_RPC_RESPONSE_SEND_FLAG): self.__gateway.send_rpc_reply(content["device"], content["data"]["id"], {"success": done}) def run(self): need_run = True while need_run: bus_notifier = None poller = None try: interface = self.__bus_conf["interface"] channel = self.__bus_conf["channel"] kwargs = self.__bus_conf["backend"] self.__bus = ThreadSafeBus(interface=interface, channel=channel, **kwargs) reader = BufferedReader() bus_notifier = Notifier(self.__bus, [reader]) log.info("[%s] Connected to CAN bus (interface=%s,channel=%s)", self.get_name(), interface, channel) if self.__polling_messages: poller = Poller(self) # Poll once to check if network is not down. # It would be better to have some kind of a ping message to check a bus state. poller.poll() # Initialize the connected flag and reconnect count only after bus creation and sending poll messages. # It is expected that after these operations most likely the bus is up. self.__connected = True self.__reconnect_count = 0 while not self.__stopped: message = reader.get_message() if message is not None: # log.debug("[%s] New CAN message received %s", self.get_name(), message) self.__process_message(message) self.__check_if_error_happened() except Exception as e: log.error("[%s] Error on CAN bus: %s", self.get_name(), str(e)) finally: try: if poller is not None: poller.stop() if bus_notifier is not None: bus_notifier.stop() if self.__bus is not None: log.debug( "[%s] Shutting down connection to CAN bus (state=%s)", self.get_name(), self.__bus.state) self.__bus.shutdown() except Exception as e: log.error( "[%s] Error on shutdown connection to CAN bus: %s", self.get_name(), str(e)) self.__connected = False if not self.__stopped: if self.__is_reconnect_enabled(): retry_period = self.__reconnect_conf["period"] log.info( "[%s] Next attempt to connect will be in %f seconds (%s attempt left)", self.get_name(), retry_period, "infinite" if self.__reconnect_conf["maxCount"] is None else self.__reconnect_conf["maxCount"] - self.__reconnect_count + 1) time.sleep(retry_period) else: need_run = False log.info( "[%s] Last attempt to connect has failed. Exiting...", self.get_name()) else: need_run = False log.info("[%s] Stopped", self.get_name()) def is_stopped(self): return self.__stopped def get_polling_messages(self): return self.__polling_messages def send_data_to_bus(self, data, config, data_check=True, raise_exception=False): try: self.__bus.send( Message(arbitration_id=config["nodeId"], is_extended_id=config.get( "isExtendedId", self.DEFAULT_EXTENDED_ID_FLAG), is_fd=config.get("isFd", self.DEFAULT_FD_FLAG), bitrate_switch=config.get( "bitrateSwitch", self.DEFAULT_BITRATE_SWITCH_FLAG), data=data, check=data_check)) return True except (ValueError, TypeError) as e: log.error("[%s] Wrong CAN message data: %s", self.get_name(), str(e)) except CanError as e: log.error("[%s] Failed to send CAN message: %s", self.get_name(), str(e)) if raise_exception: raise e else: self.__on_bus_error(e) return False def __on_bus_error(self, e): log.warning( "[%s] Notified about CAN bus error. Store it to later processing", self.get_name()) self.__bus_error = e def __check_if_error_happened(self): if self.__bus_error is not None: # Maybe copying is redundant e = copy(self.__bus_error) self.__bus_error = None raise e def __merge_rpc_configs(self, rpc_params, rpc_config): config = {} options = { "nodeId": self.UNKNOWN_ARBITRATION_ID, "isExtendedId": self.DEFAULT_EXTENDED_ID_FLAG, "isFd": self.DEFAULT_FD_FLAG, "bitrateSwitch": self.DEFAULT_BITRATE_SWITCH_FLAG, "response": True, "dataLength": 1, "dataByteorder": self.DEFAULT_BYTEORDER, "dataSigned": self.DEFAULT_SIGNED_FLAG, "dataExpression": None, "dataEncoding": self.DEFAULT_ENCODING, "dataBefore": None, "dataAfter": None, "dataInHex": None } for option_name, option_value in options.items(): if option_name in rpc_params: config[option_name] = rpc_params[option_name] elif option_value is not None: config[option_name] = rpc_config.get(option_name, option_value) return config def __process_message(self, message): if message.arbitration_id not in self.__nodes: # Too lot log messages in case of high message generation frequency log.debug("[%s] Ignoring CAN message. Unknown arbitration_id %d", self.get_name(), message.arbitration_id) return cmd_conf = self.__commands[message.arbitration_id] if cmd_conf is not None: cmd_id = int.from_bytes( message.data[cmd_conf["start"]:cmd_conf["start"] + cmd_conf["length"]], cmd_conf["byteorder"]) else: cmd_id = self.NO_CMD_ID if cmd_id not in self.__nodes[message.arbitration_id]: log.debug("[%s] Ignoring CAN message. Unknown cmd_id %d", self.get_name(), cmd_id) return log.debug("[%s] Processing CAN message (id=%d,cmd_id=%s): %s", self.get_name(), message.arbitration_id, cmd_id, message) parsing_conf = self.__nodes[message.arbitration_id][cmd_id] data = self.__converters[parsing_conf["deviceName"]]["uplink"].convert( parsing_conf["configs"], message.data) if data is None or not data.get("attributes", []) and not data.get( "telemetry", []): log.warning( "[%s] Failed to process CAN message (id=%d,cmd_id=%s): data conversion failure", self.get_name(), message.arbitration_id, cmd_id) return self.__check_and_send(parsing_conf, data) def __check_and_send(self, conf, new_data): self.statistics['MessagesReceived'] += 1 to_send = {"attributes": [], "telemetry": []} send_on_change = conf["sendOnChange"] for tb_key in to_send.keys(): for key, new_value in new_data[tb_key].items(): if not send_on_change or self.__devices[ conf["deviceName"]][tb_key][key] != new_value: self.__devices[conf["deviceName"]][tb_key][key] = new_value to_send[tb_key].append({key: new_value}) if to_send["attributes"] or to_send["telemetry"]: to_send["deviceName"] = conf["deviceName"] to_send["deviceType"] = conf["deviceType"] log.debug("[%s] Pushing to TB server '%s' device data: %s", self.get_name(), conf["deviceName"], to_send) self.__gateway.send_to_storage(self.get_name(), to_send) self.statistics['MessagesSent'] += 1 else: log.debug("[%s] '%s' device data has not been changed", self.get_name(), conf["deviceName"]) def __is_reconnect_enabled(self): if self.__reconnect_conf["enabled"]: if self.__reconnect_conf["maxCount"] is None: return True self.__reconnect_count += 1 return self.__reconnect_conf["maxCount"] >= self.__reconnect_count else: return False def __parse_config(self, config): self.__reconnect_count = 0 self.__reconnect_conf = { "enabled": config.get("reconnect", self.DEFAULT_RECONNECT_STATE), "period": config.get("reconnectPeriod", self.DEFAULT_RECONNECT_PERIOD), "maxCount": config.get("reconnectCount", None) } self.__bus_conf = { "interface": config.get("interface", "socketcan"), "channel": config.get("channel", "vcan0"), "backend": config.get("backend", {}) } for device_config in config.get("devices"): is_device_config_valid = False device_name = device_config["name"] device_type = device_config.get("type", self.__connector_type) strict_eval = device_config.get("strictEval", self.DEFAULT_STRICT_EVAL_FLAG) self.__devices[device_name] = {} self.__devices[device_name][ "enableUnknownRpc"] = device_config.get( "enableUnknownRpc", self.DEFAULT_ENABLE_UNKNOWN_RPC) self.__devices[device_name]["overrideRpcConfig"] = True if self.__devices[device_name]["enableUnknownRpc"] \ else device_config.get("overrideRpcConfig", self.DEFAULT_OVERRIDE_RPC_PARAMS) self.__converters[device_name] = {} if not strict_eval: log.info( "[%s] Data converters for '%s' device will use non-strict eval", self.get_name(), device_name) if "serverSideRpc" in device_config and device_config[ "serverSideRpc"]: is_device_config_valid = True self.__rpc_calls[device_name] = {} self.__converters[device_name][ "downlink"] = self.__get_converter( device_config.get("converters"), False) for rpc_config in device_config["serverSideRpc"]: rpc_config["strictEval"] = strict_eval self.__rpc_calls[device_name][ rpc_config["method"]] = rpc_config if "attributeUpdates" in device_config and device_config[ "attributeUpdates"]: is_device_config_valid = True self.__shared_attributes[device_name] = {} if "downlink" not in self.__converters[device_name]: self.__converters[device_name][ "downlink"] = self.__get_converter( device_config.get("converters"), False) for attribute_config in device_config["attributeUpdates"]: attribute_config["strictEval"] = strict_eval attribute_name = attribute_config.get( "attributeOnThingsBoard") or attribute_config.get( "attribute") self.__shared_attributes[device_name][ attribute_name] = attribute_config for config_key in ["timeseries", "attributes"]: if config_key not in device_config or not device_config[ config_key]: continue is_device_config_valid = True is_ts = (config_key[0] == "t") tb_item = "telemetry" if is_ts else "attributes" self.__devices[device_name][tb_item] = {} if "uplink" not in self.__converters[device_name]: self.__converters[device_name][ "uplink"] = self.__get_converter( device_config.get("converters"), True) for msg_config in device_config[config_key]: tb_key = msg_config["key"] msg_config["strictEval"] = strict_eval msg_config["is_ts"] = is_ts node_id = msg_config.get("nodeId", self.UNKNOWN_ARBITRATION_ID) if node_id == self.UNKNOWN_ARBITRATION_ID: log.warning( "[%s] Ignore '%s' %s configuration: no arbitration id", self.get_name(), tb_key, config_key) continue value_config = self.__parse_value_config( msg_config.get("value")) if value_config is not None: msg_config.update(value_config) else: log.warning( "[%s] Ignore '%s' %s configuration: no value configuration", self.get_name(), tb_key, config_key, ) continue if msg_config.get("command", "") and node_id not in self.__commands: cmd_config = self.__parse_command_config( msg_config["command"]) if cmd_config is None: log.warning( "[%s] Ignore '%s' %s configuration: wrong command configuration", self.get_name(), tb_key, config_key, ) continue cmd_id = cmd_config["value"] self.__commands[node_id] = cmd_config else: cmd_id = self.NO_CMD_ID self.__commands[node_id] = None if node_id not in self.__nodes: self.__nodes[node_id] = {} if cmd_id not in self.__nodes[node_id]: self.__nodes[node_id][cmd_id] = { "deviceName": device_name, "deviceType": device_type, "sendOnChange": device_config.get("sendDataOnlyOnChange", self.DEFAULT_SEND_IF_CHANGED), "configs": [] } self.__nodes[node_id][cmd_id]["configs"].append(msg_config) self.__devices[device_name][tb_item][tb_key] = None if "polling" in msg_config: try: polling_config = msg_config.get("polling") polling_config["key"] = tb_key # Just for logging polling_config["type"] = polling_config.get( "type", "always") polling_config["period"] = polling_config.get( "period", self.DEFAULT_POLL_PERIOD) polling_config["nodeId"] = node_id polling_config["isExtendedId"] = msg_config.get( "isExtendedId", self.DEFAULT_EXTENDED_ID_FLAG) polling_config["isFd"] = msg_config.get( "isFd", self.DEFAULT_FD_FLAG) polling_config["bitrateSwitch"] = msg_config.get( "bitrateSwitch", self.DEFAULT_BITRATE_SWITCH_FLAG) # Create CAN message object to validate its data can_msg = Message( arbitration_id=polling_config["nodeId"], is_extended_id=polling_config["isExtendedId"], is_fd=polling_config["isFd"], bitrate_switch=polling_config["bitrateSwitch"], data=bytearray.fromhex( polling_config["dataInHex"]), check=True) self.__polling_messages.append(polling_config) except (ValueError, TypeError) as e: log.warning( "[%s] Ignore '%s' %s polling configuration, wrong CAN data: %s", self.get_name(), tb_key, config_key, str(e)) continue if is_device_config_valid: log.debug("[%s] Done parsing of '%s' device configuration", self.get_name(), device_name) self.__gateway.add_device(device_name, {"connector": self}) else: log.warning( "[%s] Ignore '%s' device configuration, because it doesn't have attributes," "attributeUpdates,timeseries or serverSideRpc", self.get_name(), device_name) def __parse_value_config(self, config): if config is None: log.warning("[%s] Wrong value configuration: no data", self.get_name()) return if isinstance(config, str): value_matches = re.search(self.VALUE_REGEX, config) if not value_matches: log.warning( "[%s] Wrong value configuration: '%s' doesn't match pattern", self.get_name(), config) return value_config = { "start": int(value_matches.group(1)), "length": int(value_matches.group(2)), "byteorder": value_matches.group(3) if value_matches.group(3) else self.DEFAULT_BYTEORDER, "type": value_matches.group(4) } if value_config["type"][0] == "i" or value_config["type"][0] == "l": value_config["signed"] = value_matches.group(5) == "signed" if value_matches.group(5) \ else self.DEFAULT_SIGNED_FLAG elif value_config["type"][0] == "s": value_config["encoding"] = value_matches.group( 5) if value_matches.group(5) else self.DEFAULT_ENCODING return value_config elif isinstance(config, dict): try: value_config = { "start": int(config["start"]), "length": int(config["length"]), "byteorder": config["byteorder"] if config.get("byteorder", "") else self.DEFAULT_BYTEORDER, "type": config["type"] } if value_config["type"][0] == "i" or value_config["type"][ 0] == "l": value_config["signed"] = config.get( "signed", self.DEFAULT_SIGNED_FLAG) elif value_config["type"][0] == "s": value_config[ "encoding"] = config["encoding"] if config.get( "encoding", "") else self.DEFAULT_ENCODING return value_config except (KeyError, ValueError) as e: log.warning("[%s] Wrong value configuration: %s", self.get_name(), str(e)) return log.warning("[%s] Wrong value configuration: unknown type", self.get_name()) return def __parse_command_config(self, config): if config is None: log.warning("[%s] Wrong command configuration: no data", self.get_name()) return if isinstance(config, str): cmd_matches = re.search(self.CMD_REGEX, config) if not cmd_matches: log.warning( "[%s] Wrong command configuration: '%s' doesn't match pattern", self.get_name(), config) return return { "start": int(cmd_matches.group(1)), "length": int(cmd_matches.group(2)), "byteorder": cmd_matches.group(3) if cmd_matches.group(3) else self.DEFAULT_BYTEORDER, "value": int(cmd_matches.group(4)) } elif isinstance(config, dict): try: return { "start": int(config["start"]), "length": int(config["length"]), "byteorder": config["byteorder"] if config.get("byteorder", "") else self.DEFAULT_BYTEORDER, "value": int(config["value"]) } except (KeyError, ValueError) as e: log.warning("[%s] Wrong command configuration: %s", self.get_name(), str(e)) return log.warning("[%s] Wrong command configuration: unknown type", self.get_name()) return def __get_converter(self, config, need_uplink): if config is None: return BytesCanUplinkConverter( ) if need_uplink else BytesCanDownlinkConverter() else: if need_uplink: uplink = config.get("uplink") return BytesCanUplinkConverter() if uplink is None \ else TBUtility.check_and_import(self.__connector_type, uplink) else: downlink = config.get("downlink") return BytesCanDownlinkConverter() if downlink is None \ else TBUtility.check_and_import(self.__connector_type, downlink)
def run(self): need_run = True while need_run: bus_notifier = None poller = None try: interface = self.__bus_conf["interface"] channel = self.__bus_conf["channel"] kwargs = self.__bus_conf["backend"] self.__bus = ThreadSafeBus(interface=interface, channel=channel, **kwargs) reader = BufferedReader() bus_notifier = Notifier(self.__bus, [reader]) log.info("[%s] Connected to CAN bus (interface=%s,channel=%s)", self.get_name(), interface, channel) if self.__polling_messages: poller = Poller(self) # Poll once to check if network is not down. # It would be better to have some kind of a ping message to check a bus state. poller.poll() # Initialize the connected flag and reconnect count only after bus creation and sending poll messages. # It is expected that after these operations most likely the bus is up. self.__connected = True self.__reconnect_count = 0 while not self.__stopped: message = reader.get_message() if message is not None: # log.debug("[%s] New CAN message received %s", self.get_name(), message) self.__process_message(message) self.__check_if_error_happened() except Exception as e: log.error("[%s] Error on CAN bus: %s", self.get_name(), str(e)) finally: try: if poller is not None: poller.stop() if bus_notifier is not None: bus_notifier.stop() if self.__bus is not None: log.debug( "[%s] Shutting down connection to CAN bus (state=%s)", self.get_name(), self.__bus.state) self.__bus.shutdown() except Exception as e: log.error( "[%s] Error on shutdown connection to CAN bus: %s", self.get_name(), str(e)) self.__connected = False if not self.__stopped: if self.__is_reconnect_enabled(): retry_period = self.__reconnect_conf["period"] log.info( "[%s] Next attempt to connect will be in %f seconds (%s attempt left)", self.get_name(), retry_period, "infinite" if self.__reconnect_conf["maxCount"] is None else self.__reconnect_conf["maxCount"] - self.__reconnect_count + 1) time.sleep(retry_period) else: need_run = False log.info( "[%s] Last attempt to connect has failed. Exiting...", self.get_name()) else: need_run = False log.info("[%s] Stopped", self.get_name())