pyfastnet 3.0.0__tar.gz → 3.1.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-3.0.0/pyfastnet.egg-info → pyfastnet-3.1.0}/PKG-INFO +13 -4
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/README.md +12 -3
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/__init__.py +3 -3
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/signalk_map.py +64 -5
- {pyfastnet-3.0.0 → pyfastnet-3.1.0/pyfastnet.egg-info}/PKG-INFO +13 -4
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyfastnet.egg-info/SOURCES.txt +1 -0
- pyfastnet-3.1.0/tests/test_channel_map.py +28 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_signalk_projection.py +20 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/LICENSE +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/decode_fastnet.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/frame_buffer.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/logger.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/mappings.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/utils.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyfastnet.egg-info/dependency_links.txt +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyfastnet.egg-info/requires.txt +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyfastnet.egg-info/top_level.txt +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyproject.toml +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/setup.cfg +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_autopilot.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_awa.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_aws.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_backlight.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_battery.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_boatspeed.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_depth_frame.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_format07_msb_regression.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_format08_layout.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_golden.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_heading.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_heel_trim.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_latlon_frame.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_sea_temp.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_sog.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_tidal.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_timer.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_twa.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_twd.py +0 -0
- {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_tws.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyfastnet
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.1.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
|
|
@@ -170,9 +170,18 @@ unit_for("environment.water.temperature") # "K"
|
|
|
170
170
|
unit_for("navigation.position") # "deg"
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
The
|
|
174
|
-
`
|
|
175
|
-
|
|
173
|
+
The master reference — every B&G channel number → name → Signal K path + unit — is
|
|
174
|
+
`channel_map()`, derived from the projection tables so it can't drift:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from fastnet_decoder import channel_map
|
|
178
|
+
channel_map()[0x41]
|
|
179
|
+
# {'name': 'Boatspeed (Knots)', 'path': 'navigation.speedThroughWater',
|
|
180
|
+
# 'unit': 'm/s', 'kind': 'standard'}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
It is rendered as a table in [`docs/channel_map.md`](docs/channel_map.md). The design
|
|
184
|
+
rationale, `bandg.*` namespace, Magnetic/True routing, and open TBCs are in
|
|
176
185
|
[`docs/v3_signalk_mapping.md`](docs/v3_signalk_mapping.md).
|
|
177
186
|
|
|
178
187
|
### Position
|
|
@@ -118,9 +118,18 @@ unit_for("environment.water.temperature") # "K"
|
|
|
118
118
|
unit_for("navigation.position") # "deg"
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
The
|
|
122
|
-
`
|
|
123
|
-
|
|
121
|
+
The master reference — every B&G channel number → name → Signal K path + unit — is
|
|
122
|
+
`channel_map()`, derived from the projection tables so it can't drift:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from fastnet_decoder import channel_map
|
|
126
|
+
channel_map()[0x41]
|
|
127
|
+
# {'name': 'Boatspeed (Knots)', 'path': 'navigation.speedThroughWater',
|
|
128
|
+
# 'unit': 'm/s', 'kind': 'standard'}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
It is rendered as a table in [`docs/channel_map.md`](docs/channel_map.md). The design
|
|
132
|
+
rationale, `bandg.*` namespace, Magnetic/True routing, and open TBCs are in
|
|
124
133
|
[`docs/v3_signalk_mapping.md`](docs/v3_signalk_mapping.md).
|
|
125
134
|
|
|
126
135
|
### Position
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
__version__ = "3.
|
|
1
|
+
__version__ = "3.1.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 #
|
|
5
|
+
from .signalk_map import project, unit_for, channel_map # projection + units + master map
|
|
6
6
|
from .logger import logger, set_log_level # Import set_log_level for user control
|
|
7
7
|
|
|
8
8
|
__all__ = ["FrameBuffer", "decode_frame", "decode_ascii_frame", "decode_light_frame",
|
|
9
|
-
"project", "unit_for", "logger", "set_log_level"]
|
|
9
|
+
"project", "unit_for", "channel_map", "logger", "set_log_level"]
|
|
@@ -121,7 +121,7 @@ VENDOR = {
|
|
|
121
121
|
0x29: ("bandg.steering.offCourse", _deg),
|
|
122
122
|
0xAF: ("bandg.steering.autopilot.offCourse", _deg),
|
|
123
123
|
0x46: ("bandg.steering.autopilot.fixedSpeed", _kn),
|
|
124
|
-
0x86: ("bandg.environment.pressureTrend",
|
|
124
|
+
0x86: ("bandg.environment.pressureTrend", _id), # rate/tendency, encoding TBC — opaque
|
|
125
125
|
0xDC: ("bandg.time.local", _id),
|
|
126
126
|
0x75: ("bandg.time.timer", _id),
|
|
127
127
|
}
|
|
@@ -180,6 +180,59 @@ def unit_for(path):
|
|
|
180
180
|
return ""
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
# ── Master reference: B&G channel -> name -> Signal K path ─────────────────────
|
|
184
|
+
# Channels collapsed onto a canonical sibling (redundant unit/format variants).
|
|
185
|
+
COLLAPSED = {
|
|
186
|
+
0x1C: "environment.outside.temperature", # Air Temp °F -> °C
|
|
187
|
+
0x1E: "environment.water.temperature", # Sea Temp °F -> °C
|
|
188
|
+
0x4D: "environment.wind.speedApparent", # AWS knots -> m/s
|
|
189
|
+
0x55: "environment.wind.speedTrue", # TWS knots -> m/s
|
|
190
|
+
0x65: "bandg.navigation.speedThroughWaterAverage", # Average Speed raw -> 0x64
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _disposition(cid):
|
|
195
|
+
"""(path, unit, kind) for a channel id, resolved against the projection maps."""
|
|
196
|
+
if cid in _ROUTED:
|
|
197
|
+
m, _t, _ = _ROUTED[cid]
|
|
198
|
+
stem = m[:-len("Magnetic")] # m ends with "Magnetic"
|
|
199
|
+
return f"{stem}{{Magnetic,True}}", "rad", "routed(M/T)"
|
|
200
|
+
if cid in (0xC1, 0xC2, 0xC3):
|
|
201
|
+
return DEPTH_PATH, "m", "standard(depth fallback)"
|
|
202
|
+
if cid == 0xB5:
|
|
203
|
+
return "steering.autopilot.state", "enum", "standard"
|
|
204
|
+
if cid in STANDARD:
|
|
205
|
+
p = STANDARD[cid][0]
|
|
206
|
+
return p, unit_for(p.format(id="house")), "standard"
|
|
207
|
+
if cid in VENDOR:
|
|
208
|
+
p = VENDOR[cid][0]
|
|
209
|
+
return p, unit_for(p), "vendor"
|
|
210
|
+
if cid in COLLAPSED:
|
|
211
|
+
return f"→ {COLLAPSED[cid]}", "", "collapsed"
|
|
212
|
+
if cid in DROP:
|
|
213
|
+
return "—", "", "drop"
|
|
214
|
+
return f"bandg.unknown.0x{cid:02X}", "", "unknown"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def channel_map():
|
|
218
|
+
"""Master reference table: ``{channel_id: {"name","path","unit","kind"}}`` for
|
|
219
|
+
every B&G channel in ``CHANNEL_LOOKUP``.
|
|
220
|
+
|
|
221
|
+
Derived from the projection maps (``STANDARD``/``DEPTH``/``_ROUTED``/``VENDOR``/
|
|
222
|
+
``COLLAPSED``/``DROP``), so it is always in step with what ``project()`` emits —
|
|
223
|
+
treat this as the authoritative B&G-channel → Signal K-path reference.
|
|
224
|
+
|
|
225
|
+
Note: position (``navigation.position``) and backlight come from *command* frames
|
|
226
|
+
(LatLon / Light Intensity), not channel ids, so they are not listed here.
|
|
227
|
+
"""
|
|
228
|
+
from .mappings import CHANNEL_LOOKUP
|
|
229
|
+
out = {}
|
|
230
|
+
for cid, bng in sorted(CHANNEL_LOOKUP.items()):
|
|
231
|
+
path, unit, kind = _disposition(cid)
|
|
232
|
+
out[cid] = {"name": bng, "path": path, "unit": unit, "kind": kind}
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
|
|
183
236
|
def _cid(entry):
|
|
184
237
|
"""Parse a decoded value's channel_id ('0xC1') to int, or None."""
|
|
185
238
|
raw = entry.get("channel_id")
|
|
@@ -229,9 +282,10 @@ def parse_position(ascii_text):
|
|
|
229
282
|
def project(decoded_frame, battery_id="house"):
|
|
230
283
|
"""Project a decoded frame (from decode_frame/ascii/light) → {signalk_path: value}.
|
|
231
284
|
|
|
232
|
-
Returns SI floats / enum strings / position dicts / None.
|
|
233
|
-
channels are omitted
|
|
234
|
-
|
|
285
|
+
Returns SI floats / enum strings / position dicts / None. Dropped and collapsed
|
|
286
|
+
channels are omitted; a channel we could decode but have no mapping for is kept
|
|
287
|
+
under ``bandg.unknown.0x<id>`` (raw value) so decodable data is never silently
|
|
288
|
+
lost. Layout-routed bearings pick Magnetic vs True from the decoded layout byte.
|
|
235
289
|
"""
|
|
236
290
|
command = decoded_frame.get("command")
|
|
237
291
|
values = decoded_frame.get("values", {})
|
|
@@ -249,7 +303,7 @@ def project(decoded_frame, battery_id="house"):
|
|
|
249
303
|
for entry in values.values():
|
|
250
304
|
cid = _cid(entry)
|
|
251
305
|
value = entry.get("value")
|
|
252
|
-
if cid is None or cid in DROP:
|
|
306
|
+
if cid is None or cid in DROP or cid in COLLAPSED:
|
|
253
307
|
continue
|
|
254
308
|
|
|
255
309
|
# Depth fallback chain, resolved after the loop.
|
|
@@ -283,6 +337,11 @@ def project(decoded_frame, battery_id="house"):
|
|
|
283
337
|
if cid in VENDOR:
|
|
284
338
|
path, tf = VENDOR[cid]
|
|
285
339
|
out[path] = tf(value) if value is not None else None
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# Unmapped but decodable — keep it (raw value) rather than drop it silently.
|
|
343
|
+
if value is not None:
|
|
344
|
+
out[f"bandg.unknown.0x{cid:02X}"] = value
|
|
286
345
|
|
|
287
346
|
# Resolve depth by priority: metres → feet → fathoms.
|
|
288
347
|
for cid, tf in DEPTH:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyfastnet
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.1.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
|
|
@@ -170,9 +170,18 @@ unit_for("environment.water.temperature") # "K"
|
|
|
170
170
|
unit_for("navigation.position") # "deg"
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
The
|
|
174
|
-
`
|
|
175
|
-
|
|
173
|
+
The master reference — every B&G channel number → name → Signal K path + unit — is
|
|
174
|
+
`channel_map()`, derived from the projection tables so it can't drift:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from fastnet_decoder import channel_map
|
|
178
|
+
channel_map()[0x41]
|
|
179
|
+
# {'name': 'Boatspeed (Knots)', 'path': 'navigation.speedThroughWater',
|
|
180
|
+
# 'unit': 'm/s', 'kind': 'standard'}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
It is rendered as a table in [`docs/channel_map.md`](docs/channel_map.md). The design
|
|
184
|
+
rationale, `bandg.*` namespace, Magnetic/True routing, and open TBCs are in
|
|
176
185
|
[`docs/v3_signalk_mapping.md`](docs/v3_signalk_mapping.md).
|
|
177
186
|
|
|
178
187
|
### Position
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""channel_map() is the master reference — verify coverage and doc sync."""
|
|
2
|
+
import importlib.util
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fastnet_decoder import channel_map
|
|
6
|
+
from fastnet_decoder.mappings import CHANNEL_LOOKUP
|
|
7
|
+
|
|
8
|
+
DOCS = Path(__file__).resolve().parents[1] / "docs"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_covers_every_channel():
|
|
12
|
+
cm = channel_map()
|
|
13
|
+
assert set(cm) == set(CHANNEL_LOOKUP), "channel_map must cover all CHANNEL_LOOKUP ids"
|
|
14
|
+
for cid, r in cm.items():
|
|
15
|
+
assert r["name"] == CHANNEL_LOOKUP[cid]
|
|
16
|
+
assert set(r) == {"name", "path", "unit", "kind"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_doc_is_in_sync():
|
|
20
|
+
# Render via the generator and compare to the committed doc — catches drift.
|
|
21
|
+
spec = importlib.util.spec_from_file_location("gen_cm", DOCS / "generate_channel_map.py")
|
|
22
|
+
gen = importlib.util.module_from_spec(spec)
|
|
23
|
+
spec.loader.exec_module(gen)
|
|
24
|
+
rows = channel_map()
|
|
25
|
+
committed = (DOCS / "channel_map.md").read_text()
|
|
26
|
+
for cid, r in rows.items():
|
|
27
|
+
assert f"| 0x{cid:02X} | {r['name']} | `{r['path']}` |" in committed, \
|
|
28
|
+
f"docs/channel_map.md out of date for 0x{cid:02X} — regenerate it"
|
|
@@ -135,6 +135,26 @@ def test_vendor_path():
|
|
|
135
135
|
assert "bandg.wind.upwash" in out
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def test_unmapped_channel_emitted_as_unknown():
|
|
139
|
+
out = sk.project(_frame("0x0C", 123.0)) # Linear 5 — in CHANNEL_LOOKUP, unmapped
|
|
140
|
+
assert out == {"bandg.unknown.0x0C": 123.0}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_unknown_channel_id_emitted():
|
|
144
|
+
out = sk.project(_frame("0xC4", 42.0)) # id not in CHANNEL_LOOKUP
|
|
145
|
+
assert out == {"bandg.unknown.0xC4": 42.0}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_collapsed_channel_not_unknown():
|
|
149
|
+
out = sk.project(_frame("0x1C", 70.0)) # Air Temp °F — collapsed, must be dropped
|
|
150
|
+
assert out == {}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_unknown_skips_none_value():
|
|
154
|
+
out = sk.project(_frame("0xC5", None)) # unmapped, no value → nothing
|
|
155
|
+
assert out == {}
|
|
156
|
+
|
|
157
|
+
|
|
138
158
|
# ── end-to-end against a real decoded frame ───────────────────────────────────
|
|
139
159
|
def test_real_heel_frame_end_to_end():
|
|
140
160
|
# Heel-to-port frame (value -20.4°) from tests/test_heel_trim.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|