|
| 1 | +""" |
| 2 | +pyneovi interface module. |
| 3 | +
|
| 4 | +pyneovi is a Python wrapper around the API provided by Intrepid Control Systems |
| 5 | +for communicating with their NeoVI range of devices. |
| 6 | +
|
| 7 | +Implementation references: |
| 8 | +* http://pyneovi.readthedocs.io/en/latest/ |
| 9 | +* https://bitbucket.org/Kemp_J/pyneovi |
| 10 | +""" |
| 11 | + |
| 12 | +import logging |
| 13 | + |
| 14 | +import sys |
| 15 | + |
| 16 | +logger = logging.getLogger(__name__) |
| 17 | + |
| 18 | +try: |
| 19 | + from queue import Queue, Empty |
| 20 | +except ImportError: |
| 21 | + from Queue import Queue, Empty |
| 22 | + |
| 23 | +if sys.platform == "win32": |
| 24 | + try: |
| 25 | + from neovi import neodevice |
| 26 | + from neovi import neovi |
| 27 | + from neovi.structures import icsSpyMessage |
| 28 | + except ImportError as e: |
| 29 | + logger.warning("Cannot load pyneovi: %s", e) |
| 30 | +else: |
| 31 | + # Will not work on other systems, but have it importable anyway for |
| 32 | + # tests/sphinx |
| 33 | + logger.warning("pyneovi library does not work on %s platform", sys.platform) |
| 34 | + |
| 35 | +from can import Message |
| 36 | +from can.bus import BusABC |
| 37 | + |
| 38 | + |
| 39 | +SPY_STATUS_XTD_FRAME = 0x04 |
| 40 | +SPY_STATUS_REMOTE_FRAME = 0x08 |
| 41 | + |
| 42 | + |
| 43 | +# For the neoVI hardware, TimeHardware2 is more significant than TimeHardware. |
| 44 | +# The resolution of TimeHardware is 1.6us and and TimeHardware2 is 104.8576 ms. |
| 45 | +# To calculate the time of the message in seconds use the following formula: |
| 46 | +# "Timestamp (sec) = TimeHardware2* 0.1048576 + TimeHardware * 0.0000016". |
| 47 | +NEOVI_TIMEHARDWARE_SCALING = 0.0000016 |
| 48 | +NEOVI_TIMEHARDWARE2_SCALING = 0.1048576 |
| 49 | + |
| 50 | +# For the neoVI PRO or ValueCAN hardware, TimeHardware2 is more significant than |
| 51 | +# TimeHardware. The resolution of TimeHardware is 1.0us and and TimeHardware2 is |
| 52 | +# 65.536 ms. To calculate the time of the message in seconds use the following |
| 53 | +# formula: "Timestamp (sec) = TimeHardware2* 0.065536 + TimeHardware * 0.000001" |
| 54 | +VALUECAN_TIMEHARDWARE_SCALING = 0.000001 |
| 55 | +VALUECAN_TIMEHARDWARE2_SCALING = 0.065536 |
| 56 | + |
| 57 | + |
| 58 | +def neo_device_name(device_type): |
| 59 | + names = { |
| 60 | + neovi.NEODEVICE_BLUE: 'neoVI BLUE', |
| 61 | + neovi.NEODEVICE_DW_VCAN: 'ValueCAN', |
| 62 | + neovi.NEODEVICE_FIRE: 'neoVI FIRE', |
| 63 | + neovi.NEODEVICE_VCAN3: 'ValueCAN3', |
| 64 | + neovi.NEODEVICE_YELLOW: 'neoVI YELLOW', |
| 65 | + neovi.NEODEVICE_RED: 'neoVI RED', |
| 66 | + neovi.NEODEVICE_ECU: 'neoECU', |
| 67 | + # neovi.NEODEVICE_IEVB: '' |
| 68 | + } |
| 69 | + return names.get(device_type, 'Unknown neoVI') |
| 70 | + |
| 71 | + |
| 72 | +class NeoVIBus(BusABC): |
| 73 | + """ |
| 74 | + The CAN Bus implemented for the pyneovi interface. |
| 75 | + """ |
| 76 | + |
| 77 | + def __init__(self, channel=None, can_filters=None, **config): |
| 78 | + """ |
| 79 | +
|
| 80 | + :param int channel: |
| 81 | + The Channel id to create this bus with. |
| 82 | + :param list can_filters: |
| 83 | + A list of dictionaries each containing a "can_id" and a "can_mask". |
| 84 | +
|
| 85 | + >>> [{"can_id": 0x11, "can_mask": 0x21}] |
| 86 | +
|
| 87 | + """ |
| 88 | + type_filter = config.get('type_filter', neovi.NEODEVICE_ALL) |
| 89 | + neodevice.init_api() |
| 90 | + self.device = neodevice.find_devices(type_filter)[0] |
| 91 | + self.device.open() |
| 92 | + self.channel_info = '%s %s on channel %s' % ( |
| 93 | + neo_device_name(self.device.get_type()), |
| 94 | + self.device.device.SerialNumber, |
| 95 | + channel |
| 96 | + ) |
| 97 | + |
| 98 | + if self.device.get_type() in [neovi.NEODEVICE_DW_VCAN]: |
| 99 | + self._time_scaling = (VALUECAN_TIMEHARDWARE_SCALING, |
| 100 | + VALUECAN_TIMEHARDWARE2_SCALING) |
| 101 | + else: |
| 102 | + self._time_scaling = (NEOVI_TIMEHARDWARE_SCALING, |
| 103 | + NEOVI_TIMEHARDWARE2_SCALING) |
| 104 | + |
| 105 | + self.sw_filters = None |
| 106 | + self.set_filters(can_filters) |
| 107 | + self.rx_buffer = Queue() |
| 108 | + |
| 109 | + self.network = int(channel) if channel is not None else None |
| 110 | + self.device.subscribe_to(self._rx_buffer, network=self.network) |
| 111 | + |
| 112 | + def __del__(self): |
| 113 | + self.shutdown() |
| 114 | + |
| 115 | + def shutdown(self): |
| 116 | + self.device.pump_messages = False |
| 117 | + if self.device.msg_queue_thread is not None: |
| 118 | + self.device.msg_queue_thread.join() |
| 119 | + |
| 120 | + def _rx_buffer(self, msg, user_data): |
| 121 | + self.rx_buffer.put_nowait(msg) |
| 122 | + |
| 123 | + def _is_filter_match(self, arb_id): |
| 124 | + """ |
| 125 | + If SW filtering is used, checks if the `arb_id` matches any of |
| 126 | + the filters setup. |
| 127 | +
|
| 128 | + :param int arb_id: |
| 129 | + CAN ID to check against. |
| 130 | +
|
| 131 | + :return: |
| 132 | + True if `arb_id` matches any filters |
| 133 | + (or if SW filtering is not used). |
| 134 | + """ |
| 135 | + if not self.sw_filters: |
| 136 | + # Filtering done on HW or driver level or no filtering |
| 137 | + return True |
| 138 | + for can_filter in self.sw_filters: |
| 139 | + if not (arb_id ^ can_filter['can_id']) & can_filter['can_mask']: |
| 140 | + return True |
| 141 | + |
| 142 | + logging.info("%s not matching" % arb_id) |
| 143 | + return False |
| 144 | + |
| 145 | + def _ics_msg_to_message(self, ics_msg): |
| 146 | + return Message( |
| 147 | + timestamp=( |
| 148 | + self._time_scaling[1] * ics_msg.TimeHardware2 + |
| 149 | + self._time_scaling[0] * ics_msg.TimeHardware |
| 150 | + ), |
| 151 | + arbitration_id=ics_msg.ArbIDOrHeader, |
| 152 | + data=ics_msg.Data, |
| 153 | + dlc=ics_msg.NumberBytesData, |
| 154 | + extended_id=bool(ics_msg.StatusBitField & |
| 155 | + SPY_STATUS_XTD_FRAME), |
| 156 | + is_remote_frame=bool(ics_msg.StatusBitField & |
| 157 | + SPY_STATUS_REMOTE_FRAME), |
| 158 | + ) |
| 159 | + |
| 160 | + def recv(self, timeout=None): |
| 161 | + try: |
| 162 | + ics_msg = self.rx_buffer.get(block=True, timeout=timeout) |
| 163 | + except Empty: |
| 164 | + pass |
| 165 | + else: |
| 166 | + if ics_msg.NetworkID == self.network and \ |
| 167 | + self._is_filter_match(ics_msg.ArbIDOrHeader): |
| 168 | + return self._ics_msg_to_message(ics_msg) |
| 169 | + |
| 170 | + def send(self, msg): |
| 171 | + data = tuple(msg.data) |
| 172 | + flags = SPY_STATUS_XTD_FRAME if msg.is_extended_id else 0 |
| 173 | + if msg.is_remote_frame: |
| 174 | + flags |= SPY_STATUS_REMOTE_FRAME |
| 175 | + |
| 176 | + ics_msg = icsSpyMessage() |
| 177 | + ics_msg.ArbIDOrHeader = msg.arbitration_id |
| 178 | + ics_msg.NumberBytesData = len(data) |
| 179 | + ics_msg.Data = data |
| 180 | + ics_msg.StatusBitField = flags |
| 181 | + ics_msg.StatusBitField2 = 0 |
| 182 | + ics_msg.DescriptionID = self.device.tx_id |
| 183 | + self.device.tx_id += 1 |
| 184 | + self.device.tx_raw_message(ics_msg, self.network) |
| 185 | + |
| 186 | + def set_filters(self, can_filters=None): |
| 187 | + """Apply filtering to all messages received by this Bus. |
| 188 | +
|
| 189 | + Calling without passing any filters will reset the applied filters. |
| 190 | +
|
| 191 | + :param list can_filters: |
| 192 | + A list of dictionaries each containing a "can_id" and a "can_mask". |
| 193 | +
|
| 194 | + >>> [{"can_id": 0x11, "can_mask": 0x21}] |
| 195 | +
|
| 196 | + A filter matches, when |
| 197 | + ``<received_can_id> & can_mask == can_id & can_mask`` |
| 198 | +
|
| 199 | + """ |
| 200 | + self.sw_filters = can_filters |
| 201 | + |
| 202 | + if self.sw_filters is None: |
| 203 | + logger.info("Filtering has been disabled") |
| 204 | + else: |
| 205 | + for can_filter in can_filters: |
| 206 | + can_id = can_filter["can_id"] |
| 207 | + can_mask = can_filter["can_mask"] |
| 208 | + logger.info("Filtering on ID 0x%X, mask 0x%X", can_id, can_mask) |
0 commit comments