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.
Files changed (26) hide show
  1. {pyfastnet-2.0.2/pyfastnet.egg-info → pyfastnet-2.0.4}/PKG-INFO +1 -1
  2. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/decode_fastnet.py +63 -47
  3. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/frame_buffer.py +23 -15
  4. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/mappings.py +1 -1
  5. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/utils.py +1 -1
  6. {pyfastnet-2.0.2 → pyfastnet-2.0.4/pyfastnet.egg-info}/PKG-INFO +1 -1
  7. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/setup.py +1 -1
  8. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/LICENSE +0 -0
  9. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/MANIFEST.in +0 -0
  10. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/README.md +0 -0
  11. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/__init__.py +0 -0
  12. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/fastnet_decoder/logger.py +0 -0
  13. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/pyfastnet.egg-info/SOURCES.txt +0 -0
  14. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/pyfastnet.egg-info/dependency_links.txt +0 -0
  15. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/pyfastnet.egg-info/top_level.txt +0 -0
  16. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/setup.cfg +0 -0
  17. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_apparent_frame.py +0 -0
  18. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_autopilot_frame.py +0 -0
  19. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_channels.py +0 -0
  20. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_depth_frame.py +0 -0
  21. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_format07_msb_regression.py +0 -0
  22. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_format08_layout.py +0 -0
  23. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_heel_frame.py +0 -0
  24. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_rudder_frame.py +0 -0
  25. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_tide_frame.py +0 -0
  26. {pyfastnet-2.0.2 → pyfastnet-2.0.4}/tests/test_true_frame.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfastnet
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: A Python library for decoding FastNet protocol data streams.
5
5
  Home-page: https://github.com/ghotihook/pyfastnet
6
6
  Author: Alex Salmon
@@ -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
- _DP_MAP = {1: 0, 10: 1, 100: 2, 1000: 3}
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": return f"{formatted}°M"
18
- if layout == "H[data]": return f"H{formatted}"
19
- if layout == "[data]H": return f"{formatted}H"
20
- if layout in ("[data]=", "[data]-"):
21
- return f"{formatted}{layout[-1]}"
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 = frame[0]
28
- from_address = frame[1]
29
- body_size = frame[2]
30
- command = frame[3]
31
- header_checksum = frame[4]
32
- body = frame[5:-1]
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": ADDRESS_LOOKUP.get(to_address, f"Unknown (0x{to_address:02X})"),
49
- "from_address": ADDRESS_LOOKUP.get(from_address, f"Unknown (0x{from_address:02X})"),
50
- "command": COMMAND_LOOKUP.get(command, f"Unknown (0x{command:02X})"),
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, f"Unknown (0x{channel_id:02X})")
63
- index += 2
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" value={decoded_value['value']} "
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, f"Unknown (0x{channel_id:02X})")
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": ADDRESS_LOOKUP.get(to_address, f"Unknown (0x{to_address:02X})"),
119
- "from_address": ADDRESS_LOOKUP.get(from_address, f"Unknown (0x{from_address:02X})"),
120
- "command": COMMAND_LOOKUP.get(command, f"Unknown (0x{command:02X})"),
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 = _DIVISOR_MAP[(format_byte >> 6) & 0b11]
139
- dp = _DP_MAP[divisor]
140
- format_bits = format_byte & 0b1111
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:.{dp}f}"
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:.{dp}f}"
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:.{dp}f}")
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:.{dp}f}"
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:.{dp}f}")
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:.{dp}f}")
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:.{dp}f} / {second:.{dp}f}"
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
- frame_hex = bytes(frame[:8]).hex()
56
+
57
+ debug = logger.isEnabledFor(logging.DEBUG)
56
58
 
57
59
  if calculate_checksum(self.buffer[:4]) != header_checksum:
58
- logger.debug(f"FRAME discard header-checksum [{frame_hex}...]")
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
- logger.debug(f"FRAME discard body-checksum [{frame_hex}...]")
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
- logger.debug(f"FRAME skip cmd={command_name}")
74
+ if debug:
75
+ logger.debug(f"FRAME skip cmd={command_name}")
71
76
  continue
72
77
 
73
- logger.debug(
74
- f"FRAME cmd={command_name} "
75
- f"0x{to_address:02X}←0x{from_address:02X} "
76
- f"body={body_size}B [{frame_hex}...]"
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.debug(f"QUEUE {len(channel_names)} channel(s) [{names_str}]")
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.debug(f"QUEUE fail decode error [{frame.hex()[:16]}...]")
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, # 16 bits (2 bytes)
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)
@@ -8,7 +8,7 @@ def calculate_checksum(data):
8
8
  Returns:
9
9
  int: The calculated checksum.
10
10
  """
11
- return (0x100 - sum(data) % 0x100) & 0xFF
11
+ return (-sum(data)) & 0xFF
12
12
 
13
13
 
14
14
  def calculate_nmea_checksum(sentence):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfastnet
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: A Python library for decoding FastNet protocol data streams.
5
5
  Home-page: https://github.com/ghotihook/pyfastnet
6
6
  Author: Alex Salmon
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pyfastnet",
5
- version="2.0.2", # Ensure this matches your intended version
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