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.
- pyfastnet-2.0.14/PKG-INFO +165 -0
- pyfastnet-2.0.14/README.md +142 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/decode_fastnet.py +9 -5
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/mappings.py +0 -1
- pyfastnet-2.0.14/fastnet_decoder/utils.py +11 -0
- pyfastnet-2.0.14/pyfastnet.egg-info/PKG-INFO +165 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/pyfastnet.egg-info/SOURCES.txt +1 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/setup.py +1 -1
- pyfastnet-2.0.14/tests/test_latlon_frame.py +56 -0
- pyfastnet-2.0.12/PKG-INFO +0 -134
- pyfastnet-2.0.12/README.md +0 -112
- pyfastnet-2.0.12/fastnet_decoder/utils.py +0 -48
- pyfastnet-2.0.12/pyfastnet.egg-info/PKG-INFO +0 -134
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/LICENSE +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/MANIFEST.in +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/__init__.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/frame_buffer.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/fastnet_decoder/logger.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/pyfastnet.egg-info/dependency_links.txt +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/pyfastnet.egg-info/top_level.txt +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/setup.cfg +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_apparent_frame.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_autopilot_frame.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_channels.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_depth_frame.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_format07_msb_regression.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_format08_layout.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_heel_frame.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_rudder_frame.py +0 -0
- {pyfastnet-2.0.12 → pyfastnet-2.0.14}/tests/test_tide_frame.py +0 -0
- {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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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, "
|
|
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:
|
|
@@ -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
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="pyfastnet",
|
|
5
|
-
version="2.0.
|
|
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!
|
pyfastnet-2.0.12/README.md
DELETED
|
@@ -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
|
|
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
|