pyfastnet 2.0.2__tar.gz → 2.0.4__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.2/pyfastnet.egg-info → pyfastnet-2.0.4}/PKG-INFO +1 -1
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/decode_fastnet.py +63 -47
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/frame_buffer.py +23 -15
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/mappings.py +1 -1
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/utils.py +1 -1
- {pyfastnet-2.0.2 → pyfastnet-2.0.4/pyfastnet.egg-info}/PKG-INFO +1 -1
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/setup.py +1 -1
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/LICENSE +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/MANIFEST.in +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/README.md +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/__init__.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/logger.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/pyfastnet.egg-info/SOURCES.txt +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/pyfastnet.egg-info/dependency_links.txt +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/pyfastnet.egg-info/top_level.txt +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/setup.cfg +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_apparent_frame.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_autopilot_frame.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_channels.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_depth_frame.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_format07_msb_regression.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_format08_layout.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_heel_frame.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_rudder_frame.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_tide_frame.py +0 -0
- {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_true_frame.py +0 -0
|
@@ -1,53 +1,54 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
from .utils import calculate_checksum
|
|
3
2
|
from .mappings import ADDRESS_LOOKUP, COMMAND_LOOKUP, CHANNEL_LOOKUP, FORMAT_SIZE_MAP
|
|
4
3
|
from .mappings import SEGMENT_A, SEGMENT_B, AUTOPILOT_MODES
|
|
5
4
|
from .logger import logger
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
_DIVISOR_MAP = {0b00: 1, 0b01: 10, 0b10: 100, 0b11: 1000}
|
|
9
|
-
|
|
8
|
+
_DECIMAL_PLACES_MAP = {1: 0, 10: 1, 100: 2, 1000: 3}
|
|
10
9
|
|
|
10
|
+
# Layout strings describe how the display reads around the numeric value.
|
|
11
|
+
# "[data]" is a placeholder for the number. Examples:
|
|
12
|
+
# "[data]-" → value is positive, trailing minus means port/starboard convention
|
|
13
|
+
# "-[data]" → value is negative (subtract sign)
|
|
14
|
+
# "=[data]" → value is negative (equals sign variant)
|
|
15
|
+
# "H[data]" → "H" prefix, e.g. H045 for a heading
|
|
16
|
+
# "°M" → magnetic bearing, suffix added after value
|
|
11
17
|
|
|
12
18
|
def _sign_from_layout(layout: str) -> int:
|
|
13
19
|
return -1 if layout in ("-[data]", "=[data]") else 1
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
def _display_from_layout(layout: str, formatted: str) -> str:
|
|
17
|
-
if layout == "°M":
|
|
18
|
-
if layout == "H[data]":
|
|
19
|
-
if layout == "[data]H":
|
|
20
|
-
if layout
|
|
21
|
-
|
|
23
|
+
if layout == "°M": return f"{formatted}°M"
|
|
24
|
+
if layout == "H[data]": return f"H{formatted}"
|
|
25
|
+
if layout == "[data]H": return f"{formatted}H"
|
|
26
|
+
if layout == "[data]=": return f"{formatted}="
|
|
27
|
+
if layout == "[data]-": return f"{formatted}-"
|
|
22
28
|
return formatted
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
def decode_frame(frame: bytes) -> dict:
|
|
26
32
|
try:
|
|
27
|
-
to_address
|
|
28
|
-
from_address
|
|
29
|
-
body_size
|
|
30
|
-
command
|
|
31
|
-
|
|
32
|
-
body
|
|
33
|
-
body_checksum = frame[-1]
|
|
34
|
-
|
|
35
|
-
if calculate_checksum(frame[:4]) != header_checksum:
|
|
36
|
-
logger.debug(f"FRAME discard header-checksum [{frame.hex()[:16]}...]")
|
|
37
|
-
return {"error": "Header checksum mismatch"}
|
|
38
|
-
|
|
39
|
-
if calculate_checksum(body) != body_checksum:
|
|
40
|
-
logger.debug(f"FRAME discard body-checksum [{frame.hex()[:16]}...]")
|
|
41
|
-
return {"error": "Body checksum mismatch"}
|
|
33
|
+
to_address = frame[0]
|
|
34
|
+
from_address = frame[1]
|
|
35
|
+
body_size = frame[2]
|
|
36
|
+
command = frame[3]
|
|
37
|
+
# frame[4] is the header checksum — already validated by FrameBuffer before this is called
|
|
38
|
+
body = frame[5:-1]
|
|
42
39
|
|
|
43
40
|
if len(body) < 2 or len(body) != body_size:
|
|
44
41
|
logger.debug(f"FRAME discard body-size expected={body_size} actual={len(body)}")
|
|
45
42
|
return {"error": "Invalid body size"}
|
|
46
43
|
|
|
44
|
+
to_name = ADDRESS_LOOKUP.get(to_address)
|
|
45
|
+
from_name = ADDRESS_LOOKUP.get(from_address)
|
|
46
|
+
cmd_name = COMMAND_LOOKUP.get(command)
|
|
47
|
+
|
|
47
48
|
decoded_data = {
|
|
48
|
-
"to_address":
|
|
49
|
-
"from_address":
|
|
50
|
-
"command":
|
|
49
|
+
"to_address": to_name if to_name is not None else f"Unknown (0x{to_address:02X})",
|
|
50
|
+
"from_address": from_name if from_name is not None else f"Unknown (0x{from_address:02X})",
|
|
51
|
+
"command": cmd_name if cmd_name is not None else f"Unknown (0x{command:02X})",
|
|
51
52
|
"values": {}
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -59,8 +60,10 @@ def decode_frame(frame: bytes) -> dict:
|
|
|
59
60
|
|
|
60
61
|
channel_id = body[index]
|
|
61
62
|
format_byte = body[index + 1]
|
|
62
|
-
channel_name = CHANNEL_LOOKUP.get(channel_id
|
|
63
|
-
|
|
63
|
+
channel_name = CHANNEL_LOOKUP.get(channel_id)
|
|
64
|
+
if channel_name is None:
|
|
65
|
+
channel_name = f"Unknown (0x{channel_id:02X})"
|
|
66
|
+
index += 2
|
|
64
67
|
|
|
65
68
|
data_length = FORMAT_SIZE_MAP.get(format_byte & 0x0F, 0)
|
|
66
69
|
if index + data_length > len(body):
|
|
@@ -73,20 +76,22 @@ def decode_frame(frame: bytes) -> dict:
|
|
|
73
76
|
data_bytes = body[index:index + data_length]
|
|
74
77
|
index += data_length
|
|
75
78
|
|
|
76
|
-
logger.debug(
|
|
77
|
-
f" CH 0x{channel_id:02X} {channel_name} "
|
|
78
|
-
f"fmt=0x{format_byte:02X} data=[{data_bytes.hex()}]"
|
|
79
|
-
)
|
|
80
|
-
|
|
81
79
|
decoded_value = decode_format_and_data(channel_id, format_byte, data_bytes)
|
|
82
80
|
decoded_data["values"][channel_name] = decoded_value
|
|
83
81
|
|
|
84
82
|
if decoded_value:
|
|
85
83
|
logger.debug(
|
|
86
|
-
f"
|
|
84
|
+
f" CH 0x{channel_id:02X} {channel_name} "
|
|
85
|
+
f"fmt=0x{format_byte:02X} data=[{data_bytes.hex()}] "
|
|
86
|
+
f"value={decoded_value['value']} "
|
|
87
87
|
f"display='{decoded_value['display_text']}' "
|
|
88
88
|
f"layout={decoded_value['layout']}"
|
|
89
89
|
)
|
|
90
|
+
else:
|
|
91
|
+
logger.debug(
|
|
92
|
+
f" CH 0x{channel_id:02X} {channel_name} "
|
|
93
|
+
f"fmt=0x{format_byte:02X} data=[{data_bytes.hex()}] (no decode)"
|
|
94
|
+
)
|
|
90
95
|
|
|
91
96
|
return decoded_data
|
|
92
97
|
|
|
@@ -103,8 +108,11 @@ def decode_ascii_frame(frame: bytes) -> dict:
|
|
|
103
108
|
body = frame[5:-1]
|
|
104
109
|
|
|
105
110
|
channel_id = body[0]
|
|
111
|
+
# body[1] is a format byte — not used for ASCII frames
|
|
106
112
|
data_bytes = body[2:]
|
|
107
|
-
channel_name = CHANNEL_LOOKUP.get(channel_id
|
|
113
|
+
channel_name = CHANNEL_LOOKUP.get(channel_id)
|
|
114
|
+
if channel_name is None:
|
|
115
|
+
channel_name = f"Unknown (0x{channel_id:02X})"
|
|
108
116
|
|
|
109
117
|
try:
|
|
110
118
|
ascii_text = data_bytes.decode("ascii").strip()
|
|
@@ -114,10 +122,14 @@ def decode_ascii_frame(frame: bytes) -> dict:
|
|
|
114
122
|
|
|
115
123
|
logger.debug(f" CH 0x{channel_id:02X} {channel_name} ascii='{ascii_text}'")
|
|
116
124
|
|
|
125
|
+
to_name = ADDRESS_LOOKUP.get(to_address)
|
|
126
|
+
from_name = ADDRESS_LOOKUP.get(from_address)
|
|
127
|
+
cmd_name = COMMAND_LOOKUP.get(command)
|
|
128
|
+
|
|
117
129
|
return {
|
|
118
|
-
"to_address":
|
|
119
|
-
"from_address":
|
|
120
|
-
"command":
|
|
130
|
+
"to_address": to_name if to_name is not None else f"Unknown (0x{to_address:02X})",
|
|
131
|
+
"from_address": from_name if from_name is not None else f"Unknown (0x{from_address:02X})",
|
|
132
|
+
"command": cmd_name if cmd_name is not None else f"Unknown (0x{command:02X})",
|
|
121
133
|
"values": {
|
|
122
134
|
channel_name: {
|
|
123
135
|
"channel_id": f"0x{channel_id:02X}",
|
|
@@ -135,9 +147,9 @@ def decode_ascii_frame(frame: bytes) -> dict:
|
|
|
135
147
|
|
|
136
148
|
def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
137
149
|
try:
|
|
138
|
-
divisor
|
|
139
|
-
|
|
140
|
-
format_bits
|
|
150
|
+
divisor = _DIVISOR_MAP[(format_byte >> 6) & 0b11]
|
|
151
|
+
decimal_places = _DECIMAL_PLACES_MAP[divisor]
|
|
152
|
+
format_bits = format_byte & 0b1111
|
|
141
153
|
|
|
142
154
|
if len(data_bytes) == 0:
|
|
143
155
|
return None
|
|
@@ -153,14 +165,14 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
153
165
|
display_text = AUTOPILOT_MODES.get(raw, f"Unknown ({raw})")
|
|
154
166
|
else:
|
|
155
167
|
value = raw / divisor
|
|
156
|
-
display_text = f"{value:.{
|
|
168
|
+
display_text = f"{value:.{decimal_places}f}"
|
|
157
169
|
|
|
158
170
|
elif format_bits == 0x02:
|
|
159
171
|
if len(data_bytes) != 2:
|
|
160
172
|
return None
|
|
161
173
|
unsigned = ((data_bytes[0] & 0b11) << 8) | data_bytes[1]
|
|
162
174
|
value = unsigned / divisor
|
|
163
|
-
display_text = f"{value:.{
|
|
175
|
+
display_text = f"{value:.{decimal_places}f}"
|
|
164
176
|
|
|
165
177
|
elif format_bits == 0x03:
|
|
166
178
|
if len(data_bytes) != 2:
|
|
@@ -168,18 +180,20 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
168
180
|
layout = SEGMENT_A.get(data_bytes[0], "?")
|
|
169
181
|
unsigned = data_bytes[1]
|
|
170
182
|
value = _sign_from_layout(layout) * unsigned / divisor
|
|
171
|
-
display_text = _display_from_layout(layout, f"{value:.{
|
|
183
|
+
display_text = _display_from_layout(layout, f"{value:.{decimal_places}f}")
|
|
172
184
|
|
|
173
185
|
elif format_bits == 0x04:
|
|
174
186
|
if len(data_bytes) != 4:
|
|
175
187
|
return None
|
|
188
|
+
# data_bytes[0] is a status/flag byte, not part of the value
|
|
176
189
|
unsigned = int.from_bytes(data_bytes[1:], byteorder="big", signed=False)
|
|
177
190
|
value = unsigned / divisor
|
|
178
|
-
display_text = f"{value:.{
|
|
191
|
+
display_text = f"{value:.{decimal_places}f}"
|
|
179
192
|
|
|
180
193
|
elif format_bits == 0x05:
|
|
181
194
|
if len(data_bytes) != 4:
|
|
182
195
|
return None
|
|
196
|
+
# data_bytes[0] is a status/flag byte; bytes 1-3 are h, m, s
|
|
183
197
|
h, m, s = data_bytes[1], data_bytes[2], data_bytes[3]
|
|
184
198
|
value = float(h * 3600 + m * 60 + s)
|
|
185
199
|
display_text = str(datetime.timedelta(hours=h, minutes=m, seconds=s))
|
|
@@ -193,11 +207,12 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
193
207
|
elif format_bits == 0x07:
|
|
194
208
|
if len(data_bytes) != 4:
|
|
195
209
|
return None
|
|
210
|
+
# data_bytes[0] is a status/flag byte; byte 1 is the segment/layout code
|
|
196
211
|
layout = SEGMENT_A.get(data_bytes[1], "?")
|
|
197
212
|
msb = data_bytes[2] & 0b01111111
|
|
198
213
|
unsigned = (msb << 8) | data_bytes[3]
|
|
199
214
|
value = _sign_from_layout(layout) * unsigned / divisor
|
|
200
|
-
display_text = _display_from_layout(layout, f"{value:.{
|
|
215
|
+
display_text = _display_from_layout(layout, f"{value:.{decimal_places}f}")
|
|
201
216
|
|
|
202
217
|
elif format_bits == 0x08:
|
|
203
218
|
if len(data_bytes) != 2:
|
|
@@ -206,7 +221,7 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
206
221
|
layout = SEGMENT_A.get(segment_code, "?")
|
|
207
222
|
unsigned = ((data_bytes[0] & 0b1) << 8) | data_bytes[1]
|
|
208
223
|
value = unsigned / divisor
|
|
209
|
-
display_text = _display_from_layout(layout, f"{value:.{
|
|
224
|
+
display_text = _display_from_layout(layout, f"{value:.{decimal_places}f}")
|
|
210
225
|
|
|
211
226
|
elif format_bits == 0x0A:
|
|
212
227
|
if len(data_bytes) != 4:
|
|
@@ -214,9 +229,10 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
214
229
|
first = int.from_bytes(data_bytes[:2], byteorder="big", signed=True) / divisor
|
|
215
230
|
second = int.from_bytes(data_bytes[2:], byteorder="big", signed=True) / divisor
|
|
216
231
|
value = first
|
|
217
|
-
display_text = f"{first:.{
|
|
232
|
+
display_text = f"{first:.{decimal_places}f} / {second:.{decimal_places}f}"
|
|
218
233
|
|
|
219
234
|
else:
|
|
235
|
+
# format 0x09 has not been observed in captured data
|
|
220
236
|
logger.debug(f" unsupported format 0x{format_bits:02X}")
|
|
221
237
|
return None
|
|
222
238
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from .utils import calculate_checksum
|
|
2
3
|
from .mappings import COMMAND_LOOKUP, IGNORED_COMMANDS
|
|
3
4
|
from .decode_fastnet import decode_frame, decode_ascii_frame
|
|
@@ -52,29 +53,34 @@ class FrameBuffer:
|
|
|
52
53
|
frame = self.buffer[:full_frame_length]
|
|
53
54
|
body = self.buffer[5:full_frame_length - 1]
|
|
54
55
|
body_checksum = self.buffer[full_frame_length - 1]
|
|
55
|
-
|
|
56
|
+
|
|
57
|
+
debug = logger.isEnabledFor(logging.DEBUG)
|
|
56
58
|
|
|
57
59
|
if calculate_checksum(self.buffer[:4]) != header_checksum:
|
|
58
|
-
|
|
60
|
+
if debug:
|
|
61
|
+
logger.debug(f"FRAME discard header-checksum [{bytes(frame).hex()}]")
|
|
59
62
|
self.buffer = self.buffer[1:]
|
|
60
63
|
continue
|
|
61
64
|
|
|
62
65
|
if calculate_checksum(body) != body_checksum:
|
|
63
|
-
|
|
66
|
+
if debug:
|
|
67
|
+
logger.debug(f"FRAME discard body-checksum [{bytes(frame).hex()}]")
|
|
64
68
|
self.buffer = self.buffer[1:]
|
|
65
69
|
continue
|
|
66
70
|
|
|
67
71
|
self.buffer = self.buffer[full_frame_length:]
|
|
68
72
|
|
|
69
73
|
if command_name in IGNORED_COMMANDS:
|
|
70
|
-
|
|
74
|
+
if debug:
|
|
75
|
+
logger.debug(f"FRAME skip cmd={command_name}")
|
|
71
76
|
continue
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
if debug:
|
|
79
|
+
logger.debug(
|
|
80
|
+
f"FRAME cmd={command_name} "
|
|
81
|
+
f"0x{to_address:02X}←0x{from_address:02X} "
|
|
82
|
+
f"body={body_size}B [{bytes(frame).hex()}]"
|
|
83
|
+
)
|
|
78
84
|
self.decode_and_queue_frame(frame, command_name)
|
|
79
85
|
|
|
80
86
|
def decode_and_queue_frame(self, frame, command_name):
|
|
@@ -82,17 +88,19 @@ class FrameBuffer:
|
|
|
82
88
|
decoder = decode_ascii_frame if command_name == "LatLon" else decode_frame
|
|
83
89
|
decoded_frame = decoder(frame)
|
|
84
90
|
if decoded_frame and "values" in decoded_frame:
|
|
85
|
-
channel_names = list(decoded_frame["values"].keys())
|
|
86
|
-
names_str = ", ".join(channel_names[:4])
|
|
87
|
-
if len(channel_names) > 4:
|
|
88
|
-
names_str += f", +{len(channel_names) - 4} more"
|
|
89
91
|
try:
|
|
90
92
|
self.frame_queue.put_nowait(decoded_frame)
|
|
91
|
-
logger.
|
|
93
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
94
|
+
channel_names = list(decoded_frame["values"].keys())
|
|
95
|
+
names_str = ", ".join(channel_names[:4])
|
|
96
|
+
if len(channel_names) > 4:
|
|
97
|
+
names_str += f", +{len(channel_names) - 4} more"
|
|
98
|
+
logger.debug(f" QUEUE {len(channel_names)} channel(s) [{names_str}]")
|
|
92
99
|
except Full:
|
|
93
100
|
logger.warning("Frame queue full, dropping frame.")
|
|
94
101
|
else:
|
|
95
|
-
logger.
|
|
102
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
103
|
+
logger.debug(f" QUEUE fail decode error [{frame.hex()}]")
|
|
96
104
|
|
|
97
105
|
def get_buffer_size(self):
|
|
98
106
|
return len(self.buffer)
|
|
@@ -301,7 +301,7 @@ FORMAT_SIZE_MAP = {
|
|
|
301
301
|
0x03: 2, # 16 bits (2 bytes)
|
|
302
302
|
0x04: 4, # 32 bits (4 bytes)
|
|
303
303
|
0x05: 4, # 32 bits (4 bytes)
|
|
304
|
-
0x06: 4, #
|
|
304
|
+
0x06: 4, # 32 bits (4 bytes)
|
|
305
305
|
0x07: 4, # 16 bits (2 bytes)
|
|
306
306
|
0x08: 2, # 16 bits (2 bytes)
|
|
307
307
|
0x0A: 4 # 32 bits (4 bytes)
|
|
@@ -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.4", # 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.",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|