pyfastnet 2.0.3__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.3/pyfastnet.egg-info → pyfastnet-2.0.4}/PKG-INFO +1 -1
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/decode_fastnet.py +55 -41
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/frame_buffer.py +23 -15
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/mappings.py +1 -1
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/utils.py +1 -1
- {pyfastnet-2.0.3 → pyfastnet-2.0.4/pyfastnet.egg-info}/PKG-INFO +1 -1
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/setup.py +1 -1
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/LICENSE +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/MANIFEST.in +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/README.md +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/__init__.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/logger.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/pyfastnet.egg-info/SOURCES.txt +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/pyfastnet.egg-info/dependency_links.txt +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/pyfastnet.egg-info/top_level.txt +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/setup.cfg +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_apparent_frame.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_autopilot_frame.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_channels.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_depth_frame.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_format07_msb_regression.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_format08_layout.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_heel_frame.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_rudder_frame.py +0 -0
- {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_tide_frame.py +0 -0
- {pyfastnet-2.0.3 → 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()}]")
|
|
37
|
-
return {"error": "Header checksum mismatch"}
|
|
38
|
-
|
|
39
|
-
if calculate_checksum(body) != body_checksum:
|
|
40
|
-
logger.debug(f"FRAME discard body-checksum [{frame.hex()}]")
|
|
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):
|
|
@@ -105,8 +108,11 @@ def decode_ascii_frame(frame: bytes) -> dict:
|
|
|
105
108
|
body = frame[5:-1]
|
|
106
109
|
|
|
107
110
|
channel_id = body[0]
|
|
111
|
+
# body[1] is a format byte — not used for ASCII frames
|
|
108
112
|
data_bytes = body[2:]
|
|
109
|
-
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})"
|
|
110
116
|
|
|
111
117
|
try:
|
|
112
118
|
ascii_text = data_bytes.decode("ascii").strip()
|
|
@@ -116,10 +122,14 @@ def decode_ascii_frame(frame: bytes) -> dict:
|
|
|
116
122
|
|
|
117
123
|
logger.debug(f" CH 0x{channel_id:02X} {channel_name} ascii='{ascii_text}'")
|
|
118
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
|
+
|
|
119
129
|
return {
|
|
120
|
-
"to_address":
|
|
121
|
-
"from_address":
|
|
122
|
-
"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})",
|
|
123
133
|
"values": {
|
|
124
134
|
channel_name: {
|
|
125
135
|
"channel_id": f"0x{channel_id:02X}",
|
|
@@ -137,9 +147,9 @@ def decode_ascii_frame(frame: bytes) -> dict:
|
|
|
137
147
|
|
|
138
148
|
def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
139
149
|
try:
|
|
140
|
-
divisor
|
|
141
|
-
|
|
142
|
-
format_bits
|
|
150
|
+
divisor = _DIVISOR_MAP[(format_byte >> 6) & 0b11]
|
|
151
|
+
decimal_places = _DECIMAL_PLACES_MAP[divisor]
|
|
152
|
+
format_bits = format_byte & 0b1111
|
|
143
153
|
|
|
144
154
|
if len(data_bytes) == 0:
|
|
145
155
|
return None
|
|
@@ -155,14 +165,14 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
155
165
|
display_text = AUTOPILOT_MODES.get(raw, f"Unknown ({raw})")
|
|
156
166
|
else:
|
|
157
167
|
value = raw / divisor
|
|
158
|
-
display_text = f"{value:.{
|
|
168
|
+
display_text = f"{value:.{decimal_places}f}"
|
|
159
169
|
|
|
160
170
|
elif format_bits == 0x02:
|
|
161
171
|
if len(data_bytes) != 2:
|
|
162
172
|
return None
|
|
163
173
|
unsigned = ((data_bytes[0] & 0b11) << 8) | data_bytes[1]
|
|
164
174
|
value = unsigned / divisor
|
|
165
|
-
display_text = f"{value:.{
|
|
175
|
+
display_text = f"{value:.{decimal_places}f}"
|
|
166
176
|
|
|
167
177
|
elif format_bits == 0x03:
|
|
168
178
|
if len(data_bytes) != 2:
|
|
@@ -170,18 +180,20 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
170
180
|
layout = SEGMENT_A.get(data_bytes[0], "?")
|
|
171
181
|
unsigned = data_bytes[1]
|
|
172
182
|
value = _sign_from_layout(layout) * unsigned / divisor
|
|
173
|
-
display_text = _display_from_layout(layout, f"{value:.{
|
|
183
|
+
display_text = _display_from_layout(layout, f"{value:.{decimal_places}f}")
|
|
174
184
|
|
|
175
185
|
elif format_bits == 0x04:
|
|
176
186
|
if len(data_bytes) != 4:
|
|
177
187
|
return None
|
|
188
|
+
# data_bytes[0] is a status/flag byte, not part of the value
|
|
178
189
|
unsigned = int.from_bytes(data_bytes[1:], byteorder="big", signed=False)
|
|
179
190
|
value = unsigned / divisor
|
|
180
|
-
display_text = f"{value:.{
|
|
191
|
+
display_text = f"{value:.{decimal_places}f}"
|
|
181
192
|
|
|
182
193
|
elif format_bits == 0x05:
|
|
183
194
|
if len(data_bytes) != 4:
|
|
184
195
|
return None
|
|
196
|
+
# data_bytes[0] is a status/flag byte; bytes 1-3 are h, m, s
|
|
185
197
|
h, m, s = data_bytes[1], data_bytes[2], data_bytes[3]
|
|
186
198
|
value = float(h * 3600 + m * 60 + s)
|
|
187
199
|
display_text = str(datetime.timedelta(hours=h, minutes=m, seconds=s))
|
|
@@ -195,11 +207,12 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
195
207
|
elif format_bits == 0x07:
|
|
196
208
|
if len(data_bytes) != 4:
|
|
197
209
|
return None
|
|
210
|
+
# data_bytes[0] is a status/flag byte; byte 1 is the segment/layout code
|
|
198
211
|
layout = SEGMENT_A.get(data_bytes[1], "?")
|
|
199
212
|
msb = data_bytes[2] & 0b01111111
|
|
200
213
|
unsigned = (msb << 8) | data_bytes[3]
|
|
201
214
|
value = _sign_from_layout(layout) * unsigned / divisor
|
|
202
|
-
display_text = _display_from_layout(layout, f"{value:.{
|
|
215
|
+
display_text = _display_from_layout(layout, f"{value:.{decimal_places}f}")
|
|
203
216
|
|
|
204
217
|
elif format_bits == 0x08:
|
|
205
218
|
if len(data_bytes) != 2:
|
|
@@ -208,7 +221,7 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
208
221
|
layout = SEGMENT_A.get(segment_code, "?")
|
|
209
222
|
unsigned = ((data_bytes[0] & 0b1) << 8) | data_bytes[1]
|
|
210
223
|
value = unsigned / divisor
|
|
211
|
-
display_text = _display_from_layout(layout, f"{value:.{
|
|
224
|
+
display_text = _display_from_layout(layout, f"{value:.{decimal_places}f}")
|
|
212
225
|
|
|
213
226
|
elif format_bits == 0x0A:
|
|
214
227
|
if len(data_bytes) != 4:
|
|
@@ -216,9 +229,10 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
216
229
|
first = int.from_bytes(data_bytes[:2], byteorder="big", signed=True) / divisor
|
|
217
230
|
second = int.from_bytes(data_bytes[2:], byteorder="big", signed=True) / divisor
|
|
218
231
|
value = first
|
|
219
|
-
display_text = f"{first:.{
|
|
232
|
+
display_text = f"{first:.{decimal_places}f} / {second:.{decimal_places}f}"
|
|
220
233
|
|
|
221
234
|
else:
|
|
235
|
+
# format 0x09 has not been observed in captured data
|
|
222
236
|
logger.debug(f" unsupported format 0x{format_bits:02X}")
|
|
223
237
|
return None
|
|
224
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
|