pyfastnet 2.0.12__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 (31) hide show
  1. pyfastnet-2.0.14/PKG-INFO +165 -0
  2. pyfastnet-2.0.14/README.md +142 -0
  3. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/decode_fastnet.py +9 -5
  4. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/mappings.py +0 -1
  5. pyfastnet-2.0.14/fastnet_decoder/utils.py +11 -0
  6. pyfastnet-2.0.14/pyfastnet.egg-info/PKG-INFO +165 -0
  7. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/pyfastnet.egg-info/SOURCES.txt +1 -0
  8. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/setup.py +1 -1
  9. pyfastnet-2.0.14/tests/test_latlon_frame.py +56 -0
  10. pyfastnet-2.0.12/PKG-INFO +0 -134
  11. pyfastnet-2.0.12/README.md +0 -112
  12. pyfastnet-2.0.12/fastnet_decoder/utils.py +0 -48
  13. pyfastnet-2.0.12/pyfastnet.egg-info/PKG-INFO +0 -134
  14. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/LICENSE +0 -0
  15. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/MANIFEST.in +0 -0
  16. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/__init__.py +0 -0
  17. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/frame_buffer.py +0 -0
  18. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/logger.py +0 -0
  19. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/pyfastnet.egg-info/dependency_links.txt +0 -0
  20. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/pyfastnet.egg-info/top_level.txt +0 -0
  21. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/setup.cfg +0 -0
  22. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_apparent_frame.py +0 -0
  23. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_autopilot_frame.py +0 -0
  24. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_channels.py +0 -0
  25. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_depth_frame.py +0 -0
  26. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_format07_msb_regression.py +0 -0
  27. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_format08_layout.py +0 -0
  28. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_heel_frame.py +0 -0
  29. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_rudder_frame.py +0 -0
  30. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_tide_frame.py +0 -0
  31. {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_true_frame.py +0 -0
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyfastnet
3
+ Version: 2.0.14
4
+ Summary: A Python library for decoding FastNet protocol data streams.
5
+ Home-page: https://github.com/ghotihook/pyfastnet
6
+ Author: Alex Salmon
7
+ Author-email: alex@ivila.net
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: author
15
+ Dynamic: author-email
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: license-file
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
24
+ # pyfastnet
25
+
26
+ Python library for decoding the FastNet protocol used by B&G Hydra/H2000 instruments. Developed for personal use and published for general interest.
27
+
28
+ ## Purpose
29
+
30
+ Feed a raw byte stream from a FastNet bus into the library. It handles synchronisation, checksum validation, and decoding — returning structured instrument data ready for further processing.
31
+
32
+ ## Installation
33
+
34
+ ```
35
+ pip install pyfastnet
36
+ ```
37
+
38
+ ## Example usage
39
+
40
+ ```python
41
+ #!/usr/bin/env python3
42
+ import serial
43
+ from fastnet_decoder import FrameBuffer
44
+
45
+ fb = FrameBuffer()
46
+
47
+ ser = serial.Serial(
48
+ port="/dev/ttyUSB0",
49
+ baudrate=28800,
50
+ bytesize=serial.EIGHTBITS,
51
+ stopbits=serial.STOPBITS_TWO,
52
+ parity=serial.PARITY_ODD,
53
+ timeout=0.1,
54
+ )
55
+
56
+ try:
57
+ while True:
58
+ data = ser.read(256)
59
+ if not data:
60
+ continue
61
+ fb.add_to_buffer(data)
62
+ fb.get_complete_frames()
63
+ while not fb.frame_queue.empty():
64
+ frame = fb.frame_queue.get()
65
+ for channel, decoded in frame["values"].items():
66
+ print(channel, decoded)
67
+ finally:
68
+ ser.close()
69
+ ```
70
+
71
+ ## Output format
72
+
73
+ Each decoded frame is a dict with `to_address`, `from_address`, `command`, and `values`. Each entry in `values` is keyed by channel name:
74
+
75
+ ```python
76
+ {
77
+ "to_address": "Entire System",
78
+ "from_address": "Normal CPU (Wind Board in H2000)",
79
+ "command": "Broadcast",
80
+ "values": {
81
+ "Apparent Wind Speed (Knots)": {
82
+ "channel_id": "0x4D",
83
+ "value": 7.0,
84
+ "display_text": "7.0",
85
+ "layout": None,
86
+ },
87
+ "Apparent Wind Angle": {
88
+ "channel_id": "0x51",
89
+ "value": -6.0,
90
+ "display_text": "-6.0",
91
+ "layout": "-[data]",
92
+ },
93
+ "True Wind Direction": {
94
+ "channel_id": "0x6D",
95
+ "value": 213.0,
96
+ "display_text": "213.0°M",
97
+ "layout": "°M",
98
+ },
99
+ }
100
+ }
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
+
119
+ ### Layout field
120
+
121
+ The `layout` field describes the indicator symbol shown on the physical display around the numeric value:
122
+
123
+ | `layout` | Meaning | Sign |
124
+ |---|---|---|
125
+ | `None` | No indicator symbol | positive |
126
+ | `"[data]="` | `=` after value (starboard) | positive |
127
+ | `"=[data]"` | `=` before value (port) | negative |
128
+ | `"[data]-"` | `-` after value (starboard) | positive |
129
+ | `"-[data]"` | `-` before value (port) | negative |
130
+ | `"H[data]"` | `H` prefix — heading | positive |
131
+ | `"°M"` | Magnetic bearing suffix | positive |
132
+ | `"u[data]"` | `u` prefix — upwind (VMG) | positive |
133
+ | `"d[data]"` | `d` prefix — downwind (VMG) | positive |
134
+ | `"L[data]"` | `L` before — leeway port | negative |
135
+ | `"[data]L"` | `L` after — AP compass target | positive |
136
+ | `"[data]°C"` | Celsius suffix | positive |
137
+ | `"[data]°F"` | Fahrenheit suffix | positive |
138
+ | `"[data]z"` / `"z[data]"` | Dog-leg symbol — AP off course | positive |
139
+ | `"TBC"` | Symbol seen but not yet identified | positive |
140
+
141
+ ## Debug API
142
+
143
+ ```python
144
+ from fastnet_decoder import set_log_level
145
+ import logging
146
+ set_log_level(logging.DEBUG)
147
+ ```
148
+
149
+ ```python
150
+ fb.get_buffer_size() # bytes currently in buffer
151
+ fb.get_buffer_contents() # hex string of buffer contents
152
+ ```
153
+
154
+ ## Companion apps
155
+
156
+ - [fastnet2ip](https://github.com/ghotihook/fastnet2ip) — reads FastNet from serial, broadcasts NMEA 0183 via UDP
157
+ - [fastnet2ip_n2k](https://github.com/ghotihook/fastnet2ip_n2k) — reads FastNet from serial, broadcasts NMEA 2000 via UDP
158
+
159
+ Both run on Raspberry Pi, macOS, or Linux.
160
+
161
+ ## Acknowledgments
162
+
163
+ - [trlafleur](https://github.com/trlafleur) — background research
164
+ - [Oppedijk](https://www.oppedijk.com/bandg/fastnet.html) — protocol documentation
165
+ - [timmathews](https://github.com/timmathews/bg-fastnet-driver) — C++ reference implementation
@@ -0,0 +1,142 @@
1
+ # pyfastnet
2
+
3
+ Python library for decoding the FastNet protocol used by B&G Hydra/H2000 instruments. Developed for personal use and published for general interest.
4
+
5
+ ## Purpose
6
+
7
+ Feed a raw byte stream from a FastNet bus into the library. It handles synchronisation, checksum validation, and decoding — returning structured instrument data ready for further processing.
8
+
9
+ ## Installation
10
+
11
+ ```
12
+ pip install pyfastnet
13
+ ```
14
+
15
+ ## Example usage
16
+
17
+ ```python
18
+ #!/usr/bin/env python3
19
+ import serial
20
+ from fastnet_decoder import FrameBuffer
21
+
22
+ fb = FrameBuffer()
23
+
24
+ ser = serial.Serial(
25
+ port="/dev/ttyUSB0",
26
+ baudrate=28800,
27
+ bytesize=serial.EIGHTBITS,
28
+ stopbits=serial.STOPBITS_TWO,
29
+ parity=serial.PARITY_ODD,
30
+ timeout=0.1,
31
+ )
32
+
33
+ try:
34
+ while True:
35
+ data = ser.read(256)
36
+ if not data:
37
+ continue
38
+ fb.add_to_buffer(data)
39
+ fb.get_complete_frames()
40
+ while not fb.frame_queue.empty():
41
+ frame = fb.frame_queue.get()
42
+ for channel, decoded in frame["values"].items():
43
+ print(channel, decoded)
44
+ finally:
45
+ ser.close()
46
+ ```
47
+
48
+ ## Output format
49
+
50
+ Each decoded frame is a dict with `to_address`, `from_address`, `command`, and `values`. Each entry in `values` is keyed by channel name:
51
+
52
+ ```python
53
+ {
54
+ "to_address": "Entire System",
55
+ "from_address": "Normal CPU (Wind Board in H2000)",
56
+ "command": "Broadcast",
57
+ "values": {
58
+ "Apparent Wind Speed (Knots)": {
59
+ "channel_id": "0x4D",
60
+ "value": 7.0,
61
+ "display_text": "7.0",
62
+ "layout": None,
63
+ },
64
+ "Apparent Wind Angle": {
65
+ "channel_id": "0x51",
66
+ "value": -6.0,
67
+ "display_text": "-6.0",
68
+ "layout": "-[data]",
69
+ },
70
+ "True Wind Direction": {
71
+ "channel_id": "0x6D",
72
+ "value": 213.0,
73
+ "display_text": "213.0°M",
74
+ "layout": "°M",
75
+ },
76
+ }
77
+ }
78
+ ```
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
+
96
+ ### Layout field
97
+
98
+ The `layout` field describes the indicator symbol shown on the physical display around the numeric value:
99
+
100
+ | `layout` | Meaning | Sign |
101
+ |---|---|---|
102
+ | `None` | No indicator symbol | positive |
103
+ | `"[data]="` | `=` after value (starboard) | positive |
104
+ | `"=[data]"` | `=` before value (port) | negative |
105
+ | `"[data]-"` | `-` after value (starboard) | positive |
106
+ | `"-[data]"` | `-` before value (port) | negative |
107
+ | `"H[data]"` | `H` prefix — heading | positive |
108
+ | `"°M"` | Magnetic bearing suffix | positive |
109
+ | `"u[data]"` | `u` prefix — upwind (VMG) | positive |
110
+ | `"d[data]"` | `d` prefix — downwind (VMG) | positive |
111
+ | `"L[data]"` | `L` before — leeway port | negative |
112
+ | `"[data]L"` | `L` after — AP compass target | positive |
113
+ | `"[data]°C"` | Celsius suffix | positive |
114
+ | `"[data]°F"` | Fahrenheit suffix | positive |
115
+ | `"[data]z"` / `"z[data]"` | Dog-leg symbol — AP off course | positive |
116
+ | `"TBC"` | Symbol seen but not yet identified | positive |
117
+
118
+ ## Debug API
119
+
120
+ ```python
121
+ from fastnet_decoder import set_log_level
122
+ import logging
123
+ set_log_level(logging.DEBUG)
124
+ ```
125
+
126
+ ```python
127
+ fb.get_buffer_size() # bytes currently in buffer
128
+ fb.get_buffer_contents() # hex string of buffer contents
129
+ ```
130
+
131
+ ## Companion apps
132
+
133
+ - [fastnet2ip](https://github.com/ghotihook/fastnet2ip) — reads FastNet from serial, broadcasts NMEA 0183 via UDP
134
+ - [fastnet2ip_n2k](https://github.com/ghotihook/fastnet2ip_n2k) — reads FastNet from serial, broadcasts NMEA 2000 via UDP
135
+
136
+ Both run on Raspberry Pi, macOS, or Linux.
137
+
138
+ ## Acknowledgments
139
+
140
+ - [trlafleur](https://github.com/trlafleur) — background research
141
+ - [Oppedijk](https://www.oppedijk.com/bandg/fastnet.html) — protocol documentation
142
+ - [timmathews](https://github.com/timmathews/bg-fastnet-driver) — C++ reference implementation
@@ -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})",
@@ -211,7 +215,7 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
211
215
  if len(data_bytes) != 4:
212
216
  return None
213
217
  value = None
214
- display_text = "".join(SEGMENT_B.get(b, "?") for b in data_bytes)
218
+ display_text = "".join(SEGMENT_B.get(b, "TBC") for b in data_bytes)
215
219
 
216
220
  elif format_bits == 0x07:
217
221
  if len(data_bytes) != 4:
@@ -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)",
@@ -0,0 +1,11 @@
1
+
2
+
3
+ def calculate_checksum(data):
4
+ """
5
+ Calculates the checksum for the given data bytes this one is for Fastnet Checksum
6
+ Args:
7
+ data (bytes): The data bytes to calculate checksum for.
8
+ Returns:
9
+ int: The calculated checksum.
10
+ """
11
+ return (-sum(data)) & 0xFF
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyfastnet
3
+ Version: 2.0.14
4
+ Summary: A Python library for decoding FastNet protocol data streams.
5
+ Home-page: https://github.com/ghotihook/pyfastnet
6
+ Author: Alex Salmon
7
+ Author-email: alex@ivila.net
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: author
15
+ Dynamic: author-email
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: license-file
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
24
+ # pyfastnet
25
+
26
+ Python library for decoding the FastNet protocol used by B&G Hydra/H2000 instruments. Developed for personal use and published for general interest.
27
+
28
+ ## Purpose
29
+
30
+ Feed a raw byte stream from a FastNet bus into the library. It handles synchronisation, checksum validation, and decoding — returning structured instrument data ready for further processing.
31
+
32
+ ## Installation
33
+
34
+ ```
35
+ pip install pyfastnet
36
+ ```
37
+
38
+ ## Example usage
39
+
40
+ ```python
41
+ #!/usr/bin/env python3
42
+ import serial
43
+ from fastnet_decoder import FrameBuffer
44
+
45
+ fb = FrameBuffer()
46
+
47
+ ser = serial.Serial(
48
+ port="/dev/ttyUSB0",
49
+ baudrate=28800,
50
+ bytesize=serial.EIGHTBITS,
51
+ stopbits=serial.STOPBITS_TWO,
52
+ parity=serial.PARITY_ODD,
53
+ timeout=0.1,
54
+ )
55
+
56
+ try:
57
+ while True:
58
+ data = ser.read(256)
59
+ if not data:
60
+ continue
61
+ fb.add_to_buffer(data)
62
+ fb.get_complete_frames()
63
+ while not fb.frame_queue.empty():
64
+ frame = fb.frame_queue.get()
65
+ for channel, decoded in frame["values"].items():
66
+ print(channel, decoded)
67
+ finally:
68
+ ser.close()
69
+ ```
70
+
71
+ ## Output format
72
+
73
+ Each decoded frame is a dict with `to_address`, `from_address`, `command`, and `values`. Each entry in `values` is keyed by channel name:
74
+
75
+ ```python
76
+ {
77
+ "to_address": "Entire System",
78
+ "from_address": "Normal CPU (Wind Board in H2000)",
79
+ "command": "Broadcast",
80
+ "values": {
81
+ "Apparent Wind Speed (Knots)": {
82
+ "channel_id": "0x4D",
83
+ "value": 7.0,
84
+ "display_text": "7.0",
85
+ "layout": None,
86
+ },
87
+ "Apparent Wind Angle": {
88
+ "channel_id": "0x51",
89
+ "value": -6.0,
90
+ "display_text": "-6.0",
91
+ "layout": "-[data]",
92
+ },
93
+ "True Wind Direction": {
94
+ "channel_id": "0x6D",
95
+ "value": 213.0,
96
+ "display_text": "213.0°M",
97
+ "layout": "°M",
98
+ },
99
+ }
100
+ }
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
+
119
+ ### Layout field
120
+
121
+ The `layout` field describes the indicator symbol shown on the physical display around the numeric value:
122
+
123
+ | `layout` | Meaning | Sign |
124
+ |---|---|---|
125
+ | `None` | No indicator symbol | positive |
126
+ | `"[data]="` | `=` after value (starboard) | positive |
127
+ | `"=[data]"` | `=` before value (port) | negative |
128
+ | `"[data]-"` | `-` after value (starboard) | positive |
129
+ | `"-[data]"` | `-` before value (port) | negative |
130
+ | `"H[data]"` | `H` prefix — heading | positive |
131
+ | `"°M"` | Magnetic bearing suffix | positive |
132
+ | `"u[data]"` | `u` prefix — upwind (VMG) | positive |
133
+ | `"d[data]"` | `d` prefix — downwind (VMG) | positive |
134
+ | `"L[data]"` | `L` before — leeway port | negative |
135
+ | `"[data]L"` | `L` after — AP compass target | positive |
136
+ | `"[data]°C"` | Celsius suffix | positive |
137
+ | `"[data]°F"` | Fahrenheit suffix | positive |
138
+ | `"[data]z"` / `"z[data]"` | Dog-leg symbol — AP off course | positive |
139
+ | `"TBC"` | Symbol seen but not yet identified | positive |
140
+
141
+ ## Debug API
142
+
143
+ ```python
144
+ from fastnet_decoder import set_log_level
145
+ import logging
146
+ set_log_level(logging.DEBUG)
147
+ ```
148
+
149
+ ```python
150
+ fb.get_buffer_size() # bytes currently in buffer
151
+ fb.get_buffer_contents() # hex string of buffer contents
152
+ ```
153
+
154
+ ## Companion apps
155
+
156
+ - [fastnet2ip](https://github.com/ghotihook/fastnet2ip) — reads FastNet from serial, broadcasts NMEA 0183 via UDP
157
+ - [fastnet2ip_n2k](https://github.com/ghotihook/fastnet2ip_n2k) — reads FastNet from serial, broadcasts NMEA 2000 via UDP
158
+
159
+ Both run on Raspberry Pi, macOS, or Linux.
160
+
161
+ ## Acknowledgments
162
+
163
+ - [trlafleur](https://github.com/trlafleur) — background research
164
+ - [Oppedijk](https://www.oppedijk.com/bandg/fastnet.html) — protocol documentation
165
+ - [timmathews](https://github.com/timmathews/bg-fastnet-driver) — C++ reference implementation
@@ -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.12", # 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()
pyfastnet-2.0.12/PKG-INFO DELETED
@@ -1,134 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: pyfastnet
3
- Version: 2.0.12
4
- Summary: A Python library for decoding FastNet protocol data streams.
5
- Home-page: https://github.com/ghotihook/pyfastnet
6
- Author: Alex Salmon
7
- Author-email: alex@ivila.net
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.7
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
- Dynamic: author
15
- Dynamic: author-email
16
- Dynamic: classifier
17
- Dynamic: description
18
- Dynamic: description-content-type
19
- Dynamic: home-page
20
- Dynamic: requires-python
21
- Dynamic: summary
22
-
23
- # pyfastnet
24
- Fastnet is the propriatory protocol used by B&G on some older instruments, tested on Hydra/H2000. It might work on other systems. I developed this for personal use and publishing for general interest only.
25
-
26
- # Purpose
27
- This library can be fed a stream of fastnet data, it will decode and return structured instrument data for further processing. Syncronisation, checksum and decoding is handled by the library.
28
-
29
- # Companion App
30
- - A full implementation can be found here, it takes input from a serial port or dummy file and broadcasts NMEA messages via UDP [fastnet2ip](https://github.com/ghotihook/fastnet2ip) Easy to install on a raspberry pi, core mp135, mac, linux.
31
-
32
- # Example input/output
33
- Byte string from Fastnet including "ff051801e34e0a02c402754d6100464f610024520af683f6835113a0064b"
34
-
35
- to_address: Entire System
36
- from_address: Normal CPU (Wind Board in H2000)
37
- command: Broadcast
38
- values:
39
- ```
40
- {
41
- 'Apparent Wind Speed (Raw)': {'channel_id': '0x4E', 'format_byte': '0x0A', 'data_bytes': '02c40275', 'divisor': 1, 'digits': 1, 'format_bits': 10, 'raw': {'first': 708.0, 'second': 629.0}, 'interpreted': 708.0},
42
-
43
- 'Apparent Wind Speed (Knots)': {'channel_id': '0x4D', 'format_byte': '0x61', 'data_bytes': '0046', 'divisor': 10, 'digits': 3, 'format_bits': 1, 'raw': 70, 'interpreted': 7.0},
44
-
45
- 'Apparent Wind Speed (m/s)': {'channel_id': '0x4F', 'format_byte': '0x61', 'data_bytes': '0024', 'divisor': 10, 'digits': 3, 'format_bits': 1, 'raw': 36, 'interpreted': 3.6},
46
-
47
- 'Apparent Wind Angle (Raw)': {'channel_id': '0x52', 'format_byte': '0x0A', 'data_bytes': 'f683f683', 'divisor': 1, 'digits': 1, 'format_bits': 10, 'raw': {'first': -2429.0, 'second': -2429.0}, 'interpreted': -2429.0},
48
-
49
- 'Apparent Wind Angle': {'channel_id': '0x51', 'format_byte': '0x13', 'data_bytes': 'a006', 'divisor': 1, 'digits': 2, 'format_bits': 3, 'raw': {'segment_code': '0xa0', 'segment_code_bin': '0b10100000', 'unsigned_value': 6, 'layout': '-[data]'}, 'interpreted': -6.0}}
50
- ```
51
-
52
- # Example implementation
53
- ```
54
- #!/usr/bin/env python3
55
- import serial
56
- import time
57
- from pprint import pprint
58
- from fastnet_decoder import FrameBuffer
59
-
60
- def main():
61
- fb = FrameBuffer()
62
- # open /dev/ttyUSB0 at 28,800 baud, 8E2, 0.1 s timeout
63
- ser = serial.Serial(
64
- port="/dev/ttyUSB0",
65
- baudrate=28800,
66
- bytesize=serial.EIGHTBITS,
67
- stopbits=serial.STOPBITS_TWO,
68
- parity=serial.PARITY_ODD,
69
- timeout=0.1
70
- )
71
-
72
- try:
73
- while True:
74
- data = ser.read(256)
75
- if not data:
76
- time.sleep(0.01)
77
- continue
78
-
79
- # 1) feed raw bytes into the frame buffer
80
- fb.add_to_buffer(data)
81
-
82
- # 2) extract & decode any complete frames
83
- fb.get_complete_frames()
84
-
85
- # 3) peek at the entire queue as a list
86
- queue_contents = list(fb.frame_queue.queue)
87
- if queue_contents:
88
- print("Current decoded frames in queue:")
89
- pprint(queue_contents)
90
- else:
91
- print("Queue is empty.")
92
-
93
- # 4) (optionally) drain the queue for processing
94
- while not fb.frame_queue.empty():
95
- frame = fb.frame_queue.get()
96
- # replace this with whatever you need
97
- print("Processing frame:", frame)
98
-
99
- except KeyboardInterrupt:
100
- print("Stopping…")
101
- finally:
102
- ser.close()
103
-
104
- if __name__ == "__main__":
105
- main()
106
- ```
107
-
108
-
109
- # Important library calls - debug
110
- - ```set_log_level(DEBUG)```
111
- - ```fastnetframebuffer.get_buffer_size()```
112
- - ```fastnetframebuffer.get_buffer_contents()```
113
-
114
-
115
-
116
- # Installation
117
- ```pip3 install pyfastnet```
118
-
119
- On a raspberry pi and some other systems this is done from with a virtual env
120
-
121
- ```python -m venv --system-site-packages ~/python_environment
122
- source ~/python_environment/bin/activate
123
- pip3 install pyfastnet
124
- deactivate
125
- ~/python_environment/bin/python3 pyfastnet.py -h
126
- ```
127
-
128
-
129
- ## Acknowledgments / References
130
-
131
- - [trlafleur - Collector of significant background](https://github.com/trlafleur)
132
- - [Oppedijk - Background and patches](https://www.oppedijk.com/bandg/fastnet.html)
133
- - [timmathews - Significant implementation in Cpp](https://github.com/timmathews/bg-fastnet-driver)
134
- - Significant help from chatGPT!
@@ -1,112 +0,0 @@
1
- # pyfastnet
2
- Fastnet is the propriatory protocol used by B&G on some older instruments, tested on Hydra/H2000. It might work on other systems. I developed this for personal use and publishing for general interest only.
3
-
4
- # Purpose
5
- This library can be fed a stream of fastnet data, it will decode and return structured instrument data for further processing. Syncronisation, checksum and decoding is handled by the library.
6
-
7
- # Companion App
8
- - A full implementation can be found here, it takes input from a serial port or dummy file and broadcasts NMEA messages via UDP [fastnet2ip](https://github.com/ghotihook/fastnet2ip) Easy to install on a raspberry pi, core mp135, mac, linux.
9
-
10
- # Example input/output
11
- Byte string from Fastnet including "ff051801e34e0a02c402754d6100464f610024520af683f6835113a0064b"
12
-
13
- to_address: Entire System
14
- from_address: Normal CPU (Wind Board in H2000)
15
- command: Broadcast
16
- values:
17
- ```
18
- {
19
- 'Apparent Wind Speed (Raw)': {'channel_id': '0x4E', 'format_byte': '0x0A', 'data_bytes': '02c40275', 'divisor': 1, 'digits': 1, 'format_bits': 10, 'raw': {'first': 708.0, 'second': 629.0}, 'interpreted': 708.0},
20
-
21
- 'Apparent Wind Speed (Knots)': {'channel_id': '0x4D', 'format_byte': '0x61', 'data_bytes': '0046', 'divisor': 10, 'digits': 3, 'format_bits': 1, 'raw': 70, 'interpreted': 7.0},
22
-
23
- 'Apparent Wind Speed (m/s)': {'channel_id': '0x4F', 'format_byte': '0x61', 'data_bytes': '0024', 'divisor': 10, 'digits': 3, 'format_bits': 1, 'raw': 36, 'interpreted': 3.6},
24
-
25
- 'Apparent Wind Angle (Raw)': {'channel_id': '0x52', 'format_byte': '0x0A', 'data_bytes': 'f683f683', 'divisor': 1, 'digits': 1, 'format_bits': 10, 'raw': {'first': -2429.0, 'second': -2429.0}, 'interpreted': -2429.0},
26
-
27
- 'Apparent Wind Angle': {'channel_id': '0x51', 'format_byte': '0x13', 'data_bytes': 'a006', 'divisor': 1, 'digits': 2, 'format_bits': 3, 'raw': {'segment_code': '0xa0', 'segment_code_bin': '0b10100000', 'unsigned_value': 6, 'layout': '-[data]'}, 'interpreted': -6.0}}
28
- ```
29
-
30
- # Example implementation
31
- ```
32
- #!/usr/bin/env python3
33
- import serial
34
- import time
35
- from pprint import pprint
36
- from fastnet_decoder import FrameBuffer
37
-
38
- def main():
39
- fb = FrameBuffer()
40
- # open /dev/ttyUSB0 at 28,800 baud, 8E2, 0.1 s timeout
41
- ser = serial.Serial(
42
- port="/dev/ttyUSB0",
43
- baudrate=28800,
44
- bytesize=serial.EIGHTBITS,
45
- stopbits=serial.STOPBITS_TWO,
46
- parity=serial.PARITY_ODD,
47
- timeout=0.1
48
- )
49
-
50
- try:
51
- while True:
52
- data = ser.read(256)
53
- if not data:
54
- time.sleep(0.01)
55
- continue
56
-
57
- # 1) feed raw bytes into the frame buffer
58
- fb.add_to_buffer(data)
59
-
60
- # 2) extract & decode any complete frames
61
- fb.get_complete_frames()
62
-
63
- # 3) peek at the entire queue as a list
64
- queue_contents = list(fb.frame_queue.queue)
65
- if queue_contents:
66
- print("Current decoded frames in queue:")
67
- pprint(queue_contents)
68
- else:
69
- print("Queue is empty.")
70
-
71
- # 4) (optionally) drain the queue for processing
72
- while not fb.frame_queue.empty():
73
- frame = fb.frame_queue.get()
74
- # replace this with whatever you need
75
- print("Processing frame:", frame)
76
-
77
- except KeyboardInterrupt:
78
- print("Stopping…")
79
- finally:
80
- ser.close()
81
-
82
- if __name__ == "__main__":
83
- main()
84
- ```
85
-
86
-
87
- # Important library calls - debug
88
- - ```set_log_level(DEBUG)```
89
- - ```fastnetframebuffer.get_buffer_size()```
90
- - ```fastnetframebuffer.get_buffer_contents()```
91
-
92
-
93
-
94
- # Installation
95
- ```pip3 install pyfastnet```
96
-
97
- On a raspberry pi and some other systems this is done from with a virtual env
98
-
99
- ```python -m venv --system-site-packages ~/python_environment
100
- source ~/python_environment/bin/activate
101
- pip3 install pyfastnet
102
- deactivate
103
- ~/python_environment/bin/python3 pyfastnet.py -h
104
- ```
105
-
106
-
107
- ## Acknowledgments / References
108
-
109
- - [trlafleur - Collector of significant background](https://github.com/trlafleur)
110
- - [Oppedijk - Background and patches](https://www.oppedijk.com/bandg/fastnet.html)
111
- - [timmathews - Significant implementation in Cpp](https://github.com/timmathews/bg-fastnet-driver)
112
- - Significant help from chatGPT!
@@ -1,48 +0,0 @@
1
-
2
-
3
- def calculate_checksum(data):
4
- """
5
- Calculates the checksum for the given data bytes this one is for Fastnet Checksum
6
- Args:
7
- data (bytes): The data bytes to calculate checksum for.
8
- Returns:
9
- int: The calculated checksum.
10
- """
11
- return (-sum(data)) & 0xFF
12
-
13
-
14
- def calculate_nmea_checksum(sentence):
15
- """
16
- Calculates the NMEA checksum for a given sentence (excluding '$' and '*').
17
- Args:
18
- sentence (str): NMEA sentence string without '$' and checksum '*'.
19
- Returns:
20
- str: Hexadecimal checksum as a string.
21
- """
22
- checksum = 0
23
- for char in sentence:
24
- checksum ^= ord(char)
25
- return f"{checksum:02X}"
26
-
27
-
28
-
29
- def parse_format_byte(format_byte):
30
- """
31
- Parses the format byte into divisor, digits, and format type.
32
- Args:
33
- format_byte (int): The format byte.
34
- Returns:
35
- dict: Parsed divisor, digits, and format type.
36
- """
37
- divisor_bits = (format_byte >> 6) & 0b11 # First two bits
38
- digits_bits = (format_byte >> 4) & 0b11 # Next two bits
39
- format_type = format_byte & 0x0F # Last 4 bits (format type)
40
-
41
- divisor_map = {0b00: 1, 0b01: 10, 0b10: 100, 0b11: 1000}
42
- digits_map = {0b00: 1, 0b01: 2, 0b10: 3, 0b11: 4}
43
-
44
- return {
45
- "divisor": divisor_map.get(divisor_bits, 1),
46
- "digits": digits_map.get(digits_bits, 1),
47
- "format_type": format_type,
48
- }
@@ -1,134 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: pyfastnet
3
- Version: 2.0.12
4
- Summary: A Python library for decoding FastNet protocol data streams.
5
- Home-page: https://github.com/ghotihook/pyfastnet
6
- Author: Alex Salmon
7
- Author-email: alex@ivila.net
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.7
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
- Dynamic: author
15
- Dynamic: author-email
16
- Dynamic: classifier
17
- Dynamic: description
18
- Dynamic: description-content-type
19
- Dynamic: home-page
20
- Dynamic: requires-python
21
- Dynamic: summary
22
-
23
- # pyfastnet
24
- Fastnet is the propriatory protocol used by B&G on some older instruments, tested on Hydra/H2000. It might work on other systems. I developed this for personal use and publishing for general interest only.
25
-
26
- # Purpose
27
- This library can be fed a stream of fastnet data, it will decode and return structured instrument data for further processing. Syncronisation, checksum and decoding is handled by the library.
28
-
29
- # Companion App
30
- - A full implementation can be found here, it takes input from a serial port or dummy file and broadcasts NMEA messages via UDP [fastnet2ip](https://github.com/ghotihook/fastnet2ip) Easy to install on a raspberry pi, core mp135, mac, linux.
31
-
32
- # Example input/output
33
- Byte string from Fastnet including "ff051801e34e0a02c402754d6100464f610024520af683f6835113a0064b"
34
-
35
- to_address: Entire System
36
- from_address: Normal CPU (Wind Board in H2000)
37
- command: Broadcast
38
- values:
39
- ```
40
- {
41
- 'Apparent Wind Speed (Raw)': {'channel_id': '0x4E', 'format_byte': '0x0A', 'data_bytes': '02c40275', 'divisor': 1, 'digits': 1, 'format_bits': 10, 'raw': {'first': 708.0, 'second': 629.0}, 'interpreted': 708.0},
42
-
43
- 'Apparent Wind Speed (Knots)': {'channel_id': '0x4D', 'format_byte': '0x61', 'data_bytes': '0046', 'divisor': 10, 'digits': 3, 'format_bits': 1, 'raw': 70, 'interpreted': 7.0},
44
-
45
- 'Apparent Wind Speed (m/s)': {'channel_id': '0x4F', 'format_byte': '0x61', 'data_bytes': '0024', 'divisor': 10, 'digits': 3, 'format_bits': 1, 'raw': 36, 'interpreted': 3.6},
46
-
47
- 'Apparent Wind Angle (Raw)': {'channel_id': '0x52', 'format_byte': '0x0A', 'data_bytes': 'f683f683', 'divisor': 1, 'digits': 1, 'format_bits': 10, 'raw': {'first': -2429.0, 'second': -2429.0}, 'interpreted': -2429.0},
48
-
49
- 'Apparent Wind Angle': {'channel_id': '0x51', 'format_byte': '0x13', 'data_bytes': 'a006', 'divisor': 1, 'digits': 2, 'format_bits': 3, 'raw': {'segment_code': '0xa0', 'segment_code_bin': '0b10100000', 'unsigned_value': 6, 'layout': '-[data]'}, 'interpreted': -6.0}}
50
- ```
51
-
52
- # Example implementation
53
- ```
54
- #!/usr/bin/env python3
55
- import serial
56
- import time
57
- from pprint import pprint
58
- from fastnet_decoder import FrameBuffer
59
-
60
- def main():
61
- fb = FrameBuffer()
62
- # open /dev/ttyUSB0 at 28,800 baud, 8E2, 0.1 s timeout
63
- ser = serial.Serial(
64
- port="/dev/ttyUSB0",
65
- baudrate=28800,
66
- bytesize=serial.EIGHTBITS,
67
- stopbits=serial.STOPBITS_TWO,
68
- parity=serial.PARITY_ODD,
69
- timeout=0.1
70
- )
71
-
72
- try:
73
- while True:
74
- data = ser.read(256)
75
- if not data:
76
- time.sleep(0.01)
77
- continue
78
-
79
- # 1) feed raw bytes into the frame buffer
80
- fb.add_to_buffer(data)
81
-
82
- # 2) extract & decode any complete frames
83
- fb.get_complete_frames()
84
-
85
- # 3) peek at the entire queue as a list
86
- queue_contents = list(fb.frame_queue.queue)
87
- if queue_contents:
88
- print("Current decoded frames in queue:")
89
- pprint(queue_contents)
90
- else:
91
- print("Queue is empty.")
92
-
93
- # 4) (optionally) drain the queue for processing
94
- while not fb.frame_queue.empty():
95
- frame = fb.frame_queue.get()
96
- # replace this with whatever you need
97
- print("Processing frame:", frame)
98
-
99
- except KeyboardInterrupt:
100
- print("Stopping…")
101
- finally:
102
- ser.close()
103
-
104
- if __name__ == "__main__":
105
- main()
106
- ```
107
-
108
-
109
- # Important library calls - debug
110
- - ```set_log_level(DEBUG)```
111
- - ```fastnetframebuffer.get_buffer_size()```
112
- - ```fastnetframebuffer.get_buffer_contents()```
113
-
114
-
115
-
116
- # Installation
117
- ```pip3 install pyfastnet```
118
-
119
- On a raspberry pi and some other systems this is done from with a virtual env
120
-
121
- ```python -m venv --system-site-packages ~/python_environment
122
- source ~/python_environment/bin/activate
123
- pip3 install pyfastnet
124
- deactivate
125
- ~/python_environment/bin/python3 pyfastnet.py -h
126
- ```
127
-
128
-
129
- ## Acknowledgments / References
130
-
131
- - [trlafleur - Collector of significant background](https://github.com/trlafleur)
132
- - [Oppedijk - Background and patches](https://www.oppedijk.com/bandg/fastnet.html)
133
- - [timmathews - Significant implementation in Cpp](https://github.com/timmathews/bg-fastnet-driver)
134
- - Significant help from chatGPT!
File without changes
File without changes
File without changes