소스 검색

Merge branch 'MN_BRIDGE' of irep/nBus-Client-API into master

xnecas 11 시간 전
부모
커밋
e5884d9bbd
46개의 변경된 파일1057개의 추가작업 그리고 305개의 파일을 삭제
  1. 2 0
      .gitignore
  2. 4 1
      .idea/misc.xml
  3. 3 1
      .idea/nbus_api.iml
  4. 6 0
      .idea/vcs.xml
  5. 54 0
      BEYOND_README.md
  6. 15 0
      LICENSE
  7. 187 4
      README.md
  8. 67 0
      examples/example_bridge.py
  9. 93 0
      examples/example_slave_module.py
  10. BIN
      nbus_api/__pycache__/nbus_bridge.cpython-313.pyc
  11. BIN
      nbus_api/__pycache__/nbus_common_parser.cpython-313.pyc
  12. BIN
      nbus_api/__pycache__/nbus_module_slave.cpython-313.pyc
  13. BIN
      nbus_api/__pycache__/nbus_sensor.cpython-313.pyc
  14. 401 0
      nbus_api/nbus_bridge.py
  15. 1 1
      nbus_api/nbus_common_parser.py
  16. 29 53
      nbus_api/nbus_module_slave.py
  17. 17 105
      nbus_api/nbus_sensor.py
  18. BIN
      nbus_hal/__pycache__/crc8.cpython-313.pyc
  19. BIN
      nbus_hal/__pycache__/nbus_generic_port.cpython-313.pyc
  20. 38 1
      nbus_hal/nbus_generic_port.py
  21. BIN
      nbus_hal/nbus_serial/__pycache__/serial_config.cpython-313.pyc
  22. BIN
      nbus_hal/nbus_serial/__pycache__/serial_port.cpython-313.pyc
  23. 2 0
      nbus_hal/nbus_serial/serial_config.py
  24. 58 13
      nbus_hal/nbus_serial/serial_port.py
  25. 0 24
      nbus_sensor_drivers/generic_sensor_driver.py
  26. BIN
      nbus_types/__pycache__/nbus_address_type.cpython-313.pyc
  27. BIN
      nbus_types/__pycache__/nbus_command_type.cpython-313.pyc
  28. BIN
      nbus_types/__pycache__/nbus_data_fomat.cpython-313.pyc
  29. BIN
      nbus_types/__pycache__/nbus_defines.cpython-313.pyc
  30. BIN
      nbus_types/__pycache__/nbus_info_type.cpython-313.pyc
  31. BIN
      nbus_types/__pycache__/nbus_parameter_type.cpython-313.pyc
  32. BIN
      nbus_types/__pycache__/nbus_sensor_count_type.cpython-313.pyc
  33. BIN
      nbus_types/__pycache__/nbus_sensor_type.cpython-313.pyc
  34. BIN
      nbus_types/__pycache__/nbus_slave_meta_type.cpython-313.pyc
  35. BIN
      nbus_types/__pycache__/nbus_status_type.cpython-313.pyc
  36. 1 0
      nbus_types/nbus_command_type.py
  37. 29 0
      nbus_types/nbus_defines.py
  38. BIN
      nbus_types/nbus_exceptions/__pycache__/nbus_api_exception.cpython-313.pyc
  39. BIN
      nbus_types/nbus_exceptions/__pycache__/nbus_network_exception.cpython-313.pyc
  40. BIN
      nbus_types/nbus_exceptions/__pycache__/nbus_node_exception.cpython-313.pyc
  41. 1 1
      nbus_types/nbus_exceptions/nbus_api_exception.py
  42. 18 5
      nbus_types/nbus_info_type.py
  43. 2 0
      nbus_types/nbus_parameter_type.py
  44. 8 7
      nbus_types/nbus_sensor_type.py
  45. 21 0
      nbus_types/nbus_slave_meta_type.py
  46. 0 89
      test.py

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+*.csv
+*.venv

+ 4 - 1
.idea/misc.xml

@@ -1,4 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (nbus_api)" project-jdk-type="Python SDK" />
+  <component name="Black">
+    <option name="sdkName" value="Python 3.13 (nBus-Client-API)" />
+  </component>
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (nBus-Client-API)" project-jdk-type="Python SDK" />
 </project>

+ 3 - 1
.idea/nbus_api.iml

@@ -2,9 +2,11 @@
 <module type="PYTHON_MODULE" version="4">
   <component name="NewModuleRootManager">
     <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
+      <excludeFolder url="file://$MODULE_DIR$/.venv" />
       <excludeFolder url="file://$MODULE_DIR$/im" />
     </content>
-    <orderEntry type="jdk" jdkName="Python 3.9 (nbus_api)" jdkType="Python SDK" />
+    <orderEntry type="jdk" jdkName="Python 3.13 (nBus-Client-API)" jdkType="Python SDK" />
     <orderEntry type="sourceFolder" forTests="false" />
   </component>
   <component name="PackageRequirementsSettings">

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 54 - 0
BEYOND_README.md

@@ -0,0 +1,54 @@
+This code is human-written.
+Not perfect, not pure — but every line carries the echo of a man who tried:
+a true story of who I was,
+and who I wanted to be.
+
+This code wasn’t crafted on a delightful terrace.
+It was forged in the quiet hours —
+through crimson tides of mistakes,
+through sunsets when even hope felt out of reach.
+And still I kept writing,
+as if each line could pull me closer to something that mattered.
+
+It’s a machine, yes —
+but more like a lone Formula 1 phantom on a rain-soaked track:
+meant to run, meant to endure,
+meant to survive the pit stops where you question everything.
+
+Runnable.
+Repairable.
+Built to finish the race.
+
+To me, it stands as a reward for caring about the hidden details —
+the marbles in the margins,
+the silent voices no one will ever hear.
+The kind of beauty only its creator ever notices,
+and only in the quiet.
+
+But we have reached the gates of the Artificial Age,
+washing the passion away — not only from silicon scripts,
+but from the people who flash them.
+Creators turn into operators,
+thinkers into copyists,
+engineers into extensions of someone else’s shadow.
+
+The code stays velvet-warm on the surface,
+yet lacks the inner structure meant to last for decades.
+
+And it’s painfully tempting to walk that treacherous path —
+to become a programming zombie, marching through borrowed thoughts,
+fast, efficient, and hollow inside.
+
+But I refuse.
+
+I won’t trade consciousness for speed.
+I won’t lose my voice to the noise.
+I won’t let the work of code
+collapse into mere instructions.
+
+I want to see every line I write.
+I want to remember why I began.
+I want to be alive —
+even when the world forgets what that means...
+
+- Matúš

+ 15 - 0
LICENSE

@@ -0,0 +1,15 @@
+Copyright 2025 Matúš Nečas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the “Software”), to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.

+ 187 - 4
README.md

@@ -1,5 +1,188 @@
-# nBus Client API 
+# nBus Client API
 
-This project provides a Python client API for the nBus protocol over a serial port. 
-It offers a high-level, strictly object-oriented approach, ensuring type safety, clear error handling
-and maintainability. It uses The Beatype runtime typechecking.
+The **nBus Client API** is a Python library providing a high-level, strictly object-oriented interface for communicating with devices over the **nBus protocol**.
+
+It focuses on:
+
+- Clean architecture  
+- Strong runtime type safety (via *Beatype*)  
+- Predictable error handling  
+- Maintainable abstractions for multi-sensor systems  
+
+The API exposes **three levels of control**:
+
+1. **`NBusSensor`** – individual sensors  
+2. **`NBusSlaveModule`** – modules containing multiple sensors  
+3. **`NBusBridge`** – top-level interface for managing modules and data streams
+
+## Serial Configuration
+
+All communication begins by creating an `NBusSerialPort` with a configuration object:
+
+```python
+from nbus_hal.nbus_serial.serial_config import *
+
+config = {
+    "port_name": "COM4",
+    "baud": NBusBaudrate.SPEED_921600,
+    "parity": NBusParity.NONE,
+    "timeout": 1.0,
+    "flush_delay": 0.05,
+    "request_attempts": 1,
+    "enable_log": False
+}
+
+port = NBusSerialPort(NBusSerialConfig(**config))
+```
+
+---
+
+# 1. Using the NBusBridge
+
+The **Bridge** represents the top-level interface for the entire nBus network.  
+It discovers modules, retrieves global sensor frames, and enables continuous streaming.
+
+### Example
+
+```python
+import time
+from nbus_hal.nbus_serial.serial_port import *
+from nbus_api.nbus_bridge import NBusBridge
+
+BRIDGE_ACQUIRE_WAIT = 0.5
+BRIDGE_STREAM_INTERVAL = 5.0
+
+port = NBusSerialPort(NBusSerialConfig(**config))
+bridge = NBusBridge(port, BRIDGE_ACQUIRE_WAIT)
+
+try:
+    bridge.init()
+
+    # --- Bridge GET commands ---
+    print("INFO:", bridge.cmd_get_info())
+    print("SLAVES:", bridge.cmd_get_slaves())
+    print("FORMAT:", bridge.cmd_get_format())
+    print("DATA:", bridge.cmd_get_data())
+
+    # --- Bridge SET commands ---
+    print("RESET:", bridge.cmd_set_reset())
+
+    # --- Streaming example ---
+    print(f"START STREAM FOR {BRIDGE_STREAM_INTERVAL} SECONDS")
+    bridge.start_streaming()
+    time.sleep(BRIDGE_STREAM_INTERVAL)
+
+    print("STREAM CHUNK:", bridge.fetch_stream_chunk())
+
+    print(f"STOP STREAM")
+    bridge.stop_streaming()
+
+    print("SAVE FULL STREAM")
+    df = bridge.fetch_full_stream()
+    df.to_csv("data.csv", index=False)
+
+finally:
+    port.flush()
+    port.close()
+```
+
+---
+
+# 2. Using the NBusSlaveModule
+
+A **Module** manages multiple sensors.
+
+### Example
+
+```python
+from nbus_api.nbus_module_slave import NBusSlaveModule
+from nbus_types.nbus_parameter_type import NBusParameterID
+
+port = NBusSerialPort(NBusSerialConfig(**config))
+module = NBusSlaveModule(port, 5)
+
+try:
+    module.init(False)
+
+    devices = module.get_devices()
+    accelerometer = devices[1]
+    led = devices[129]
+
+    print(module.cmd_get_echo(b"Hello world!"))
+    print(module.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
+    print(module.cmd_get_all_params())
+    print(module.cmd_get_sensor_cnt())
+    print(module.cmd_get_sensor_type())
+    print(module.cmd_get_info())
+    print(module.cmd_get_format())
+    print(module.cmd_get_data())
+
+    module.cmd_set_module_stop()
+    module.cmd_set_module_start()
+
+    module.cmd_set_param(NBusParameterID.PARAM_SAMPLERATE, 12345)
+
+    params = {
+        NBusParameterID.PARAM_RANGE: 12,
+        NBusParameterID.PARAM_RANGE0: 4234
+    }
+    module.cmd_set_multi_params(params)
+
+    module.cmd_set_calibrate()
+    module.cmd_set_data({129: [1], 130: [10, -32]})
+
+finally:
+    port.flush()
+    port.close()
+```
+
+---
+
+# 3. Using NBusSensor
+
+### Example
+
+```python
+accelerometer = devices[1]
+print(accelerometer.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
+print(accelerometer.cmd_get_all_params())
+print(accelerometer.cmd_get_sensor_type())
+print(accelerometer.cmd_get_format())
+print(accelerometer.cmd_get_data())
+
+led = devices[129]
+led.cmd_set_find(True)
+led.cmd_set_param(NBusParameterID.PARAM_SAMPLERATE, 23456)
+led.cmd_set_multi_params({
+    NBusParameterID.PARAM_RANGE: 12,
+    NBusParameterID.PARAM_RANGE0: 4234
+})
+led.cmd_set_calibrate()
+led.cmd_set_data([1])
+print(led.cmd_get_data())
+```
+
+---
+
+# API Architecture Overview
+
+```
+┌──────────────────────────┐
+│       NBusBridge         │
+└──────────────┬───────────┘
+               │ 2..*
+┌──────────────▼───────────┐
+│    NBusSlaveModule       │
+└──────────────┬───────────┘
+               │ 1..*
+┌──────────────▼───────────┐
+│        NBusSensor        │
+└──────────────────────────┘
+```
+
+# License
+
+MIT License
+
+# Author
+[*This code is human-written*](BEYOND_README.md) by Matúš Nečas, 2024-2025

+ 67 - 0
examples/example_bridge.py

@@ -0,0 +1,67 @@
+import sys
+from nbus_hal.nbus_serial.serial_port import *
+from nbus_api.nbus_bridge import NBusBridge
+from nbus_hal.nbus_serial.serial_config import *
+
+# example config
+config = {
+    "port_name": "COM4",
+    "baud": NBusBaudrate.SPEED_921600,
+    "parity": NBusParity.NONE,
+    "timeout": 1.0,
+    "flush_delay": 0.05,
+    "request_attempts": 1,
+    "enable_log": False
+}
+
+BRIDGE_ACQUIRE_WAIT = 0.5
+BRIDGE_STREAM_INTERVAL = 5.0
+
+if __name__ == "__main__":
+    # create port
+    port = NBusSerialPort(NBusSerialConfig(**config))
+    # create module
+    bridge = NBusBridge(port, BRIDGE_ACQUIRE_WAIT)
+
+    # run commands
+    try:
+        # initialize bridge
+        bridge.init()
+
+        # test bridge get
+        print("<<TEST BRIDGE GET>>")
+        print("CMD GET INFO: ", bridge.cmd_get_info())
+        print("CMD GET SLAVES: ", bridge.cmd_get_slaves())
+        print("CMD GET FORMAT: ", bridge.cmd_get_format())
+        print("CMD GET DATA: ",bridge.cmd_get_data())
+
+        # test bridge set
+        print("<<TEST BRIDGE SET>>")
+        print("CMD SET RESET: ",bridge.cmd_set_reset())
+
+        print("<<TEST BRIDGE STREAMING>>")
+
+        print(f"START STREAM FOR {BRIDGE_STREAM_INTERVAL} SECONDS")
+        bridge.start_streaming()
+        time.sleep(BRIDGE_STREAM_INTERVAL)
+
+
+        print("FETCH STREAM CHUNK:", bridge.fetch_stream_chunk())
+        print(f"WAIT FOR {BRIDGE_STREAM_INTERVAL} SECONDS")
+        time.sleep(BRIDGE_STREAM_INTERVAL)
+
+        print(f"STOP STREAM AFTER {2*BRIDGE_STREAM_INTERVAL} SECONDS")
+        bridge.stop_streaming()
+
+        print("FETCH STREAM CHUNK:", bridge.fetch_stream_chunk())
+
+        print("SAVE FULL STREAM:")
+        df = bridge.fetch_full_stream()
+        df.to_csv("data.csv", index=False)
+
+    except Exception as e:
+        print(str(e))
+
+    finally:
+        port.flush()
+        port.close()

+ 93 - 0
examples/example_slave_module.py

@@ -0,0 +1,93 @@
+import sys
+
+from nbus_hal.nbus_serial.serial_port import *
+from nbus_api.nbus_module_slave import NBusSlaveModule
+from nbus_hal.nbus_serial.serial_config import *
+from nbus_types.nbus_parameter_type import NBusParameterID
+
+# example config
+config = {
+    "port_name": "COM4",
+    "baud": NBusBaudrate.SPEED_921600,
+    "parity": NBusParity.NONE,
+    "timeout": 99.0,
+    "flush_delay": 0.05,
+    "request_attempts": 1,
+    "enable_log": False
+}
+
+if __name__ == "__main__":
+
+    # create port
+    port = NBusSerialPort(NBusSerialConfig(**config))
+    # create module
+    module = NBusSlaveModule(port, 5)
+
+    # run commands
+    try:
+        # initialize module from hardware
+        module.init(False)   # initialize and load format
+
+        # get devices
+        devices = module.get_devices()
+        accelerometer = devices[1]
+        led = devices[129]
+
+        # test module get
+        print("<<TEST MODULE GET>>")
+        print("CMD GET ECHO: ", module.cmd_get_echo(bytearray("Hello world!", "ascii")))
+        print("CMD GET PARAM SAMPLERATE: ", module.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
+        print("CMD GET ALL PARAMS: ", module.cmd_get_all_params())
+        print("CMD GET SENSOR COUNT: ", module.cmd_get_sensor_cnt())
+        print("CMD GET SENSOR TYPE: ", module.cmd_get_sensor_type())
+        print("CMD GET INFO: ", module.cmd_get_info())
+        print("CMD GET SENSOR FORMAT: ", module.cmd_get_format())
+        print("CMD GET SENSOR DATA: ", module.cmd_get_data())
+
+        # test module set
+        print("\n<<TEST MODULE SET>>")
+        print("CMD SET STOP: ", module.cmd_set_module_stop())
+        print("CMD SET START: ", module.cmd_set_module_start())
+
+        print("CMD SET PARAM SAMPLERATE: ", module.cmd_set_param(NBusParameterID.PARAM_SAMPLERATE, 12345))
+        print("CMD GET PARAM SAMPLERATE: ", module.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
+
+        params = {NBusParameterID.PARAM_RANGE: 12, NBusParameterID.PARAM_RANGE0: 4234}
+        print("CMD SET MULTI PARAMS: ", module.cmd_set_multi_params(params))
+        print("CMD GET ALL PARAMS: ", module.cmd_get_all_params())
+
+        print("CMD SET CALIBRATE: ", module.cmd_set_calibrate())
+
+        data = {129: [1], 130: [10, -32]}
+        print("CMD SET DATA: ", module.cmd_set_data(data))
+        print("CMD GET SENSOR DATA: ", module.cmd_get_data())
+
+        # test sensor get
+        print("\n<<TEST SENSOR GET>>")
+        print("CMD GET PARAM SAMPLERATE: ", accelerometer.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
+        print("CMD GET ALL PARAMS: ", accelerometer.cmd_get_all_params())
+        print("CMD GET SENSOR TYPE: ", accelerometer.cmd_get_sensor_type())
+        print("CMD GET SENSOR FORMAT: ", accelerometer.cmd_get_format())
+        print("CMD GET SENSOR DATA: ", accelerometer.cmd_get_data())
+
+        # test sensor set
+        print("\n<<TEST SENSOR SET>>")
+        print("CMD SET FIND: ", led.cmd_set_find(True))
+        print("CMD SET PARAM SAMPLERATE: ", led.cmd_set_param(NBusParameterID.PARAM_SAMPLERATE, 23456))
+        print("CMD GET PARAM SAMPLERATE: ", led.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
+
+        params = {NBusParameterID.PARAM_RANGE: 12, NBusParameterID.PARAM_RANGE0: 4234}
+        print("CMD SET MULTI PARAMS: ", led.cmd_set_multi_params(params))
+        print("CMD GET ALL PARAMS: ", led.cmd_get_all_params())
+
+        print("CMD SET CALIBRATE: ", led.cmd_set_calibrate())
+
+        print("CMD SET DATA: ", led.cmd_set_data([1]))
+        print("CMD GET SENSOR DATA: ", led.cmd_get_data())
+
+    except Exception as e:
+        print(e)
+
+    finally:
+        port.flush()
+        port.close()

BIN
nbus_api/__pycache__/nbus_bridge.cpython-313.pyc


BIN
nbus_api/__pycache__/nbus_common_parser.cpython-313.pyc


BIN
nbus_api/__pycache__/nbus_module_slave.cpython-313.pyc


BIN
nbus_api/__pycache__/nbus_sensor.cpython-313.pyc


+ 401 - 0
nbus_api/nbus_bridge.py

@@ -0,0 +1,401 @@
+import struct
+import time
+from typing import Optional
+from threading import Thread, Lock
+from beartype import beartype
+import pandas as pd
+
+from nbus_api.nbus_common_parser import NbusCommonParser
+from nbus_api.nbus_module_slave import NBusSlaveModule
+from nbus_api.nbus_sensor import NBusSensor
+from nbus_hal.crc8 import crc8
+from nbus_hal.nbus_generic_port import NBusPort
+from nbus_types.nbus_address_type import NBusModuleAddress, NBusSensorAddress
+from nbus_types.nbus_command_type import NBusCommand
+from nbus_types.nbus_data_fomat import NBusDataFormat, NBusDataValue
+from nbus_types.nbus_defines import *
+from nbus_types.nbus_exceptions.nbus_api_exception import NBusErrorAPI, NBusErrorAPIType
+from nbus_types.nbus_info_type import NBusBridgeInfo
+from nbus_types.nbus_sensor_count_type import NBusSensorCount
+from nbus_types.nbus_slave_meta_type import NBusSlaveMeta
+from nbus_types.nbus_status_type import NBusStatusType
+
+@beartype
+class NBusBridge:
+
+    def __init__(self, serial_port: NBusPort, acquire_delay: float):
+        """
+        Constructor.
+
+        :param serial_port: serial port
+        :param acquire_delay: intermediate delay between data fetching when data not ready
+        :param flush_delay: delay between stream flush
+        """
+        self.__port = serial_port               # serial port reference
+        self.__slaves_meta = {}                 # list of slaves meta information
+        self.__acquire_thread = None            # thread for data acquisition
+        self.__data_raw = bytearray()           # raw data buffer
+        self._lock = Lock()                     # thread lock
+        self.__in_acquisition = False           # flag when in acquisition
+        self.__acquire_delay = acquire_delay    # intermediate delay between data fetching when data not ready
+        self.__df = pd.DataFrame()              # internal data frame
+        self.__ts0 = None                       # 0-th timestamp
+
+    """
+    ================================================================================================================
+                                              Module General Methods
+    ================================================================================================================
+    """
+    def init(self) -> None:
+        """
+        Initialize the bridge module from network.
+        """
+        try:
+            self.cmd_get_slaves()
+            self.cmd_get_format()
+        except Exception:
+            self.panic()
+
+    def panic(self) -> None:
+        """
+        Reset communication when fatal error occurs.
+        """
+        self.stop_streaming()
+        self.cmd_set_reset()
+
+        self.__data_raw = bytearray()
+        self.__df = pd.DataFrame()
+        self.__slaves_meta = {}
+
+        self.cmd_get_slaves()
+        self.cmd_get_format()
+
+    def get_slaves(self) -> dict[NBusModuleAddress, NBusSlaveModule]:
+        """
+        Get connected slave modules.
+
+        :return: dictionary of connected slaves
+        """
+        slaves = {}
+
+        for key, value in self.__slaves_meta.items():
+            slaves[key] = value.module_obj
+
+        return slaves
+
+    def start_streaming(self) -> None:
+        """
+        Start data streaming (e.g. bridge-cast).
+        """
+        self.__port.send_bridge(NBusCommand.CMD_SET_START, bytearray())
+        self.__acquire_thread = Thread(target=self.__acquire_callback)
+        self.__in_acquisition = True
+
+        # end thread if running
+        if self.__acquire_thread is not None and self.__acquire_thread.is_alive():
+            self.__acquire_thread.join()
+
+        self.__acquire_thread.start()
+
+    def stop_streaming(self):
+        """
+        Stop data streaming (e.g. bridge-cast).
+        """
+        self.__port.send_bridge(NBusCommand.CMD_SET_STOP, bytearray())
+
+        self.__in_acquisition = False
+
+        if self.__acquire_thread is not None and self.__acquire_thread.is_alive():
+            self.__acquire_thread.join()
+
+        self.__port.flush()
+
+    def fetch_stream_chunk(self) -> pd.DataFrame:
+        """
+        Fetch data from stream (e.g. bridge-cast).
+        Can be called anytime.
+        It not erase internal dataframe.
+
+        :return: stream data frame
+        """
+        with self._lock:
+            packets = self.__data_raw.split(NBUS_BRIDGE_DATA_HDR)
+            packet_cnt = len(packets) - self.__in_acquisition
+            parsed_packets = []
+
+            # parse packets
+            for i in range(packet_cnt):
+                data = self._parse_bridge_data_from_packet(packets[i], True)
+                if data is not None:
+                    parsed_packets.append(data)
+
+            # extend internal dataframe
+            if parsed_packets:
+                data_frame = pd.DataFrame(parsed_packets)
+                self.__df = pd.concat([self.__df, data_frame], ignore_index=True).copy(deep=True)
+            else:
+                data_frame = pd.DataFrame()
+
+            # erase raw data buffer
+            if self.__in_acquisition:
+                unparsed_bytes = len(packets[-1]) + NBUS_BRIDGE_DATA_HDR_SIZE
+                self.__data_raw = self.__data_raw[-unparsed_bytes:]
+            else:
+                self.__data_raw = bytearray()
+
+        self._transform_timestamp(data_frame)
+
+        return data_frame
+
+    def fetch_full_stream(self) -> pd.DataFrame:
+        """
+        Fetch all data from stream (e.g. bridge-cast).
+        Must be called after stop_streaming() method.
+        It will erase internal dataframe.
+
+        :return: stream dataframe
+        """
+        if self.__in_acquisition:
+            return pd.DataFrame()
+
+        df = self.__df
+        self._transform_timestamp(df)
+
+        self.__df = pd.DataFrame()
+        self.__ts0 = None
+
+        return df
+
+    """
+    ================================================================================================================
+                                                    Bridge Get Commands
+    ================================================================================================================
+    """
+    def cmd_get_data(self) -> pd.DataFrame:
+        """
+        Get data from all slave devices.
+
+        :return: dataframe of the network data
+        """
+        response = self.__port.request_bridge(NBusCommand.CMD_GET_DATA, bytearray())
+        return pd.DataFrame([self._parse_bridge_data_from_packet(response[1:], False)])
+
+    def cmd_get_info(self) -> NBusBridgeInfo:
+        """
+        Get data from all slave devices.
+
+        :return: bridge info structure
+        """
+        response = self.__port.request_bridge(NBusCommand.CMD_GET_INFO, bytearray())
+        fw = str(response[1:4], "ascii")
+        hw_family = str(response[4:7], "ascii")
+        hw = str(response[7:10], "ascii")
+
+        return NBusBridgeInfo(fw=fw, hw_family=hw_family, hw=hw)
+
+    def cmd_get_format(self) -> dict[tuple[NBusModuleAddress, NBusSensorAddress], NBusDataFormat]:
+        """
+         Get format of all connected devices.
+
+         :return: dict of all device formats
+         """
+        if not self.__slaves_meta:
+            raise NBusErrorAPI(NBusErrorAPIType.SLAVES_NOT_LOADED)
+
+        resp_length, *response = self.__port.request_bridge(NBusCommand.CMD_GET_FORMAT, bytearray())
+        data_offset = 0
+        fmt = {}
+
+        while data_offset < resp_length:
+            slave_addr = response[data_offset]
+            data_offset += NBUS_MA_SIZE
+
+            slave_sensor_cnt = self.__slaves_meta[slave_addr].device_count
+            format_len = NBUS_FMT_SIZE * (slave_sensor_cnt.read_only_count + slave_sensor_cnt.read_write_count)
+
+            fmt |= self._set_slave_format_from_response(slave_addr, format_len,
+                                                        response[data_offset: data_offset + format_len])
+
+            data_offset += format_len
+
+        self._update_slaves_packet_size()
+        return fmt
+
+    def cmd_get_slaves(self) -> dict[NBusModuleAddress, NBusSensorCount]:
+        """
+        Get base information about all connected slaves and update internal fields.
+
+        :return: dict of device count of every connected module
+        """
+        resp_length, *response = self.__port.request_bridge(NBusCommand.CMD_GET_SLAVES, bytearray())
+        slaves = {}
+        data_offset = 0
+
+        while data_offset < resp_length - 2:
+            slave_addr = response[data_offset]
+            slave_sensor_cnt = NBusSensorCount(response[data_offset + 1] , response[data_offset + 2])
+
+            slaves[slave_addr] = slave_sensor_cnt
+
+            # update internal memory
+            if slave_addr not in self.__slaves_meta:
+                self.__slaves_meta[slave_addr] = NBusSlaveMeta(
+                    NBusSlaveModule(self.__port, slave_addr), slave_sensor_cnt, 0)
+
+            data_offset += 3
+
+        return slaves
+
+    """
+    ================================================================================================================
+                                                  Bridge Get Commands
+    ================================================================================================================
+    """
+    def cmd_set_reset(self) -> NBusStatusType:
+        """
+        Send reset command to bridge module.
+
+        :return: status of success
+        """
+        resp_length, *response = self.__port.request_bridge(NBusCommand.CMD_SET_RESET, bytearray())
+        return NBusStatusType(response[0])
+
+    """
+    ================================================================================================================
+                                                  Internal Methods
+    ================================================================================================================
+    """
+    def __acquire_callback(self) -> None:
+        """
+        Thread worker for data acquisition.
+        """
+        while self.__in_acquisition:
+            data = self.__port.try_read()
+
+            if not data:
+                time.sleep(self.__acquire_delay)
+                continue
+
+            with self._lock:
+                self.__data_raw.extend(data)
+
+    def _parse_bridge_data_from_packet(self, data_packet: bytearray, check_packet: bool) \
+            -> Optional[dict[str, NBusDataValue]]:
+        """
+        Parse the bridge data packet.
+
+        :param data_packet: packet to parse
+        :param check_packet: if crc check is required (only in bridge-cast)
+
+        :return: dictionary of data values or None
+        """
+        packet_size = len(data_packet)
+        packet_crc = crc8(data_packet[:NBUS_CRC_ADDR])
+        # check validity
+        if check_packet and (packet_size < NBUS_TS_SIZE + NBUS_CRC_SIZE or data_packet[NBUS_CRC_ADDR] != packet_crc):
+            return None
+
+        # parse data
+        try:
+            data_offset = 0
+            ts = struct.unpack("<I", data_packet[data_offset : data_offset + NBUS_TS_SIZE])[0]
+            data = {"TS": ts}
+            data_offset += NBUS_TS_SIZE
+
+            while data_offset < packet_size - NBUS_CRC_SIZE:
+                module_addr = data_packet[data_offset]
+                data_offset += NBUS_MA_SIZE
+                slave_size = self.__slaves_meta[module_addr].packet_size
+                packet = data_packet[data_offset : data_offset + slave_size]
+
+                data |= self._parse_slave_data_from_response(module_addr, slave_size, packet)
+                data_offset += slave_size
+
+            return data
+
+        except Exception:
+            return None
+
+    def _update_slaves_packet_size(self) -> None:
+        """
+        Update slaves packet size from loaded format.
+        """
+        for slave_meta in self.__slaves_meta.values():
+            packet_size = 0
+
+            for device in slave_meta.module_obj.get_devices().values():
+                fmt = device.data_format
+                packet_size += fmt.byte_length * fmt.samples + NBUS_SA_SIZE
+
+            slave_meta.packet_size = packet_size
+
+    def _transform_timestamp(self, data_frame: pd.DataFrame) -> None:
+        """
+        Transform timestamp values in dataframe.
+        :param data_frame: dataframe to transform
+        """
+        if not data_frame.empty and "TS" in self.__df.columns:
+            if self.__ts0 is None:
+                self.__ts0 = self.__df["TS"].iloc[0]
+
+            data_frame["TS"] -= self.__ts0
+
+    def _set_slave_format_from_response(self, slave_addr: NBusModuleAddress, resp_length: int, response: list[int]) \
+            -> dict[tuple[NBusModuleAddress, NBusSensorAddress], NBusDataFormat]:
+        """
+        Parse and set format for all slaves from response.
+
+        :param slave_addr: address of slave module
+        :param resp_length: length of response
+        :param response: raw data
+
+        :return: parsed formats
+        """
+        data_offset = 0
+        formats = {}
+
+        devices  = self.__slaves_meta[slave_addr].module_obj.get_devices()
+
+        # parse format
+        while data_offset < resp_length:
+            device_id = response[data_offset]
+            device_format = NbusCommonParser.format_from_response(response[data_offset : data_offset + NBUS_FMT_SIZE])
+
+            if device_id not in devices:
+                devices[device_id] = NBusSensor(self.__port, slave_addr, device_id)
+
+            devices[device_id].data_format = device_format
+            formats[(NBusModuleAddress(slave_addr), NBusSensorAddress(device_id))] = device_format
+            data_offset += NBUS_FMT_SIZE
+
+        return formats
+
+    def _parse_slave_data_from_response(self, slave_addr: NBusModuleAddress, resp_length: int, response: bytearray) \
+            -> dict[str, NBusDataValue]:
+        """
+        Parse data of single slave from response.
+        :param slave_addr: address of selected slave
+        :param resp_length: length of response
+        :param response: raw data
+
+        :return: dict of data values
+        """
+        data_offset = 0
+        data = {}
+        devices = self.__slaves_meta[slave_addr].module_obj.get_devices()
+
+        while data_offset < resp_length:
+            device_id = response[data_offset]
+
+            if devices[device_id].data_format is None:
+                raise NBusErrorAPI(NBusErrorAPIType.FORMAT_NOT_LOADED)
+
+            values, offset = NbusCommonParser.data_from_response(devices[device_id].data_format, response[data_offset:])
+            data_tag = str(slave_addr) + "." + str(device_id)
+
+            for i in range(len(values)):
+                data[data_tag + "." + str(i + 1)] = values[i]
+
+            data_offset += offset + NBUS_SA_SIZE
+
+        return data

+ 1 - 1
nbus_api/nbus_common_parser.py

@@ -62,7 +62,7 @@ class NbusCommonParser:
         return params
 
     @staticmethod
-    def data_from_response(data_format: NBusDataFormat, response: list[int]) -> Tuple[list[NBusDataValue], int]:
+    def data_from_response(data_format: NBusDataFormat, response: bytearray) -> Tuple[list[NBusDataValue], int]:
         """
         Parse data from response.
         :param data_format: format of data

+ 29 - 53
nbus_api/nbus_module_slave.py

@@ -1,14 +1,14 @@
 import struct
 from nbus_api.nbus_sensor import NBusSensor
 from nbus_api.nbus_common_parser import NbusCommonParser
-from nbus_hal.nbus_serial.serial_port import *
+from nbus_hal.nbus_generic_port import *
 from nbus_types.nbus_address_type import NBusModuleAddress
 from nbus_types.nbus_data_fomat import NBusDataValue, NBusDataFormat
 from nbus_types.nbus_exceptions.nbus_api_exception import NBusErrorAPI, NBusErrorAPIType
 from nbus_types.nbus_parameter_type import NBusParameterID, NBusParameterValue
 from nbus_types.nbus_status_type import NBusStatusType
 from nbus_types.nbus_sensor_count_type import NBusSensorCount
-from nbus_types.nbus_info_type import NBusInfo
+from nbus_types.nbus_info_type import NBusModuleInfo
 from nbus_types.nbus_sensor_type import NBusSensorType
 
 
@@ -18,56 +18,46 @@ class NBusSlaveModule:
     Class representing nBus slave module.
     """
 
-    def __init__(self, serial_port: NBusSerialPort, module_address: NBusModuleAddress):
+    def __init__(self, port: NBusPort, module_address: NBusModuleAddress):
         """
         Constructor.
 
-        :param serial_port: serial port
+        :param port: serial port
         :param module_address: address of module
+        :param device_cnt: number of devices
         """
-        self.__port = serial_port
+        self.__port = port
         self.__module_addr = module_address
         self.__params = {}
         self.__devices = {}
-        self._map_param_get = lambda t, v: v    # dummy implementation
-        self._map_param_set = lambda t, v: v    # dummy implementation
 
     """
     ================================================================================================================
                                                 Module General Methods
     ================================================================================================================
     """
-
-    def set_module_parameter_mappers(self, map_param_get_cb: Callable[[NBusParameterID, NBusParameterValue], int],
-                                     map_param_set_cb: Callable[[NBusParameterID, int], NBusParameterValue]) -> None:
+    def init(self, load_format: bool) -> None:
         """
-        Set parameter mappers for module.
+        Initialize the module from hardware.
 
-        :param map_param_get_cb: callback for map param get
-        :param map_param_set_cb: callback for map param set
+        :param load_format: flag to fetch data format
         """
-        self._map_param_get = map_param_get_cb
-        self._map_param_set = map_param_set_cb
+        sensors = self.cmd_get_sensor_type()
 
-    def add_sensor(self, sensor: NBusSensor) -> None:
-        """
-        Add sensor to container.
+        for sen_address, sen_type in sensors.items():
+            self.__devices[sen_address] = NBusSensor(self.__port, self.__module_addr, sen_address)
+            self.__devices[sen_address].type = sen_type
 
-        :param sensor: nbus sensor
-        """
-        sensor.set_parent_module_address(self.__module_addr)
-        sensor.set_device_port(self.__port)
-        self.__devices[sensor.address] = sensor
+        if load_format:
+            self.cmd_get_format()
 
-    def get_sensor(self, sensor_address: NBusSensorAddress) -> NBusSensor:
+    def get_devices(self) -> dict[NBusSensorAddress, NBusSensor]:
         """
-        Get module sensor.
+        Get module devices.
 
-        :param sensor_address: address of sensor
-
-        :return: sensor
+        :return: dictionary of connected devices
         """
-        return self.__devices[sensor_address]
+        return self.__devices
 
     """
     ================================================================================================================
@@ -96,8 +86,7 @@ class NBusSlaveModule:
                                                          bytearray([parameter.value]))
 
         # parse parameter
-        param_id, param_val_raw = NbusCommonParser.parameters_from_response(resp_len, response)[0]
-        param_val = self._map_param_get(param_id, param_val_raw)
+        param_id, param_val = NbusCommonParser.parameters_from_response(resp_len, response)[0]
 
         # store parameter value
         self.__params[param_id] = param_val
@@ -115,11 +104,9 @@ class NBusSlaveModule:
         resp_len, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_GET_PARAM, bytearray([]))
 
         # parse parameters
-        params_raw = NbusCommonParser.parameters_from_response(resp_len, response)
-
-        for param_id, param_val_raw in params_raw:
-            param_val = self._map_param_get(param_id, param_val_raw)
+        params = NbusCommonParser.parameters_from_response(resp_len, response)
 
+        for param_id, param_val in params:
             # store parameters
             self.__params[param_id] = param_val
 
@@ -154,17 +141,15 @@ class NBusSlaveModule:
             # handle errors
             if self.__devices[device_id].data_format is None:  # check for format and params
                 raise NBusErrorAPI(NBusErrorAPIType.FORMAT_NOT_LOADED)
-            if not self.__devices[device_id].data_parameters_loaded():
-                raise NBusErrorAPI(NBusErrorAPIType.PARAMS_NOT_LOADED)
 
             values, offset = NbusCommonParser.data_from_response(self.__devices[device_id].data_format,
                                                                  response[begin_idx:])
-            data[device_id] = self.__devices[device_id].map_data_get(values)
+            data[device_id] = values
             begin_idx += offset + 1
 
         return data
 
-    def cmd_get_info(self) -> NBusInfo:
+    def cmd_get_info(self) -> NBusModuleInfo:
         """
         Get module info.
 
@@ -182,8 +167,8 @@ class NBusSlaveModule:
         ro_count = int(response[30])
         rw_count = int(response[31])
 
-        return NBusInfo(module_name=name, module_type=typ, uuid=uuid, hw=hw, fw=fw, memory_id=mem_id,
-                        read_only_sensors=ro_count, read_write_sensors=rw_count)
+        return NBusModuleInfo(module_name=name, module_type=typ, uuid=uuid, hw=hw, fw=fw, memory_id=mem_id,
+                              read_only_sensors=ro_count, read_write_sensors=rw_count)
 
     def cmd_get_format(self) -> dict[NBusSensorAddress, NBusDataFormat]:
         """
@@ -263,7 +248,7 @@ class NBusSlaveModule:
 
         # create request packet
         param_id_raw = struct.pack("B", param.value)
-        param_val_raw = struct.pack("<I", self._map_param_set(param, value))
+        param_val_raw = struct.pack("<I",value)
         param_bytes = bytearray(param_id_raw) + bytearray(param_val_raw)
 
         # proceed request
@@ -284,16 +269,12 @@ class NBusSlaveModule:
         :return: dict od statuses
         """
 
-        # scale parameters
-        for p_id in params.keys():
-            params[p_id] = self._map_param_set(p_id, params[p_id])
-
         # create request packet
         param_bytes = NbusCommonParser.parameters_to_request(params)
 
         # send request
         resp_length, *response = self.__port.request_module(self.__module_addr, NBusCommand.CMD_SET_PARAM,
-                                                            bytearray(param_bytes))
+                                                            bytearray(param_bytes), long_answer=1.0)
         # parse statuses
         statuses = {}
 
@@ -335,10 +316,8 @@ class NBusSlaveModule:
             # handle errors
             if self.__devices[addr].data_format is None:  # check for format and params
                 raise NBusErrorAPI(NBusErrorAPIType.FORMAT_NOT_LOADED)
-            if not self.__devices[addr].data_parameters_loaded():
-                raise NBusErrorAPI(NBusErrorAPIType.PARAMS_NOT_LOADED)
 
-            raw_data = self.__devices[addr].map_data_set(data[addr])
+            raw_data = data[addr]
             request.append(addr)
             request.extend(NbusCommonParser.data_to_request(self.__devices[addr].data_format, raw_data))
 
@@ -353,6 +332,3 @@ class NBusSlaveModule:
             statuses[NBusSensorAddress(response[i])] = NBusStatusType(response[i + 1])
 
         return statuses
-
-
-

+ 17 - 105
nbus_api/nbus_sensor.py

@@ -1,5 +1,4 @@
 import struct
-from abc import abstractmethod, ABCMeta
 from nbus_hal.nbus_generic_port import NBusPort
 from nbus_types.nbus_command_type import NBusCommand
 from nbus_types.nbus_data_fomat import *
@@ -12,20 +11,23 @@ from nbus_types.nbus_status_type import NBusStatusType
 
 
 @beartype
-class NBusSensor(metaclass=ABCMeta):
+class NBusSensor:
     """
     Class representing nBus sensor type.
     """
 
-    def __init__(self, address: NBusSensorAddress):
+    def __init__(self, port: NBusPort, module_address: NBusModuleAddress, address: NBusSensorAddress):
         """
         Constructor.
 
+        :param port: NBusPort object
+        :param module_address: address of NBusModule
         :param address: device address
         """
         self.__address = address
-        self.__module_address = None
-        self.__port = None
+        self.__type = None
+        self.__module_address = module_address
+        self.__port = port
         self.__data_format = None
         self.__params = {}
 
@@ -80,85 +82,6 @@ class NBusSensor(metaclass=ABCMeta):
         """
         self.__params = values
 
-    """
-    ================================================================================================================
-                                                    Module-only Methods
-    ================================================================================================================
-    """
-
-    def set_parent_module_address(self, address: NBusModuleAddress) -> None:
-        """
-        Set address of parent module.
-
-        :param address: module address
-        """
-        self.__module_address = address
-
-    def set_device_port(self, port: NBusPort) -> None:
-        """
-        Set device communication port.
-        :param port: communicaiton port
-        """
-        self.__port = port
-
-    """
-    ================================================================================================================
-                                                    Abstract Methods
-    ================================================================================================================
-    """
-
-    @abstractmethod
-    def data_parameters_loaded(self) -> bool:
-        """
-        Verify that all necessary parameters are loaded
-        before performing data get/set conversion.
-
-        :return: true if ready for conversion, otherwise False
-        """
-        pass
-
-    @abstractmethod
-    def map_parameter_get(self, param_id: NBusParameterID, param_value: int) -> NBusParameterValue:
-        """
-        Convert a parameter from cmd_get_param() to its engineering range.
-
-        :param param_id: the ID of the parameter
-        :param param_value: the value of the parameter in binary format
-        :return: the converted parameter value in engineering units
-        """
-        pass
-
-    @abstractmethod
-    def map_parameter_set(self, param_id: NBusParameterID, param_value: NBusParameterValue) -> int:
-        """
-        Convert a parameter to its binary range for cmd_set_data().
-
-        :param param_id: the ID of the parameter
-        :param param_value: the value of the parameter in engineering units
-        :return: the converted parameter value in binary format
-        """
-        pass
-
-    @abstractmethod
-    def map_data_get(self, values: list[int]) -> list[NBusDataValue]:
-        """
-        Convert data from cmd_get_data() to its engineering range.
-
-        :param values: a list of values in binary format to be converted
-        :return: a list of converted values in engineering units
-        """
-        pass
-
-    @abstractmethod
-    def map_data_set(self, values: list[NBusDataValue]) -> list[int]:
-        """
-        Convert data to its binary range for cmd_set_data().
-
-        :param values: a list of values in engineering range to be converted
-        :return: a list of converted values in binary format
-        """
-        pass
-
     """
     ================================================================================================================
                                                     Device Get Commands
@@ -177,8 +100,7 @@ class NBusSensor(metaclass=ABCMeta):
                                                          NBusCommand.CMD_GET_PARAM, bytearray([parameter.value]))
 
         # parse parameter
-        param_id, param_val_raw = NbusCommonParser.parameters_from_response(resp_len, response)[0]
-        param_val = self.map_parameter_get(param_id, param_val_raw)
+        param_id, param_val = NbusCommonParser.parameters_from_response(resp_len, response)[0]
 
         # store parameter value
         self.__params[param_id] = param_val
@@ -197,11 +119,9 @@ class NBusSensor(metaclass=ABCMeta):
                                                          NBusCommand.CMD_GET_PARAM, bytearray([]))
 
         # parse parameters
-        params_raw = NbusCommonParser.parameters_from_response(resp_len, response)
-
-        for param_id, param_val_raw in params_raw:
-            param_val = self.map_parameter_get(param_id, param_val_raw)
+        params = NbusCommonParser.parameters_from_response(resp_len, response)
 
+        for param_id, param_val in params:
             # store parameters
             self.__params[param_id] = param_val
 
@@ -218,13 +138,11 @@ class NBusSensor(metaclass=ABCMeta):
 
         if self.__data_format is None:  # check for format and params
             raise NBusErrorAPI(NBusErrorAPIType.FORMAT_NOT_LOADED)
-        if not self.data_parameters_loaded():
-            raise NBusErrorAPI(NBusErrorAPIType.PARAMS_NOT_LOADED)
 
         _, *resp = self.__port.request_sensor(self.__module_address, self.__address, NBusCommand.CMD_GET_DATA,
                                               bytearray([]))
         values, _ = NbusCommonParser.data_from_response(self.data_format, resp)
-        return self.map_data_get(values)
+        return values
 
     def cmd_get_sensor_type(self) -> NBusSensorType:
         """
@@ -234,7 +152,9 @@ class NBusSensor(metaclass=ABCMeta):
         """
         _, *response = self.__port.request_sensor(self.__module_address, self.__address,
                                                   NBusCommand.CMD_GET_SENSOR_TYPE, bytearray([]))
-        return NBusSensorType(response[0])
+
+        self.__type = response[0]
+        return NBusSensorType(self.__type)
 
     def cmd_get_format(self):
         """
@@ -275,7 +195,7 @@ class NBusSensor(metaclass=ABCMeta):
 
         # create request packet
         param_id_raw = struct.pack("B", param.value)
-        param_val_raw = struct.pack("<I", self.map_parameter_set(param, value))
+        param_val_raw = struct.pack("<I", value)
         param_bytes = bytearray(param_id_raw) + bytearray(param_val_raw)
 
         # proceed request
@@ -297,10 +217,6 @@ class NBusSensor(metaclass=ABCMeta):
         :return: dict od statuses
         """
 
-        # scale parameters
-        for p_id in params.keys():
-            params[p_id] = self.map_parameter_set(p_id, params[p_id])
-
         # create request packet
         param_bytes = NbusCommonParser.parameters_to_request(params)
 
@@ -341,16 +257,12 @@ class NBusSensor(metaclass=ABCMeta):
 
         if self.__data_format is None:  # check for format and params
             raise NBusErrorAPI(NBusErrorAPIType.FORMAT_NOT_LOADED)
-        if not self.data_parameters_loaded():
-            raise NBusErrorAPI(NBusErrorAPIType.PARAMS_NOT_LOADED)
 
         # create request packet
-        request = []
+        request = [self.__address]
 
         # transform data
-        raw_data = self.map_data_set(data)
-        request.append(self.__address)
-        request.extend(NbusCommonParser.data_to_request(self.data_format, raw_data))
+        request.extend(NbusCommonParser.data_to_request(self.data_format, data))
 
         # send request
         resp_length, *response = self.__port.request_sensor(self.__module_address, self.__address,

BIN
nbus_hal/__pycache__/crc8.cpython-313.pyc


BIN
nbus_hal/__pycache__/nbus_generic_port.cpython-313.pyc


+ 38 - 1
nbus_hal/nbus_generic_port.py

@@ -1,9 +1,10 @@
 from abc import ABCMeta, abstractmethod
 from typing import Annotated
+
 from beartype import beartype
 from beartype.vale import Is
+from nbus_types.nbus_command_type import *
 from nbus_types.nbus_address_type import NBusModuleAddress, NBusSensorAddress
-from nbus_types.nbus_command_type import NBusCommand
 
 
 """
@@ -32,6 +33,22 @@ class NBusPort(metaclass=ABCMeta):
         """
         pass
 
+    @abstractmethod
+    def flush(self) -> None:
+        """
+        Flush port with periodic check.
+        """
+        pass
+
+    @abstractmethod
+    def try_read(self) -> bytes:
+        """
+       Try reading from port.
+
+       :return: bytes
+       """
+        pass
+
     @abstractmethod
     def is_connected(self) -> bool:
         """
@@ -40,6 +57,26 @@ class NBusPort(metaclass=ABCMeta):
         """
         pass
 
+    @abstractmethod
+    def request_bridge(self, command: NBusCommand, data: bytearray, long_answer: NBusDelay = 0.0):
+        """
+        Make bridge request.
+        :param command: command id
+        :param data: command data to send
+             :param long_answer: delay in s for longer answer
+        """
+        pass
+
+    @abstractmethod
+    def send_bridge(self, command: NBusCommand, data: bytearray):
+        """
+        Make bridge request without waiting for response.
+        :param command: command id
+        :param data: command data to send
+             :param long_answer: delay in s for longer answer
+        """
+        pass
+
     @abstractmethod
     def request_broadcast(self, command: NBusCommand, data: bytearray) -> None:
         """

BIN
nbus_hal/nbus_serial/__pycache__/serial_config.cpython-313.pyc


BIN
nbus_hal/nbus_serial/__pycache__/serial_port.cpython-313.pyc


+ 2 - 0
nbus_hal/nbus_serial/serial_config.py

@@ -39,6 +39,7 @@ class NBusSerialConfig:
     :ivar baud: The baud rate for the serial communication.
     :ivar parity: The parity bit setting for the serial communication.
     :ivar timeout: The timeout value for the serial communication.
+    :ivar flush_delay: The delay for inter-flushing the serial communication.
     :ivar request_attempts: The number of attempts for a request.
     :ivar enable_log: Flag to enable or disable logging.
     """
@@ -46,5 +47,6 @@ class NBusSerialConfig:
     baud: NBusBaudrate
     parity: NBusParity
     timeout: Annotated[float, Is[lambda value: value > 0]]
+    flush_delay: Annotated[float, Is[lambda value: value > 0]]
     request_attempts: Annotated[int, Is[lambda value: value > 0]]
     enable_log: bool

+ 58 - 13
nbus_hal/nbus_serial/serial_port.py

@@ -1,7 +1,6 @@
 import time
 import serial
-from typing import Any, Callable
-
+from typing import Any, Callable, Optional
 from beartype import beartype
 
 from nbus_types.nbus_command_type import *
@@ -11,6 +10,7 @@ from nbus_hal.nbus_serial.serial_config import NBusSerialConfig
 from nbus_hal.nbus_generic_port import NBusPort, NBusDelay
 from nbus_types.nbus_address_type import NBusModuleAddress, NBusSensorAddress
 from nbus_hal.crc8 import crc8
+from nbus_types.nbus_defines import *
 
 
 def default_logger(*message: Any) -> None:
@@ -38,6 +38,7 @@ class NBusSerialPort(NBusPort):
         Constructor.
         :param config: configuration
         """
+        self._flush_delay = config.flush_delay
         self._port = serial.Serial(timeout=config.timeout)
         self._port.port = config.port_name
         self._port.parity = config.parity.value
@@ -51,13 +52,13 @@ class NBusSerialPort(NBusPort):
                                                 API Methods
     ================================================================================================================
     """
-
     def change_configuration(self, config: NBusSerialConfig):
         """
         Change port configuration.
         :param config: configuration
         """
         self._port.timeout = config.timeout
+        self._flush_delay = config.flush_delay
         self._port.port = config.port_name
         self._port.parity = config.parity.value
         self._port.baudrate = config.baud.value
@@ -70,15 +71,38 @@ class NBusSerialPort(NBusPort):
         """
         self._port.open()
         self._port.flush()
-        self._log('i', 0, 'Open communication port')
+        self._log("INFO", "  0", "\tOpen communication port")
 
     def close(self) -> None:
         """
         Close port.
         """
-        self._log('i', 0, 'Close communication port')
+        self._log("INFO", "  0", "\tClose communication port")
         self._port.close()
 
+    def flush(self) -> None:
+        """
+        Flush port with periodic check.
+        """
+        while self._port.in_waiting:
+            self._log("INFO", "  0", "\tFlush communication port")
+            self._port.reset_input_buffer()
+            self._port.reset_output_buffer()
+            time.sleep(self._flush_delay)
+
+    def try_read(self) -> bytes:
+        """
+        Try reading from port.
+
+        :return: bytes
+        """
+        if self._port.in_waiting > 0:
+            data = self._port.read()
+            self._log("DATA", f"{len(data):3}", "\tARS>", list(data))
+            return data
+        else:
+            return bytes()
+
     def is_connected(self) -> bool:
         """
         Return connection status.
@@ -93,14 +117,34 @@ class NBusSerialPort(NBusPort):
         """
         self._logger_cb = callback
 
+    def request_bridge(self, command: NBusCommand, data: bytearray, long_answer: NBusDelay = 0.0):
+        """
+        Make bridge request.
+        :param command: command id
+        :param data: command data to send
+             :param long_answer: delay in s for longer answer
+        """
+        return self._request_response(NBUS_BROADCAST_ADDR, NBUS_BRIDGE_ADDR, command.value, data, long_answer)
+
+    def send_bridge(self, command: NBusCommand, data: bytearray):
+        """
+        Make bridge request without waiting for response.
+        :param command: command id
+        :param data: command data to send
+             :param long_answer: delay in s for longer answer
+        """
+        request = self._create_packet(bytearray([0, NBUS_BROADCAST_ADDR, NBUS_BRIDGE_ADDR, command.value]), data)
+        self._log("DATA", f"{request[0]:3}", "\tARQ>", list(request[1:]))
+        self._port.write(request)  # send message
+
     def request_broadcast(self, command: NBusCommand, data: bytearray) -> None:
         """
         Make broadcast request to nbus network.
         :param command: command id
         :param data: command data to send
         """
-        request = self._create_packet(bytearray([0, 0, 0, command.value]), data)
-        self._log("\tBRQ>", list(request))
+        request = self._create_packet(bytearray([0, NBUS_BROADCAST_ADDR, NBUS_BROADCAST_ADDR, command.value]), data)
+        self._log("DATA", f"{request[0]:3}", "\tARQ>", list(request[1:]))
         self._port.write(request)  # send message
 
     def request_module(self, module_addr: NBusModuleAddress, command: NBusCommand, data: bytearray,
@@ -146,7 +190,7 @@ class NBusSerialPort(NBusPort):
         :return: response length | response
         """
         request = self._create_packet(bytearray([0, module, sensor, command]), data)  # create request
-        self._log('d', sensor, "\tRQ>", list(request))  # log request
+        self._log("DATA", f"{request[0]:3}", "\tRQ>", list(request[1:]))  # log request
 
         counter = 0  # err. trials
 
@@ -204,16 +248,17 @@ class NBusSerialPort(NBusPort):
             raise NBusErrorNetwork(NBusErrorNetworkType.MESSAGE_NOT_COMPLETE)
 
         # check for crc
-        if response[-1] != crc8(response[:-1]):
+        if response[NBUS_CRC_ADDR] != crc8(response[:NBUS_CRC_ADDR]):
             raise NBusErrorNetwork(NBusErrorNetworkType.DAMAGED_MESSAGE)
 
-        self._log('d', 0, "\tRS>", [response_l] + list(response))   # log response
+        self._log("DATA", f"{response_l:3}", "\tRS>", list(response))   # log response
 
         # check for node error
-        if response[2] & NBUS_ERR_BIT:
-            raise NBusErrorNode(NBusErrorNodeType(response[3]))
+        if response[NBUS_FC_ADDR] & NBUS_ERR_BIT:
+            raise NBusErrorNode(NBusErrorNodeType(response[NBUS_DATA0_ADDR]))
 
-        return bytearray([response_l - 4]) + bytearray(response[3:-1])   # return payload length + payload
+        # return payload length + payload
+        return bytearray([response_l - NBUS_RX_META_SIZE]) + bytearray(response[NBUS_DATA0_ADDR:NBUS_CRC_ADDR])
 
     def _log(self, *message: Any) -> None:
         """

+ 0 - 24
nbus_sensor_drivers/generic_sensor_driver.py

@@ -1,24 +0,0 @@
-from nbus_api.nbus_sensor import NBusSensor
-from nbus_types.nbus_data_fomat import NBusDataValue
-from nbus_types.nbus_parameter_type import NBusParameterID, NBusParameterValue
-
-
-class NBusGenericSensor(NBusSensor):
-    """
-    Class for generic NBus sensor (no data transformation)
-    """
-
-    def data_parameters_loaded(self) -> bool:
-        return True
-
-    def map_parameter_get(self, param_id: NBusParameterID, param_value: int) -> NBusParameterValue:
-        return param_value
-
-    def map_parameter_set(self, param_id: NBusParameterID, param_value: NBusParameterValue) -> int:
-        return param_value
-
-    def map_data_get(self, values: list[int]) -> list[NBusDataValue]:
-        return values
-
-    def map_data_set(self, values: list[NBusDataValue]) -> list[int]:
-        return values

BIN
nbus_types/__pycache__/nbus_address_type.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_command_type.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_data_fomat.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_defines.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_info_type.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_parameter_type.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_sensor_count_type.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_sensor_type.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_slave_meta_type.cpython-313.pyc


BIN
nbus_types/__pycache__/nbus_status_type.cpython-313.pyc


+ 1 - 0
nbus_types/nbus_command_type.py

@@ -15,6 +15,7 @@ class NBusCommand(Enum):
     CMD_GET_SENSOR_TYPE = 0x0D
     CMD_GET_INFO = 0x0E
     CMD_GET_FORMAT = 0x0F
+    CMD_GET_SLAVES = 0x10
     
     # set
     CMD_SET_FIND = 0x20

+ 29 - 0
nbus_types/nbus_defines.py

@@ -0,0 +1,29 @@
+"""
+Constants defining the NBUS protocol layout and addressing rules.
+These values are treated as immutable configuration parameters.
+"""
+
+from typing import Final
+
+# --- General sizes ---
+NBUS_RX_META_SIZE: Final = 4
+NBUS_FMT_SIZE: Final = 4
+NBUS_TS_SIZE: Final = 4
+NBUS_CRC_SIZE: Final = 1
+NBUS_MA_SIZE: Final = 1
+NBUS_SA_SIZE: Final = 1
+
+# --- Special addresses ---
+NBUS_BROADCAST_ADDR: Final = 0x00
+NBUS_BRIDGE_ADDR: Final = 0xFF
+
+# --- Packet Index Positions ---
+NBUS_MA_ADDR: Final = 0
+NBUS_SA_ADDR: Final = 1
+NBUS_FC_ADDR: Final = 2
+NBUS_DATA0_ADDR: Final = 3
+NBUS_CRC_ADDR: Final = -1
+
+# --- Bridge header ---
+NBUS_BRIDGE_DATA_HDR: Final = bytearray([0x00] + [0xFF] * 8 + [0x00])
+NBUS_BRIDGE_DATA_HDR_SIZE: Final = len(NBUS_BRIDGE_DATA_HDR)

BIN
nbus_types/nbus_exceptions/__pycache__/nbus_api_exception.cpython-313.pyc


BIN
nbus_types/nbus_exceptions/__pycache__/nbus_network_exception.cpython-313.pyc


BIN
nbus_types/nbus_exceptions/__pycache__/nbus_node_exception.cpython-313.pyc


+ 1 - 1
nbus_types/nbus_exceptions/nbus_api_exception.py

@@ -13,4 +13,4 @@ class NBusErrorAPIType(Enum):
     Enum class for NBusErrorAPI identification.
     """
     FORMAT_NOT_LOADED = 0x201
-    PARAMS_NOT_LOADED = 0x202
+    SLAVES_NOT_LOADED = 0x202

+ 18 - 5
nbus_types/nbus_info_type.py

@@ -7,16 +7,16 @@ import numpy as np
 
 @beartype
 @dataclass(frozen=True)
-class NBusInfo:
+class NBusModuleInfo:
     """
-    Class for data format.
+    Class for module info.
 
     :ivar module_name: name of module
     :ivar module_type: type of module
     :ivar uuid: unique ID, generated by STM
-    :ivar hw: firmware version. MAJOR.MINOR
-    :ivar fw: memory ID number
-    :ivar memory_id: sensor count
+    :ivar hw: hardware version. MAJOR.MINOR
+    :ivar fw: firmware version. MAJOR.MINOR
+    :ivar memory_id: memory ID number
     :ivar read_only_sensors: count of read-only sensors
     :ivar read_write_sensors: count of read-write sensors
     """
@@ -28,3 +28,16 @@ class NBusInfo:
     memory_id: Annotated[int, Is[lambda value: 0 <= value <= np.iinfo(np.uint64).max]]
     read_only_sensors: Annotated[int, Is[lambda value: 0 <= value <= 31]]
     read_write_sensors: Annotated[int, Is[lambda value: 0 <= value <= 31]]
+
+@beartype
+@dataclass(frozen=True)
+class NBusBridgeInfo:
+    """
+    Class for bridge info.
+    :ivar fw: firmware version. MAJOR.MINOR
+    :ivar hw: hardware version. MAJOR.MINOR
+    :ivar hw_family: hardware family (ESP, STM, ...)
+    """
+    fw: Annotated[str, Is[lambda value: len(value) == 3]]
+    hw_family: Annotated[str, Is[lambda value: len(value) == 3]]
+    hw: Annotated[str, Is[lambda value: len(value) == 3]]

+ 2 - 0
nbus_types/nbus_parameter_type.py

@@ -20,3 +20,5 @@ class NBusParameterID(Enum):
     PARAM_RANGE = 5
     PARAM_RANGE0 = 6
     PARAM_FILTER = 7
+    PARAM_ENABLE = 8
+    PARAM_MODE = 9

+ 8 - 7
nbus_types/nbus_sensor_type.py

@@ -9,10 +9,11 @@ class NBusSensorType(Enum):
     TYPE_ACCELEROMETER = 0
     TYPE_GYROSCOPE = 1
     TYPE_MAGNETOMETER = 2
-    TYPE_THERMOMETER = 3
-    TYPE_HYGROMETER = 4
-    TYPE_PRESSURE_GAUGE = 5
-    TYPE_HEART_RATE_MONITOR = 6
-    TYPE_LENGTH_GAUGE = 7
-    TYPE_LED_CONTROLLER = 8
-    TYPE_MOTOR_CONTROLLER = 9
+    TYPE_EULER_ANGLES_GAUGE = 3
+    TYPE_THERMOMETER = 4
+    TYPE_HYGROMETER = 5
+    TYPE_PRESSURE_GAUGE = 6
+    TYPE_HEART_RATE_MONITOR = 7
+    TYPE_LENGTH_GAUGE = 8
+    TYPE_LED_CONTROLLER = 9
+    TYPE_MOTOR_CONTROLLER = 10

+ 21 - 0
nbus_types/nbus_slave_meta_type.py

@@ -0,0 +1,21 @@
+from typing import Annotated
+from beartype import beartype
+from beartype.vale import Is
+from dataclasses import dataclass
+from nbus_api.nbus_module_slave import NBusSlaveModule
+from nbus_types.nbus_sensor_count_type import NBusSensorCount
+
+
+@beartype
+@dataclass
+class NBusSlaveMeta:
+    """
+    Helper Class for slave module meta information used in bridge.
+
+    :ivar module_obj: module object
+    :ivar device_count: number of module devices
+    :ivar packet_size: size of default data packet
+    """
+    module_obj: NBusSlaveModule
+    device_count: NBusSensorCount
+    packet_size: Annotated[int, Is[lambda value: value >= 0]]

+ 0 - 89
test.py

@@ -1,89 +0,0 @@
-from nbus_hal.nbus_serial.serial_port import *
-from nbus_api.nbus_module_slave import NBusSlaveModule
-from nbus_hal.nbus_serial.serial_config import *
-from nbus_sensor_drivers.generic_sensor_driver import NBusGenericSensor
-from nbus_types.nbus_parameter_type import NBusParameterID
-
-# example config
-config = {
-    "port_name": "COM6",
-    "baud": NBusBaudrate.SPEED_921600,
-    "parity": NBusParity.NONE,
-    "timeout": 0.4,
-    "request_attempts": 1,
-    "enable_log": False
-}
-
-if __name__ == "__main__":
-    # create port
-    port = NBusSerialPort(NBusSerialConfig(**config))
-
-    # create module
-    module = NBusSlaveModule(port, 5)
-
-    # assemble module
-    module.add_sensor(NBusGenericSensor(1))
-    module.add_sensor(NBusGenericSensor(2))
-    module.add_sensor(NBusGenericSensor(3))
-    module.add_sensor(NBusGenericSensor(4))
-    module.add_sensor(NBusGenericSensor(5))
-    module.add_sensor(NBusGenericSensor(129))
-    module.add_sensor(NBusGenericSensor(130))
-
-    # get sensors
-    accelerometer = module.get_sensor(1)
-    led = module.get_sensor(129)
-
-    # test module get
-    print("<<TEST MODULE GET>>")
-    print("CMD GET ECHO: ", module.cmd_get_echo(bytearray("Hello world!", "ascii")))
-    print("CMD GET PARAM SAMPLERATE: ", module.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
-    print("CMD GET ALL PARAMS: ", module.cmd_get_all_params())
-    print("CMD GET SENSOR COUNT: ", module.cmd_get_sensor_cnt())
-    print("CMD GET SENSOR TYPE: ", module.cmd_get_sensor_type())
-    print("CMD GET INFO: ", module.cmd_get_info())
-    print("CMD GET SENSOR FORMAT: ", module.cmd_get_format())
-    print("CMD GET SENSOR DATA: ", module.cmd_get_data())
-
-    # test module set
-    print("\n<<TEST MODULE SET>>")
-    print("CMD SET STOP: ", module.cmd_set_module_stop())
-    print("CMD SET START: ", module.cmd_set_module_start())
-
-    print("CMD SET PARAM SAMPLERATE: ", module.cmd_set_param(NBusParameterID.PARAM_SAMPLERATE, 12345))
-    print("CMD GET PARAM SAMPLERATE: ", module.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
-
-    params = {NBusParameterID.PARAM_RANGE: 12, NBusParameterID.PARAM_RANGE0: 4234}
-    print("CMD SET MULTI PARAMS: ", module.cmd_set_multi_params(params))
-    print("CMD GET ALL PARAMS: ", module.cmd_get_all_params())
-
-    print("CMD SET CALIBRATE: ", module.cmd_set_calibrate())
-
-    data = {129: [1], 130: [10, -32]}
-    print("CMD SET DATA: ", module.cmd_set_data(data))
-    print("CMD GET SENSOR DATA: ", module.cmd_get_data())
-
-    # test sensor get
-    print("\n<<TEST SENSOR GET>>")
-    print("CMD GET PARAM SAMPLERATE: ", accelerometer.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
-    print("CMD GET ALL PARAMS: ", accelerometer.cmd_get_all_params())
-    print("CMD GET SENSOR TYPE: ", accelerometer.cmd_get_sensor_type())
-    print("CMD GET SENSOR FORMAT: ", accelerometer.cmd_get_format())
-    print("CMD GET SENSOR DATA: ", accelerometer.cmd_get_data())
-
-    # test sensor set
-    print("\n<<TEST SENSOR SET>>")
-    print("CMD SET FIND: ", led.cmd_set_find(True))
-    print("CMD SET PARAM SAMPLERATE: ", led.cmd_set_param(NBusParameterID.PARAM_SAMPLERATE, 23456))
-    print("CMD GET PARAM SAMPLERATE: ", led.cmd_get_param(NBusParameterID.PARAM_SAMPLERATE))
-
-    params = {NBusParameterID.PARAM_RANGE: 12, NBusParameterID.PARAM_RANGE0: 4234}
-    print("CMD SET MULTI PARAMS: ", led.cmd_set_multi_params(params))
-    print("CMD GET ALL PARAMS: ", led.cmd_get_all_params())
-
-    print("CMD SET CALIBRATE: ", led.cmd_set_calibrate())
-
-    print("CMD SET DATA: ", led.cmd_set_data([1]))
-    print("CMD GET SENSOR DATA: ", led.cmd_get_data())
-
-