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.
- {pyfastnet-2.0.18/pyfastnet.egg-info → pyfastnet-3.0.0}/PKG-INFO +70 -58
- pyfastnet-3.0.0/README.md +181 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/__init__.py +4 -2
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/decode_fastnet.py +4 -1
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/frame_buffer.py +33 -12
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/logger.py +6 -1
- pyfastnet-3.0.0/fastnet_decoder/signalk_map.py +293 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0/pyfastnet.egg-info}/PKG-INFO +70 -58
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyfastnet.egg-info/SOURCES.txt +3 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_backlight.py +6 -2
- pyfastnet-3.0.0/tests/test_golden.py +33 -0
- pyfastnet-3.0.0/tests/test_signalk_projection.py +171 -0
- pyfastnet-2.0.18/README.md +0 -169
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/LICENSE +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/mappings.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/fastnet_decoder/utils.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyfastnet.egg-info/dependency_links.txt +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyfastnet.egg-info/requires.txt +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyfastnet.egg-info/top_level.txt +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/pyproject.toml +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/setup.cfg +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_autopilot.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_awa.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_aws.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_battery.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_boatspeed.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_depth_frame.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_format07_msb_regression.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_format08_layout.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_heading.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_heel_trim.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_latlon_frame.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_sea_temp.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_sog.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_tidal.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_timer.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_twa.py +0 -0
- {pyfastnet-2.0.18 → pyfastnet-3.0.0}/tests/test_twd.py +0 -0
- {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:
|
|
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
|
|
58
|
-
|
|
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
|
|
95
|
-
print(
|
|
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
|
|
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`.
|
|
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
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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__ = "
|
|
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",
|
|
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]" →
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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 =
|
|
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()}.")
|