pyfastnet 2.0.13__tar.gz → 2.0.14__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 (27) hide show
  1. {pyfastnet-2.0.13/pyfastnet.egg-info → pyfastnet-2.0.14}/PKG-INFO +19 -2
  2. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/README.md +16 -0
  3. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/fastnet_decoder/decode_fastnet.py +8 -4
  4. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/fastnet_decoder/mappings.py +0 -1
  5. {pyfastnet-2.0.13 → pyfastnet-2.0.14/pyfastnet.egg-info}/PKG-INFO +19 -2
  6. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/pyfastnet.egg-info/SOURCES.txt +1 -0
  7. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/setup.py +1 -1
  8. pyfastnet-2.0.14/tests/test_latlon_frame.py +56 -0
  9. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/LICENSE +0 -0
  10. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/MANIFEST.in +0 -0
  11. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/fastnet_decoder/__init__.py +0 -0
  12. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/fastnet_decoder/frame_buffer.py +0 -0
  13. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/fastnet_decoder/logger.py +0 -0
  14. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/fastnet_decoder/utils.py +0 -0
  15. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/pyfastnet.egg-info/dependency_links.txt +0 -0
  16. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/pyfastnet.egg-info/top_level.txt +0 -0
  17. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/setup.cfg +0 -0
  18. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_apparent_frame.py +0 -0
  19. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_autopilot_frame.py +0 -0
  20. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_channels.py +0 -0
  21. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_depth_frame.py +0 -0
  22. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_format07_msb_regression.py +0 -0
  23. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_format08_layout.py +0 -0
  24. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_heel_frame.py +0 -0
  25. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_rudder_frame.py +0 -0
  26. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_tide_frame.py +0 -0
  27. {pyfastnet-2.0.13 → pyfastnet-2.0.14}/tests/test_true_frame.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pyfastnet
3
- Version: 2.0.13
3
+ Version: 2.0.14
4
4
  Summary: A Python library for decoding FastNet protocol data streams.
5
5
  Home-page: https://github.com/ghotihook/pyfastnet
6
6
  Author: Alex Salmon
@@ -17,6 +17,7 @@ Dynamic: classifier
17
17
  Dynamic: description
18
18
  Dynamic: description-content-type
19
19
  Dynamic: home-page
20
+ Dynamic: license-file
20
21
  Dynamic: requires-python
21
22
  Dynamic: summary
22
23
 
@@ -99,6 +100,22 @@ Each decoded frame is a dict with `to_address`, `from_address`, `command`, and `
99
100
  }
100
101
  ```
101
102
 
103
+ ### Position (LatLon) frames
104
+
105
+ Position frames (command `LatLon`) appear under the `"LatLon"` key. The raw coordinate
106
+ string (`DDMM.mmm` with hemisphere letters) is carried in `display_text`; `value` is
107
+ `None`. The originating source's marker byte is preserved in `channel_id` (e.g. `0x47`,
108
+ `0x4E`) but does not affect the key:
109
+
110
+ ```python
111
+ "LatLon": {
112
+ "channel_id": "0x4E",
113
+ "value": None,
114
+ "display_text": "3352.450S15113.920E",
115
+ "layout": None,
116
+ }
117
+ ```
118
+
102
119
  ### Layout field
103
120
 
104
121
  The `layout` field describes the indicator symbol shown on the physical display around the numeric value:
@@ -77,6 +77,22 @@ Each decoded frame is a dict with `to_address`, `from_address`, `command`, and `
77
77
  }
78
78
  ```
79
79
 
80
+ ### Position (LatLon) frames
81
+
82
+ Position frames (command `LatLon`) appear under the `"LatLon"` key. The raw coordinate
83
+ string (`DDMM.mmm` with hemisphere letters) is carried in `display_text`; `value` is
84
+ `None`. The originating source's marker byte is preserved in `channel_id` (e.g. `0x47`,
85
+ `0x4E`) but does not affect the key:
86
+
87
+ ```python
88
+ "LatLon": {
89
+ "channel_id": "0x4E",
90
+ "value": None,
91
+ "display_text": "3352.450S15113.920E",
92
+ "layout": None,
93
+ }
94
+ ```
95
+
80
96
  ### Layout field
81
97
 
82
98
  The `layout` field describes the indicator symbol shown on the physical display around the numeric value:
@@ -119,9 +119,14 @@ def decode_ascii_frame(frame: bytes) -> dict:
119
119
  channel_id = body[0]
120
120
  # body[1] is a format byte — not used for ASCII frames
121
121
  data_bytes = body[2:]
122
- channel_name = CHANNEL_LOOKUP.get(channel_id)
123
- if channel_name is None:
124
- channel_name = f"Unknown (0x{channel_id:02X})"
122
+ # This decoder is only ever called for LatLon command frames, so the
123
+ # entry is named from the command — not from body[0]. body[0] is a
124
+ # source/marker byte that varies by GPS unit (0x47, 0x4E, ...) and is
125
+ # NOT a generic channel id; looking it up in CHANNEL_LOOKUP produced
126
+ # nonsense names like "Apparent Wind Speed (Raw)". The raw byte is kept
127
+ # in channel_id below for diagnostics.
128
+ cmd_name = COMMAND_LOOKUP.get(command)
129
+ channel_name = cmd_name if cmd_name is not None else f"Unknown (0x{command:02X})"
125
130
 
126
131
  try:
127
132
  ascii_text = data_bytes.decode("ascii").strip()
@@ -133,7 +138,6 @@ def decode_ascii_frame(frame: bytes) -> dict:
133
138
 
134
139
  to_name = ADDRESS_LOOKUP.get(to_address)
135
140
  from_name = ADDRESS_LOOKUP.get(from_address)
136
- cmd_name = COMMAND_LOOKUP.get(command)
137
141
 
138
142
  return {
139
143
  "to_address": to_name if to_name is not None else f"Unknown (0x{to_address:02X})",
@@ -192,7 +192,6 @@ CHANNEL_LOOKUP = {
192
192
  0x42: "Boatspeed (Raw)",
193
193
  0x44: "Yaw rate",
194
194
  0x46: "Autopilot Speed Fixed (Knots)",
195
- 0x47: "LatLon",
196
195
  0x49: "Heading",
197
196
  0x4A: "Heading (Raw)",
198
197
  0x4D: "Apparent Wind Speed (Knots)",
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pyfastnet
3
- Version: 2.0.13
3
+ Version: 2.0.14
4
4
  Summary: A Python library for decoding FastNet protocol data streams.
5
5
  Home-page: https://github.com/ghotihook/pyfastnet
6
6
  Author: Alex Salmon
@@ -17,6 +17,7 @@ Dynamic: classifier
17
17
  Dynamic: description
18
18
  Dynamic: description-content-type
19
19
  Dynamic: home-page
20
+ Dynamic: license-file
20
21
  Dynamic: requires-python
21
22
  Dynamic: summary
22
23
 
@@ -99,6 +100,22 @@ Each decoded frame is a dict with `to_address`, `from_address`, `command`, and `
99
100
  }
100
101
  ```
101
102
 
103
+ ### Position (LatLon) frames
104
+
105
+ Position frames (command `LatLon`) appear under the `"LatLon"` key. The raw coordinate
106
+ string (`DDMM.mmm` with hemisphere letters) is carried in `display_text`; `value` is
107
+ `None`. The originating source's marker byte is preserved in `channel_id` (e.g. `0x47`,
108
+ `0x4E`) but does not affect the key:
109
+
110
+ ```python
111
+ "LatLon": {
112
+ "channel_id": "0x4E",
113
+ "value": None,
114
+ "display_text": "3352.450S15113.920E",
115
+ "layout": None,
116
+ }
117
+ ```
118
+
102
119
  ### Layout field
103
120
 
104
121
  The `layout` field describes the indicator symbol shown on the physical display around the numeric value:
@@ -19,6 +19,7 @@ tests/test_depth_frame.py
19
19
  tests/test_format07_msb_regression.py
20
20
  tests/test_format08_layout.py
21
21
  tests/test_heel_frame.py
22
+ tests/test_latlon_frame.py
22
23
  tests/test_rudder_frame.py
23
24
  tests/test_tide_frame.py
24
25
  tests/test_true_frame.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pyfastnet",
5
- version="2.0.13", # Ensure this matches your intended version
5
+ version="2.0.14", # Ensure this matches your intended version
6
6
  author="Alex Salmon",
7
7
  author_email="alex@ivila.net",
8
8
  description="A Python library for decoding FastNet protocol data streams.",
@@ -0,0 +1,56 @@
1
+ import unittest
2
+ from fastnet_decoder.decode_fastnet import decode_ascii_frame
3
+
4
+
5
+ class TestLatLonFrame(unittest.TestCase):
6
+ """
7
+ Tests for LatLon (position) command frames (command 0x03).
8
+
9
+ These frames are dispatched to decode_ascii_frame, which carries the raw
10
+ coordinate string in display_text. The entry is named from the command
11
+ ("LatLon") — NOT from body[0]. body[0] is a per-source marker byte that
12
+ varies by GPS unit (0x4E, 0x47, ...) and is not a generic channel id, so
13
+ it must not be looked up in CHANNEL_LOOKUP (doing so previously mislabelled
14
+ positions as "Apparent Wind Speed (Raw)").
15
+ """
16
+
17
+ # Real frame, from_address 0x60 (External Compass NMEA FFD 60).
18
+ # body[0] = 0x4E. Position: 33°52.450'S, 151°13.920'E (Sydney).
19
+ FRAME_4E = "ff601503894e50333335322e3435305331353131332e3932304572"
20
+
21
+ # Real frame, from_address 0x62 (External Compass NMEA FFD 62).
22
+ # body[0] = 0x47. Position: 16°46.61'S, 179°20.23'E.
23
+ FRAME_47 = "ff621503874750313634362e3631205331373932302e3233204595"
24
+
25
+ def test_4e_source_keyed_latlon(self):
26
+ values = decode_ascii_frame(bytes.fromhex(self.FRAME_4E))["values"]
27
+ self.assertIn("LatLon", values)
28
+ self.assertNotIn("Apparent Wind Speed (Raw)", values)
29
+ entry = values["LatLon"]
30
+ self.assertEqual(entry["channel_id"], "0x4E")
31
+ self.assertEqual(entry["display_text"], "3352.450S15113.920E")
32
+ self.assertIsNone(entry["value"])
33
+
34
+ def test_47_source_keyed_latlon(self):
35
+ values = decode_ascii_frame(bytes.fromhex(self.FRAME_47))["values"]
36
+ self.assertIn("LatLon", values)
37
+ entry = values["LatLon"]
38
+ self.assertEqual(entry["channel_id"], "0x47")
39
+ self.assertEqual(entry["display_text"], "1646.61 S17920.23 E")
40
+ self.assertIsNone(entry["value"])
41
+
42
+ def test_both_sources_share_one_key(self):
43
+ # The fix normalises differing source bytes to a single consistent key.
44
+ keys_4e = set(decode_ascii_frame(bytes.fromhex(self.FRAME_4E))["values"])
45
+ keys_47 = set(decode_ascii_frame(bytes.fromhex(self.FRAME_47))["values"])
46
+ self.assertEqual(keys_4e, keys_47, {"LatLon"})
47
+
48
+ def test_command_metadata(self):
49
+ decoded = decode_ascii_frame(bytes.fromhex(self.FRAME_4E))
50
+ self.assertNotIn("error", decoded)
51
+ self.assertEqual(decoded["command"], "LatLon")
52
+ self.assertEqual(decoded["from_address"], "External Compass (NMEA FFD 60)")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ unittest.main()
File without changes
File without changes
File without changes