nbus_module_slave.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. import struct
  2. import time
  3. from threading import Thread, Lock
  4. from typing import Optional
  5. import pandas as pd
  6. import time
  7. import traceback
  8. from threading import Thread, Lock
  9. from typing import Optional, Tuple
  10. import pandas as pd
  11. from nbus_api.nbus_sensor import NBusSensor
  12. from nbus_api.nbus_common_parser import NbusCommonParser
  13. from nbus_hal.crc8 import crc8
  14. from nbus_hal.nbus_generic_port import *
  15. from nbus_types.nbus_address_type import NBusModuleAddress
  16. from nbus_types.nbus_data_fomat import NBusDataValue, NBusDataFormat
  17. from nbus_types.nbus_defines import *
  18. from nbus_types.nbus_exceptions.nbus_api_exception import NBusErrorAPI, NBusErrorAPIType
  19. from nbus_types.nbus_parameter_type import NBusParameterID, NBusParameterValue
  20. from nbus_types.nbus_status_type import NBusStatusType
  21. from nbus_types.nbus_sensor_count_type import NBusSensorCount
  22. from nbus_types.nbus_info_type import NBusModuleInfo
  23. from nbus_types.nbus_sensor_type import NBusSensorType
  24. @beartype
  25. class NBusSlaveModule:
  26. """
  27. Class representing nBus slave module.
  28. """
  29. def __init__(self, port: NBusPort, module_address: NBusModuleAddress, acquire_delay: float):
  30. """
  31. Constructor.
  32. :param port: serial port
  33. :param module_address: address of module
  34. :param device_cnt: number of devices
  35. """
  36. self.__port = port
  37. self.__module_addr = module_address
  38. self.__params = {}
  39. self.__devices = {}
  40. self._lock = Lock() # thread lock
  41. self.__in_acquisition = False # flag when in acquisition
  42. self.__acquire_delay = acquire_delay # intermediate delay between data fetching when data not ready
  43. self.__acquire_thread = None # thread for data acquisition
  44. self.__data_raw = bytearray() # raw data buffer
  45. self.__df = pd.DataFrame() # internal data frame
  46. self.__ts0 = None # 0-th timestamp
  47. self.__payload_size = 0
  48. self.__damaged_packets = []
  49. """
  50. ================================================================================================================
  51. Module General Methods
  52. ================================================================================================================
  53. """
  54. def init(self) -> None:
  55. """
  56. Initialize the module from hardware.
  57. :param load_format: flag to fetch data format
  58. """
  59. sensors = self.cmd_get_sensor_type()
  60. for sen_address, sen_type in sensors.items():
  61. self.__devices[sen_address] = NBusSensor(self.__port, self.__module_addr, sen_address)
  62. self.__devices[sen_address].type = sen_type
  63. self.cmd_get_format()
  64. self.__calculate_payload_size()
  65. def get_devices(self) -> dict[NBusSensorAddress, NBusSensor]:
  66. """
  67. Get module devices.
  68. :return: dictionary of connected devices
  69. """
  70. return self.__devices
  71. def get_damaged_packets(self) -> list[Tuple[int, bytearray]]:
  72. """
  73. Get damaged packets from module stream.
  74. :return: list of damaged packets (id, packet)
  75. """
  76. return self.__damaged_packets
  77. """
  78. ================================================================================================================
  79. Module Get Commands
  80. ================================================================================================================
  81. """
  82. def cmd_get_echo(self, message: bytearray) -> bool:
  83. """
  84. Get echo from module.
  85. :param message: message to send
  86. :return: status (True = echo, False = no echo)
  87. """
  88. _, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_ECHO, message)
  89. return response == list(message)
  90. def cmd_get_param(self, parameter: NBusParameterID) -> NBusParameterValue:
  91. """
  92. Get single module parameter.
  93. :param parameter: parameter id
  94. :return: parameter value
  95. """
  96. # get response
  97. resp_len, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_PARAM,
  98. bytearray([parameter.value]))
  99. # parse parameter
  100. param_id, param_val = NbusCommonParser.parameters_from_response(resp_len, response)[0]
  101. # store parameter value
  102. self.__params[param_id] = param_val
  103. return param_val
  104. def cmd_get_all_params(self) -> dict[NBusParameterID, NBusParameterValue]:
  105. """
  106. Get all module parameters.
  107. :return: dict of parameter id and parameter value
  108. """
  109. # get response
  110. resp_len, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_PARAM, bytearray([]))
  111. # parse parameters
  112. params = NbusCommonParser.parameters_from_response(resp_len, response)
  113. for param_id, param_val in params:
  114. # store parameters
  115. self.__params[param_id] = param_val
  116. return self.__params.copy()
  117. def cmd_get_sensor_cnt(self) -> NBusSensorCount:
  118. """
  119. Get sensor count.
  120. :return: count of read-only and read-write sensors
  121. """
  122. _, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_SENSOR_CNT, bytearray([]))
  123. return NBusSensorCount(*response)
  124. def cmd_get_data(self) -> dict[NBusSensorAddress, list[NBusDataValue]]:
  125. """
  126. Get data from all module sensors.
  127. :return: dict of device addresses and data values
  128. """
  129. # get data
  130. resp_length, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_DATA, bytearray([]))
  131. # parse data
  132. begin_idx = 0
  133. data = {}
  134. while begin_idx < resp_length:
  135. device_id = response[begin_idx]
  136. # handle errors
  137. if self.__devices[device_id].data_format is None: # check for format and params
  138. raise NBusErrorAPI(NBusErrorAPIType.FORMAT_NOT_LOADED)
  139. values, offset = NbusCommonParser.data_from_response(self.__devices[device_id].data_format,
  140. response[begin_idx:])
  141. data[device_id] = values
  142. begin_idx += offset + 1
  143. return data
  144. def cmd_get_info(self) -> NBusModuleInfo:
  145. """
  146. Get module info.
  147. :return: module info
  148. """
  149. response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_INFO, bytearray([]))
  150. name = str(response[1:9], "ascii")
  151. typ = str(response[9:12], "ascii")
  152. uuid = struct.unpack("<I", bytearray(response[12:16]))[0]
  153. hw = str(response[16:19], "ascii")
  154. fw = str(response[19:22], "ascii")
  155. mem_id = struct.unpack("<Q", bytearray(response[22:30]))[0]
  156. ro_count = int(response[30])
  157. rw_count = int(response[31])
  158. return NBusModuleInfo(module_name=name, module_type=typ, uuid=uuid, hw=hw, fw=fw, memory_id=mem_id,
  159. read_only_sensors=ro_count, read_write_sensors=rw_count)
  160. def cmd_get_format(self) -> dict[NBusSensorAddress, NBusDataFormat]:
  161. """
  162. Get format of all on-board sensors.
  163. :return: dict of sensor addresses and sensor formats
  164. """
  165. # get response
  166. resp_length, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_FORMAT,
  167. bytearray([]))
  168. begin_idx = 0
  169. formats = {}
  170. # parse format
  171. while begin_idx < resp_length:
  172. device_id = response[begin_idx]
  173. device_format = NbusCommonParser.format_from_response(response[begin_idx:begin_idx + 4])
  174. self.__devices[device_id].data_format = device_format
  175. formats[device_id] = device_format
  176. begin_idx += 4
  177. return formats
  178. def cmd_get_sensor_type(self) -> dict[NBusSensorAddress, NBusSensorType]:
  179. """
  180. Get type of all on-board sensors.
  181. :return: dict of sensor addresses and sensor types
  182. """
  183. # get response
  184. resp_length, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_SENSOR_TYPE,
  185. bytearray([]))
  186. # parse response
  187. types = {}
  188. i = 0
  189. while i < resp_length - 1:
  190. types[NBusSensorAddress(response[i])] = NBusSensorType(response[i + 1])
  191. i += 2
  192. return types
  193. """
  194. ================================================================================================================
  195. Module Set Commands
  196. ================================================================================================================
  197. """
  198. def cmd_set_module_stop(self) -> None:
  199. """
  200. Stop automatic measuring.
  201. :return: status
  202. """
  203. self.__port.send_module(self.__module_addr, NBusCommand.CMD_SET_STOP, bytearray([]))
  204. def cmd_set_module_start(self) -> None:
  205. """
  206. Start automatic measuring.
  207. :return: status
  208. """
  209. self.__port.send_module(self.__module_addr, NBusCommand.CMD_SET_START, bytearray([]))
  210. def cmd_set_param(self, param: NBusParameterID, value: NBusParameterValue) -> NBusStatusType:
  211. """
  212. Set module parameter.
  213. :param param: parameter ID
  214. :param value: parameter value
  215. :return: status
  216. """
  217. # create request packet
  218. param_id_raw = struct.pack("B", param.value)
  219. param_val_raw = struct.pack("<I",value)
  220. param_bytes = bytearray(param_id_raw) + bytearray(param_val_raw)
  221. # proceed request
  222. _, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_SET_PARAM, param_bytes)
  223. # if response is valid, store parameter
  224. if response[0] == param.value and response[1] == NBusStatusType.STATUS_SUCCESS:
  225. self.__params[param] = value
  226. return NBusStatusType(response[1])
  227. def cmd_set_multi_params(self, params: dict[NBusParameterID, NBusParameterValue]) \
  228. -> dict[NBusParameterID, NBusStatusType]:
  229. """
  230. Set multiple module parameters.
  231. :param params: parameters
  232. :return: dict od statuses
  233. """
  234. # create request packet
  235. param_bytes = NbusCommonParser.parameters_to_request(params)
  236. # send request
  237. resp_length, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_SET_PARAM,
  238. bytearray(param_bytes), long_answer=1.0)
  239. # parse statuses
  240. statuses = {}
  241. for i in range(0, resp_length - 1, 2):
  242. p_id = NBusParameterID(response[i])
  243. if response[i+1] == NBusStatusType.STATUS_SUCCESS:
  244. self.__params[p_id] = params[p_id] # if success, store param
  245. statuses[p_id] = NBusStatusType(response[i + 1])
  246. return statuses
  247. def cmd_set_calibrate(self) -> NBusStatusType:
  248. """
  249. Send calibration command.
  250. :return: calibration status
  251. """
  252. resp_length, *response = self.__port.request_module(self.__module_addr,
  253. NBusCommand.CMD_SET_CALIBRATE, bytearray([]))
  254. print(response)
  255. return NBusStatusType(response[0])
  256. def cmd_set_data(self, data: dict[NBusSensorAddress, list[NBusDataValue]]) \
  257. -> dict[NBusSensorAddress, NBusStatusType]:
  258. """
  259. Set data to read-write sensors.
  260. :param data:
  261. :return: operation statuses
  262. """
  263. # create request packet
  264. request = []
  265. # transform data
  266. for addr in data.keys():
  267. # handle errors
  268. if self.__devices[addr].data_format is None: # check for format and params
  269. raise NBusErrorAPI(NBusErrorAPIType.FORMAT_NOT_LOADED)
  270. raw_data = data[addr]
  271. request.append(addr)
  272. request.extend(NbusCommonParser.data_to_request(self.__devices[addr].data_format, raw_data))
  273. # send request
  274. resp_length, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_SET_DATA,
  275. bytearray(request))
  276. # return response statuses
  277. statuses = {}
  278. for i in range(0, resp_length - 1, 2):
  279. statuses[NBusSensorAddress(response[i])] = NBusStatusType(response[i + 1])
  280. return statuses
  281. """
  282. ================================================================================================================
  283. Automatic Data Stream Methods
  284. ================================================================================================================
  285. """
  286. def start_streaming(self) -> None:
  287. """
  288. Start data streaming (e.g. auto-cast).
  289. """
  290. self.cmd_set_module_start()
  291. self.__acquire_thread = Thread(target=self.__acquire_callback)
  292. self.__in_acquisition = True
  293. # end thread if running
  294. if self.__acquire_thread is not None and self.__acquire_thread.is_alive():
  295. self.__acquire_thread.join()
  296. self.__acquire_thread.start()
  297. def stop_streaming(self):
  298. """
  299. Stop data streaming (e.g. auto-cast).
  300. """
  301. self.cmd_set_module_stop()
  302. self.__in_acquisition = False
  303. if self.__acquire_thread is not None and self.__acquire_thread.is_alive():
  304. self.__acquire_thread.join()
  305. self.__port.flush()
  306. def fetch_stream_chunk(self) -> pd.DataFrame:
  307. """
  308. Fetch data from stream (e.g. auto-cast).
  309. Can be called anytime.
  310. It not erase internal dataframe.
  311. :return: stream data frame
  312. """
  313. with self._lock:
  314. packets = self.__data_raw.split(NBUS_BRIDGE_DATA_HDR)[1:]
  315. packet_cnt = len(packets) - self.__in_acquisition
  316. parsed_packets = []
  317. # parse packets
  318. for i in range(packet_cnt):
  319. data = self._parse_data_from_stream_packet(packets[i])
  320. if data is not None:
  321. parsed_packets.append(data)
  322. else:
  323. print("damaged: ", i, packets[i])
  324. # extend internal dataframe
  325. if parsed_packets:
  326. data_frame = pd.DataFrame(parsed_packets)
  327. self.__df = pd.concat([self.__df, data_frame], ignore_index=True).copy(deep=True)
  328. else:
  329. data_frame = pd.DataFrame()
  330. # erase raw data buffer
  331. if self.__in_acquisition:
  332. unparsed_bytes = len(packets[-1]) + NBUS_BRIDGE_DATA_HDR_SIZE
  333. self.__data_raw = self.__data_raw[-unparsed_bytes:]
  334. else:
  335. self.__data_raw = bytearray()
  336. self._transform_timestamp(data_frame)
  337. return data_frame
  338. def fetch_full_stream(self) -> pd.DataFrame:
  339. """
  340. Fetch all data from stream (e.g. auto-cast).
  341. Must be called after stop_streaming() method.
  342. It will erase internal dataframe.
  343. :return: stream dataframe
  344. """
  345. if self.__in_acquisition:
  346. return pd.DataFrame()
  347. self.fetch_stream_chunk()
  348. df = self.__df
  349. self._transform_timestamp(df)
  350. self.__df = pd.DataFrame()
  351. self.__ts0 = None
  352. return df
  353. """
  354. ================================================================================================================
  355. Internal Helper Methods
  356. ================================================================================================================
  357. """
  358. def _transform_timestamp(self, data_frame: pd.DataFrame) -> None:
  359. """
  360. Transform timestamp values in dataframe.
  361. :param data_frame: dataframe to transform
  362. """
  363. if not data_frame.empty and "TS" in self.__df.columns:
  364. if self.__ts0 is None:
  365. self.__ts0 = self.__df["TS"].iloc[0]
  366. data_frame["TS"] -= self.__ts0
  367. def _parse_data_from_stream_response(self, module_address: NBusModuleAddress, resp_length: int, response: bytearray) \
  368. -> dict[str, NBusDataValue]:
  369. """
  370. Parse data of slave from stream response.
  371. :param module_address: address of module
  372. :param resp_length: length of response
  373. :param response: raw data
  374. :return: dict of data values
  375. """
  376. data_offset = 0
  377. data = {}
  378. while data_offset < resp_length:
  379. device_id = response[data_offset]
  380. if self.__devices[device_id].data_format is None:
  381. raise NBusErrorAPI(NBusErrorAPIType.FORMAT_NOT_LOADED)
  382. values, offset = NbusCommonParser.data_from_response(self.__devices[device_id].data_format, response[data_offset:])
  383. data_tag = str(module_address) + "." + str(device_id)
  384. for i in range(len(values)):
  385. data[data_tag + "." + str(i + 1)] = values[i]
  386. data_offset += offset + NBUS_SA_SIZE
  387. return data
  388. def _parse_data_from_stream_packet(self, data_packet: bytearray) \
  389. -> Optional[dict[str, NBusDataValue]]:
  390. """
  391. Parse the stream-mode data packet.
  392. :param data_packet: packet to parse
  393. :return: dictionary of data values or None
  394. """
  395. packet_size = len(data_packet)
  396. packet_crc = crc8(data_packet[:NBUS_CRC_ADDR])
  397. # check validity
  398. if packet_size < NBUS_TS_SIZE + NBUS_CRC_SIZE or data_packet[NBUS_CRC_ADDR] != packet_crc:
  399. return None
  400. # parse data
  401. try:
  402. data_offset = 0
  403. ts = struct.unpack("<I", data_packet[data_offset: data_offset + NBUS_TS_SIZE])[0]
  404. data = {"TS": ts}
  405. data_offset += NBUS_TS_SIZE
  406. module_addr = data_packet[data_offset]
  407. data_offset += NBUS_MA_SIZE
  408. packet = data_packet[data_offset: data_offset + self.__payload_size]
  409. data |= self._parse_data_from_stream_response(module_addr, self.__payload_size, packet)
  410. data_offset += self.__payload_size
  411. return data
  412. except Exception:
  413. return None
  414. def __calculate_payload_size(self):
  415. """
  416. Calculate payload size of stream-mode packet.
  417. :return: payload size in bytes
  418. """
  419. for device in self.__devices.values():
  420. fmt = device.data_format
  421. self.__payload_size += fmt.byte_length * fmt.samples + NBUS_SA_SIZE
  422. def __acquire_callback(self) -> None:
  423. """
  424. Thread worker for data acquisition.
  425. """
  426. while self.__in_acquisition:
  427. data = self.__port.try_read()
  428. if not data:
  429. time.sleep(self.__acquire_delay)
  430. continue
  431. with self._lock:
  432. self.__data_raw.extend(data)