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.
Files changed (26) hide show
  1. {pyfastnet-2.0.3/pyfastnet.egg-info → pyfastnet-2.0.4}/PKG-INFO +1 -1
  2. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/decode_fastnet.py +55 -41
  3. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/frame_buffer.py +23 -15
  4. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/mappings.py +1 -1
  5. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/utils.py +1 -1
  6. {pyfastnet-2.0.3 → pyfastnet-2.0.4/pyfastnet.egg-info}/PKG-INFO +1 -1
  7. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/setup.py +1 -1
  8. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/LICENSE +0 -0
  9. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/MANIFEST.in +0 -0
  10. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/README.md +0 -0
  11. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/__init__.py +0 -0
  12. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/fastnet_decoder/logger.py +0 -0
  13. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/pyfastnet.egg-info/SOURCES.txt +0 -0
  14. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/pyfastnet.egg-info/dependency_links.txt +0 -0
  15. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/pyfastnet.egg-info/top_level.txt +0 -0
  16. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/setup.cfg +0 -0
  17. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_apparent_frame.py +0 -0
  18. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_autopilot_frame.py +0 -0
  19. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_channels.py +0 -0
  20. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_depth_frame.py +0 -0
  21. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_format07_msb_regression.py +0 -0
  22. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_format08_layout.py +0 -0
  23. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_heel_frame.py +0 -0
  24. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_rudder_frame.py +0 -0
  25. {pyfastnet-2.0.3 → pyfastnet-2.0.4}/tests/test_tide_frame.py +0 -0
  26. {pyfastnet-2.0.3 → 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.3
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()}]")
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": 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):
@@ -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, 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})"
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": ADDRESS_LOOKUP.get(to_address, f"Unknown (0x{to_address:02X})"),
121
- "from_address": ADDRESS_LOOKUP.get(from_address, f"Unknown (0x{from_address:02X})"),
122
- "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})",
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 = _DIVISOR_MAP[(format_byte >> 6) & 0b11]
141
- dp = _DP_MAP[divisor]
142
- 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
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:.{dp}f}"
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:.{dp}f}"
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:.{dp}f}")
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:.{dp}f}"
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:.{dp}f}")
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:.{dp}f}")
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:.{dp}f} / {second:.{dp}f}"
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
- frame_hex = bytes(frame).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()}]")
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.3
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.3", # 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