pyfastnet 2.0.18__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {pyfastnet-2.0.18/pyfastnet.egg-info → pyfastnet-3.0.0}/PKG-INFO +70 -58
  2. pyfastnet-3.0.0/README.md +181 -0
  3. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/__init__.py +4 -2
  4. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/decode_fastnet.py +4 -1
  5. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/frame_buffer.py +33 -12
  6. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/logger.py +6 -1
  7. pyfastnet-3.0.0/fastnet_decoder/signalk_map.py +293 -0
  8. {pyfastnet-2.0.18 → pyfastnet-3.0.0/pyfastnet.egg-info}/PKG-INFO +70 -58
  9. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyfastnet.egg-info/SOURCES.txt +3 -0
  10. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_backlight.py +6 -2
  11. pyfastnet-3.0.0/tests/test_golden.py +33 -0
  12. pyfastnet-3.0.0/tests/test_signalk_projection.py +171 -0
  13. pyfastnet-2.0.18/README.md +0 -169
  14. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/LICENSE +0 -0
  15. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/mappings.py +0 -0
  16. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/utils.py +0 -0
  17. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyfastnet.egg-info/dependency_links.txt +0 -0
  18. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyfastnet.egg-info/requires.txt +0 -0
  19. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyfastnet.egg-info/top_level.txt +0 -0
  20. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyproject.toml +0 -0
  21. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/setup.cfg +0 -0
  22. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_autopilot.py +0 -0
  23. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_awa.py +0 -0
  24. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_aws.py +0 -0
  25. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_battery.py +0 -0
  26. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_boatspeed.py +0 -0
  27. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_depth_frame.py +0 -0
  28. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_format07_msb_regression.py +0 -0
  29. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_format08_layout.py +0 -0
  30. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_heading.py +0 -0
  31. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_heel_trim.py +0 -0
  32. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_latlon_frame.py +0 -0
  33. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_sea_temp.py +0 -0
  34. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_sog.py +0 -0
  35. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_tidal.py +0 -0
  36. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_timer.py +0 -0
  37. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_twa.py +0 -0
  38. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_twd.py +0 -0
  39. {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_tws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfastnet
3
- Version: 2.0.18
3
+ Version: 3.0.0
4
4
  Summary: A Python library for decoding FastNet protocol data streams.
5
5
  Author-email: Alex Salmon <alex@ivila.net>
6
6
  License: MIT License
@@ -54,8 +54,13 @@ Dynamic: license-file
54
54
 
55
55
  A Python library for decoding the **FastNet** protocol used by B&G Hydra / H2000
56
56
  instruments. Feed it raw bytes from the Fastnet bus and it handles
57
- synchronisation, checksum validation, and decoding — returning structured
58
- instrument data ready for further processing.
57
+ synchronisation, checksum validation, and decoding — returning instrument data as
58
+ **Signal K paths in SI units**, ready for further processing.
59
+
60
+ > **v3.0 changes the output format.** Decoded frames are now `{signalk_path:
61
+ > SI_value}` (e.g. `navigation.speedThroughWater = 3.6`) instead of the v2
62
+ > name-keyed `{value, display_text, layout}` dicts. See **Output format** below.
63
+ > The full v2-style decode is still available via `FrameBuffer(project=False)`.
59
64
 
60
65
  Developed for personal use and published for general interest. Runs on Raspberry
61
66
  Pi, macOS, or Linux.
@@ -91,8 +96,8 @@ try:
91
96
  fb.get_complete_frames()
92
97
  while not fb.frame_queue.empty():
93
98
  frame = fb.frame_queue.get()
94
- for channel, decoded in frame["values"].items():
95
- print(channel, decoded)
99
+ for path, value in frame["values"].items():
100
+ print(path, value) # e.g. navigation.speedThroughWater 3.6
96
101
  finally:
97
102
  ser.close()
98
103
  ```
@@ -108,7 +113,7 @@ data to end up:
108
113
 
109
114
  | Project | What it does | Use it when |
110
115
  |---|---|---|
111
- | **pyfastnet** *(this library)* | **Decoder.** Turns raw Fastnet bytes into named instrument channels. | You're writing your own Python and want the decoded data. |
116
+ | **pyfastnet** *(this library)* | **Decoder.** Turns raw Fastnet bytes into Signal K paths in SI units. | You're writing your own Python and want the decoded data. |
112
117
  | [fastnet2ip](https://github.com/ghotihook/fastnet2ip) | **Serial → network.** Broadcasts decoded data over UDP as NMEA 0183 or NMEA 2000 (over IP). | Feeding Signal K, OpenCPN, or a plotter over WiFi / Ethernet. |
113
118
  | [fastnet2n2k](https://github.com/ghotihook/fastnet2n2k) | **Serial → physical NMEA 2000 bus.** Transmits PGNs onto a CAN backbone via SocketCAN. | Wiring into a real NMEA 2000 network / chartplotter. |
114
119
 
@@ -126,7 +131,8 @@ port, a live data store, rate limiting, and output for you.
126
131
  ## Output format
127
132
 
128
133
  Each decoded frame is a dict with `to_address`, `from_address`, `command`, and
129
- `values`. Each entry in `values` is keyed by channel name:
134
+ `values`. `values` maps **Signal K paths to SI values** one canonical entry per
135
+ physical quantity:
130
136
 
131
137
  ```python
132
138
  {
@@ -134,68 +140,74 @@ Each decoded frame is a dict with `to_address`, `from_address`, `command`, and
134
140
  "from_address": "Normal CPU (Wind Board in H2000)",
135
141
  "command": "Broadcast",
136
142
  "values": {
137
- "Apparent Wind Speed (Knots)": {
138
- "channel_id": "0x4D",
139
- "value": 7.0,
140
- "display_text": "7.0",
141
- "layout": None,
142
- },
143
- "Apparent Wind Angle": {
144
- "channel_id": "0x51",
145
- "value": -6.0,
146
- "display_text": "-6.0",
147
- "layout": "-[data]",
148
- },
149
- "True Wind Direction": {
150
- "channel_id": "0x6D",
151
- "value": 213.0,
152
- "display_text": "213.0°M",
153
- "layout": "°M",
154
- },
143
+ "environment.wind.speedApparent": 4.6, # m/s
144
+ "environment.wind.angleApparent": 0.419, # radians
145
+ "environment.wind.directionMagnetic": 1.239, # radians
146
+ "navigation.speedThroughWater": 3.19, # m/s
155
147
  }
156
148
  }
157
149
  ```
158
150
 
159
- ### Position (LatLon) frames
151
+ Units follow the Signal K spec: angles in **radians**, speed in **m/s**, distance
152
+ in **metres**, temperature in **Kelvin**, pressure in **Pascals**. A value is a
153
+ `float` (SI), a `str` enum (e.g. `steering.autopilot.state` → `"standby"`), a
154
+ position object, or `None` when unavailable.
155
+
156
+ Redundant unit-variant channels the bus sends (feet/fathoms depth, knots wind, °F)
157
+ are collapsed to one canonical path. B&G-proprietary channels with no standard
158
+ Signal K path — including the pre-calibration `raw` sensor values — are emitted
159
+ under a `bandg.*` namespace (e.g. `bandg.wind.rawAngleApparent`).
160
160
 
161
- Position frames (command `LatLon`) appear under the `"LatLon"` key. The raw
162
- coordinate string (`DDMM.mmm` with hemisphere letters) is carried in
163
- `display_text`; `value` is `None`. The originating source's marker byte is
164
- preserved in `channel_id` (e.g. `0x47`, `0x4E`) but does not affect the key:
161
+ ### Units and the full path map
162
+
163
+ Every emitted path has a canonical SI unit, available programmatically:
165
164
 
166
165
  ```python
167
- "LatLon": {
168
- "channel_id": "0x4E",
169
- "value": None,
170
- "display_text": "3352.450S15113.920E",
171
- "layout": None,
172
- }
166
+ from fastnet_decoder import unit_for
167
+ unit_for("navigation.speedThroughWater") # "m/s"
168
+ unit_for("navigation.headingMagnetic") # "rad"
169
+ unit_for("environment.water.temperature") # "K"
170
+ unit_for("navigation.position") # "deg"
173
171
  ```
174
172
 
175
- ### Layout field
173
+ The complete mapping — every B&G channel id/name → Signal K path + unit, the
174
+ `bandg.*` vendor namespace, layout-driven Magnetic/True routing, and the
175
+ dropped/collapsed channels — is documented in
176
+ [`docs/v3_signalk_mapping.md`](docs/v3_signalk_mapping.md).
176
177
 
177
- The `layout` field describes the indicator symbol shown on the physical display
178
- around the numeric value. It is the **only** place True/Magnetic, port/starboard
179
- sign, and similar context is carried — the raw stream contains no magnetic
180
- variation or deviation.
178
+ ### Position
181
179
 
182
- | `layout` | Meaning | Sign |
183
- |---|---|---|
184
- | `None` | No indicator symbol | positive |
185
- | `"[data]="` | `=` after value (starboard) | positive |
186
- | `"=[data]"` | `=` before value (port) | negative |
187
- | `"[data]-"` | `-` after value (starboard) | positive |
188
- | `"-[data]"` | `-` before value (port) | negative |
189
- | `"H[data]"` | `H` prefix — heading | positive |
190
- | `"°M"` | Magnetic bearing suffix | positive |
191
- | `"u[data]"` | `u` prefix upwind (VMG) | positive |
192
- | `"d[data]"` | `d` prefix — downwind (VMG) | positive |
193
- | `"L[data]"` | `L` before — leeway port | negative |
194
- | `"[data]L"` | `L` after AP compass target | positive |
195
- | `"[data]°C"` | Celsius suffix | positive |
196
- | `"[data]°F"` | Fahrenheit suffix | positive |
197
- | `"[data]z"` / `"z[data]"` | Dog-leg symbol — AP off course | positive |
198
- | `"TBC"` | Symbol seen but not yet identified | positive |
180
+ Position frames are emitted as `navigation.position`, a decimal-degree object
181
+ (negative for S / W):
182
+
183
+ ```python
184
+ "navigation.position": {"latitude": -33.8742, "longitude": 151.2320}
185
+ ```
186
+
187
+ ### True vs Magnetic
188
+
189
+ The reference is carried by the **path**, not a separate field:
190
+ `navigation.headingMagnetic` vs `navigation.headingTrue`,
191
+ `environment.wind.directionMagnetic` vs `directionTrue`,
192
+ `environment.current.setMagnetic` vs `setTrue`. The decoder selects the right path
193
+ from the display's indicator symbol at decode time. (The raw stream carries no
194
+ magnetic variation or deviation, so it cannot convert between the two.)
195
+
196
+ ### Complete (rich) decode
197
+
198
+ `FrameBuffer(project=False)` queues the full internal decode instead of the Signal K
199
+ projection. Each value is then a dict — `value`, `display_text`, `layout`,
200
+ `channel_id` — keyed by human-readable channel name (the v2 format). Useful for
201
+ debugging and reverse-engineering; `display_text` and `layout` are not exposed in
202
+ the default Signal K output.
203
+
204
+ You can also project a single decoded frame yourself:
205
+
206
+ ```python
207
+ from fastnet_decoder import decode_frame, project
208
+ rich = decode_frame(raw_frame_bytes) # complete decode
209
+ si = project(rich) # {signalk_path: SI_value}
210
+ ```
199
211
 
200
212
  ### Debug API
201
213
 
@@ -0,0 +1,181 @@
1
+ # pyfastnet
2
+
3
+ A Python library for decoding the **FastNet** protocol used by B&G Hydra / H2000
4
+ instruments. Feed it raw bytes from the Fastnet bus and it handles
5
+ synchronisation, checksum validation, and decoding — returning instrument data as
6
+ **Signal K paths in SI units**, ready for further processing.
7
+
8
+ > **v3.0 changes the output format.** Decoded frames are now `{signalk_path:
9
+ > SI_value}` (e.g. `navigation.speedThroughWater = 3.6`) instead of the v2
10
+ > name-keyed `{value, display_text, layout}` dicts. See **Output format** below.
11
+ > The full v2-style decode is still available via `FrameBuffer(project=False)`.
12
+
13
+ Developed for personal use and published for general interest. Runs on Raspberry
14
+ Pi, macOS, or Linux.
15
+
16
+ ## Quick start
17
+
18
+ ```bash
19
+ pip install pyfastnet
20
+ ```
21
+
22
+ ```python
23
+ #!/usr/bin/env python3
24
+ import serial
25
+ from fastnet_decoder import FrameBuffer
26
+
27
+ fb = FrameBuffer()
28
+
29
+ ser = serial.Serial(
30
+ port="/dev/ttyUSB0",
31
+ baudrate=28800,
32
+ bytesize=serial.EIGHTBITS,
33
+ stopbits=serial.STOPBITS_TWO,
34
+ parity=serial.PARITY_ODD,
35
+ timeout=0.1,
36
+ )
37
+
38
+ try:
39
+ while True:
40
+ data = ser.read(256)
41
+ if not data:
42
+ continue
43
+ fb.add_to_buffer(data)
44
+ fb.get_complete_frames()
45
+ while not fb.frame_queue.empty():
46
+ frame = fb.frame_queue.get()
47
+ for path, value in frame["values"].items():
48
+ print(path, value) # e.g. navigation.speedThroughWater 3.6
49
+ finally:
50
+ ser.close()
51
+ ```
52
+
53
+ Serial settings for a B&G Fastnet bus are **28,800 baud, 8 data bits, odd parity,
54
+ 2 stop bits**. No instruments to hand? The companion apps below ship recorded
55
+ captures you can replay.
56
+
57
+ ## The Fastnet toolkit
58
+
59
+ Three projects stack together — pick the layer that matches where you want the
60
+ data to end up:
61
+
62
+ | Project | What it does | Use it when |
63
+ |---|---|---|
64
+ | **pyfastnet** *(this library)* | **Decoder.** Turns raw Fastnet bytes into Signal K paths in SI units. | You're writing your own Python and want the decoded data. |
65
+ | [fastnet2ip](https://github.com/ghotihook/fastnet2ip) | **Serial → network.** Broadcasts decoded data over UDP as NMEA 0183 or NMEA 2000 (over IP). | Feeding Signal K, OpenCPN, or a plotter over WiFi / Ethernet. |
66
+ | [fastnet2n2k](https://github.com/ghotihook/fastnet2n2k) | **Serial → physical NMEA 2000 bus.** Transmits PGNs onto a CAN backbone via SocketCAN. | Wiring into a real NMEA 2000 network / chartplotter. |
67
+
68
+ ```
69
+ ┌─ fastnet2ip → UDP (NMEA 0183 / NMEA 2000 over IP) → Signal K, OpenCPN, plotters
70
+ B&G Fastnet bus ─(serial)─→ pyfastnet ─┤
71
+ └─ fastnet2n2k → SocketCAN (NMEA 2000 PGNs) → CAN backbone, chartplotter
72
+ ```
73
+
74
+ pyfastnet is the **engine** at the bottom of the stack. If you only want the
75
+ decoded data on your network or NMEA 2000 bus — including running it as an
76
+ always-on systemd service — use one of the companion apps; they handle the serial
77
+ port, a live data store, rate limiting, and output for you.
78
+
79
+ ## Output format
80
+
81
+ Each decoded frame is a dict with `to_address`, `from_address`, `command`, and
82
+ `values`. `values` maps **Signal K paths to SI values** — one canonical entry per
83
+ physical quantity:
84
+
85
+ ```python
86
+ {
87
+ "to_address": "Entire System",
88
+ "from_address": "Normal CPU (Wind Board in H2000)",
89
+ "command": "Broadcast",
90
+ "values": {
91
+ "environment.wind.speedApparent": 4.6, # m/s
92
+ "environment.wind.angleApparent": 0.419, # radians
93
+ "environment.wind.directionMagnetic": 1.239, # radians
94
+ "navigation.speedThroughWater": 3.19, # m/s
95
+ }
96
+ }
97
+ ```
98
+
99
+ Units follow the Signal K spec: angles in **radians**, speed in **m/s**, distance
100
+ in **metres**, temperature in **Kelvin**, pressure in **Pascals**. A value is a
101
+ `float` (SI), a `str` enum (e.g. `steering.autopilot.state` → `"standby"`), a
102
+ position object, or `None` when unavailable.
103
+
104
+ Redundant unit-variant channels the bus sends (feet/fathoms depth, knots wind, °F)
105
+ are collapsed to one canonical path. B&G-proprietary channels with no standard
106
+ Signal K path — including the pre-calibration `raw` sensor values — are emitted
107
+ under a `bandg.*` namespace (e.g. `bandg.wind.rawAngleApparent`).
108
+
109
+ ### Units and the full path map
110
+
111
+ Every emitted path has a canonical SI unit, available programmatically:
112
+
113
+ ```python
114
+ from fastnet_decoder import unit_for
115
+ unit_for("navigation.speedThroughWater") # "m/s"
116
+ unit_for("navigation.headingMagnetic") # "rad"
117
+ unit_for("environment.water.temperature") # "K"
118
+ unit_for("navigation.position") # "deg"
119
+ ```
120
+
121
+ The complete mapping — every B&G channel id/name → Signal K path + unit, the
122
+ `bandg.*` vendor namespace, layout-driven Magnetic/True routing, and the
123
+ dropped/collapsed channels — is documented in
124
+ [`docs/v3_signalk_mapping.md`](docs/v3_signalk_mapping.md).
125
+
126
+ ### Position
127
+
128
+ Position frames are emitted as `navigation.position`, a decimal-degree object
129
+ (negative for S / W):
130
+
131
+ ```python
132
+ "navigation.position": {"latitude": -33.8742, "longitude": 151.2320}
133
+ ```
134
+
135
+ ### True vs Magnetic
136
+
137
+ The reference is carried by the **path**, not a separate field:
138
+ `navigation.headingMagnetic` vs `navigation.headingTrue`,
139
+ `environment.wind.directionMagnetic` vs `directionTrue`,
140
+ `environment.current.setMagnetic` vs `setTrue`. The decoder selects the right path
141
+ from the display's indicator symbol at decode time. (The raw stream carries no
142
+ magnetic variation or deviation, so it cannot convert between the two.)
143
+
144
+ ### Complete (rich) decode
145
+
146
+ `FrameBuffer(project=False)` queues the full internal decode instead of the Signal K
147
+ projection. Each value is then a dict — `value`, `display_text`, `layout`,
148
+ `channel_id` — keyed by human-readable channel name (the v2 format). Useful for
149
+ debugging and reverse-engineering; `display_text` and `layout` are not exposed in
150
+ the default Signal K output.
151
+
152
+ You can also project a single decoded frame yourself:
153
+
154
+ ```python
155
+ from fastnet_decoder import decode_frame, project
156
+ rich = decode_frame(raw_frame_bytes) # complete decode
157
+ si = project(rich) # {signalk_path: SI_value}
158
+ ```
159
+
160
+ ### Debug API
161
+
162
+ ```python
163
+ from fastnet_decoder import set_log_level
164
+ import logging
165
+ set_log_level(logging.DEBUG)
166
+ ```
167
+
168
+ ```python
169
+ fb.get_buffer_size() # bytes currently in buffer
170
+ fb.get_buffer_contents() # hex string of buffer contents
171
+ ```
172
+
173
+ ## Acknowledgments
174
+
175
+ - [trlafleur](https://github.com/trlafleur) — background research
176
+ - [Oppedijk](https://www.oppedijk.com/bandg/fastnet.html) — protocol documentation
177
+ - [timmathews](https://github.com/timmathews/bg-fastnet-driver) — C++ reference implementation
178
+
179
+ ## License
180
+
181
+ MIT — see [LICENSE](LICENSE).
@@ -1,7 +1,9 @@
1
- __version__ = "2.0.18"
1
+ __version__ = "3.0.0"
2
2
 
3
3
  from .frame_buffer import FrameBuffer
4
4
  from .decode_fastnet import decode_frame, decode_ascii_frame, decode_light_frame
5
+ from .signalk_map import project, unit_for # v3: projection + per-path SI units
5
6
  from .logger import logger, set_log_level # Import set_log_level for user control
6
7
 
7
- __all__ = ["FrameBuffer", "decode_frame", "decode_ascii_frame", "decode_light_frame", "logger", "set_log_level"]
8
+ __all__ = ["FrameBuffer", "decode_frame", "decode_ascii_frame", "decode_light_frame",
9
+ "project", "unit_for", "logger", "set_log_level"]
@@ -13,7 +13,8 @@ _DECIMAL_PLACES_MAP = {1: 0, 10: 1, 100: 2, 1000: 3}
13
13
  # "[data]-" → value is positive, trailing minus means port/starboard convention
14
14
  # "-[data]" → value is negative (subtract sign)
15
15
  # "=[data]" → value is negative (equals sign variant)
16
- # "H[data]" → "H" prefix, e.g. H045 for a heading
16
+ # "H[data]" → heel to port; H carries the sign, value stored negative (e.g. H20.4)
17
+ # "[data]H" → heel to starboard; value positive (e.g. 33.8H)
17
18
  # "°M" → magnetic bearing, suffix added after value
18
19
 
19
20
  def _sign_from_layout(layout: str) -> int:
@@ -192,6 +193,8 @@ def decode_ascii_frame(frame: bytes) -> dict:
192
193
  command = frame[3]
193
194
  body = frame[5:-1]
194
195
 
196
+ # TBC: no len(body) guard here (unlike decode_frame/decode_light_frame) —
197
+ # a short/empty body raises IndexError and falls to the except below.
195
198
  channel_id = body[0]
196
199
  # body[1] is a format byte — not used for ASCII frames
197
200
  data_bytes = body[2:]
@@ -2,6 +2,7 @@ import logging
2
2
  from .utils import calculate_checksum
3
3
  from .mappings import COMMAND_LOOKUP, IGNORED_COMMANDS
4
4
  from .decode_fastnet import decode_frame, decode_ascii_frame, decode_light_frame, probe_frame
5
+ from . import signalk_map
5
6
  from .logger import logger
6
7
  from queue import Queue, Full
7
8
 
@@ -17,10 +18,13 @@ class FrameBuffer:
17
18
  3. Pull decoded frames from frame_queue
18
19
  """
19
20
 
20
- def __init__(self, max_buffer_size=8192, max_queue_size=1000):
21
+ def __init__(self, max_buffer_size=8192, max_queue_size=1000, project=True):
21
22
  self.buffer = bytearray()
22
23
  self.max_buffer_size = max_buffer_size
23
24
  self.frame_queue = Queue(maxsize=max_queue_size)
25
+ # v3: queue the {signalk_path: SI_value} projection by default. project=False
26
+ # queues the complete (rich) decode instead — for the golden guard / debugging.
27
+ self.project = project
24
28
 
25
29
  def add_to_buffer(self, new_data):
26
30
  if not isinstance(new_data, (bytes, bytearray)):
@@ -103,20 +107,37 @@ class FrameBuffer:
103
107
  probe_frame(frame)
104
108
  return
105
109
 
106
- if decoded_frame and "values" in decoded_frame:
107
- try:
108
- self.frame_queue.put_nowait(decoded_frame)
110
+ if not (decoded_frame and "values" in decoded_frame):
111
+ if logger.isEnabledFor(logging.DEBUG):
112
+ logger.debug(f" QUEUE fail decode error [{frame.hex()}]")
113
+ return
114
+
115
+ if self.project:
116
+ values = signalk_map.project(decoded_frame)
117
+ if not values:
118
+ # every channel dropped / unmapped (e.g. Backlight, protocol) — nothing to emit
109
119
  if logger.isEnabledFor(logging.DEBUG):
110
- channel_names = list(decoded_frame["values"].keys())
111
- names_str = ", ".join(channel_names[:4])
112
- if len(channel_names) > 4:
113
- names_str += f", +{len(channel_names) - 4} more"
114
- logger.debug(f" QUEUE {len(channel_names)} channel(s) [{names_str}]")
115
- except Full:
116
- logger.warning("Frame queue full, dropping frame.")
120
+ logger.debug(f" QUEUE skip no mapped paths cmd={command_name}")
121
+ return
122
+ queued = {
123
+ "to_address": decoded_frame.get("to_address"),
124
+ "from_address": decoded_frame.get("from_address"),
125
+ "command": decoded_frame.get("command"),
126
+ "values": values,
127
+ }
117
128
  else:
129
+ queued = decoded_frame
130
+
131
+ try:
132
+ self.frame_queue.put_nowait(queued)
118
133
  if logger.isEnabledFor(logging.DEBUG):
119
- logger.debug(f" QUEUE fail decode error [{frame.hex()}]")
134
+ keys = list(queued["values"].keys())
135
+ names_str = ", ".join(keys[:4])
136
+ if len(keys) > 4:
137
+ names_str += f", +{len(keys) - 4} more"
138
+ logger.debug(f" QUEUE {len(keys)} value(s) [{names_str}]")
139
+ except Full:
140
+ logger.warning("Frame queue full, dropping frame.")
120
141
 
121
142
  def get_buffer_size(self):
122
143
  return len(self.buffer)
@@ -15,6 +15,11 @@ logger.setLevel(DEFAULT_LOG_LEVEL)
15
15
 
16
16
  def set_log_level(level_name: str):
17
17
  """Sets the log level dynamically at runtime."""
18
- level = getattr(logging, level_name.upper(), DEFAULT_LOG_LEVEL)
18
+ level = logging.getLevelName(level_name.upper())
19
+ if not isinstance(level, int):
20
+ logger.warning(
21
+ f"Unknown log level '{level_name}'; keeping current level."
22
+ )
23
+ return
19
24
  logger.setLevel(level)
20
25
  logger.info(f"Log level set to {level_name.upper()}.")