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.
Files changed (39) hide show
  1. {pyfastnet-3.0.0/pyfastnet.egg-info → pyfastnet-3.1.0}/PKG-INFO +13 -4
  2. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/README.md +12 -3
  3. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/__init__.py +3 -3
  4. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/signalk_map.py +64 -5
  5. {pyfastnet-3.0.0 → pyfastnet-3.1.0/pyfastnet.egg-info}/PKG-INFO +13 -4
  6. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyfastnet.egg-info/SOURCES.txt +1 -0
  7. pyfastnet-3.1.0/tests/test_channel_map.py +28 -0
  8. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_signalk_projection.py +20 -0
  9. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/LICENSE +0 -0
  10. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/decode_fastnet.py +0 -0
  11. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/frame_buffer.py +0 -0
  12. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/logger.py +0 -0
  13. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/mappings.py +0 -0
  14. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/fastnet_decoder/utils.py +0 -0
  15. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyfastnet.egg-info/dependency_links.txt +0 -0
  16. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyfastnet.egg-info/requires.txt +0 -0
  17. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyfastnet.egg-info/top_level.txt +0 -0
  18. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/pyproject.toml +0 -0
  19. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/setup.cfg +0 -0
  20. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_autopilot.py +0 -0
  21. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_awa.py +0 -0
  22. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_aws.py +0 -0
  23. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_backlight.py +0 -0
  24. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_battery.py +0 -0
  25. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_boatspeed.py +0 -0
  26. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_depth_frame.py +0 -0
  27. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_format07_msb_regression.py +0 -0
  28. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_format08_layout.py +0 -0
  29. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_golden.py +0 -0
  30. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_heading.py +0 -0
  31. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_heel_trim.py +0 -0
  32. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_latlon_frame.py +0 -0
  33. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_sea_temp.py +0 -0
  34. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_sog.py +0 -0
  35. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_tidal.py +0 -0
  36. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_timer.py +0 -0
  37. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_twa.py +0 -0
  38. {pyfastnet-3.0.0 → pyfastnet-3.1.0}/tests/test_twd.py +0 -0
  39. {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.0.0
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 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
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 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
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.0.0"
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 # v3: projection + per-path SI units
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", _hpa),
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. Unmapped or dropped
233
- channels are omitted. Layout-routed bearings pick Magnetic vs True from the
234
- decoded layout byte.
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.0.0
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 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
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
@@ -19,6 +19,7 @@ tests/test_aws.py
19
19
  tests/test_backlight.py
20
20
  tests/test_battery.py
21
21
  tests/test_boatspeed.py
22
+ tests/test_channel_map.py
22
23
  tests/test_depth_frame.py
23
24
  tests/test_format07_msb_regression.py
24
25
  tests/test_format08_layout.py
@@ -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