pyfastnet 2.0.1__tar.gz → 2.0.2__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.1/pyfastnet.egg-info → pyfastnet-2.0.2}/PKG-INFO +1 -1
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/fastnet_decoder/decode_fastnet.py +36 -41
- pyfastnet-2.0.2/fastnet_decoder/frame_buffer.py +103 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/fastnet_decoder/logger.py +4 -10
- {pyfastnet-2.0.1 → pyfastnet-2.0.2/pyfastnet.egg-info}/PKG-INFO +1 -1
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/setup.py +1 -1
- pyfastnet-2.0.1/fastnet_decoder/frame_buffer.py +0 -198
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/LICENSE +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/MANIFEST.in +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/README.md +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/fastnet_decoder/__init__.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/fastnet_decoder/mappings.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/fastnet_decoder/utils.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/pyfastnet.egg-info/SOURCES.txt +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/pyfastnet.egg-info/dependency_links.txt +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/pyfastnet.egg-info/top_level.txt +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/setup.cfg +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_apparent_frame.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_autopilot_frame.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_channels.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_depth_frame.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_format07_msb_regression.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_format08_layout.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_heel_frame.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_rudder_frame.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_tide_frame.py +0 -0
- {pyfastnet-2.0.1 → pyfastnet-2.0.2}/tests/test_true_frame.py +0 -0
|
@@ -24,8 +24,6 @@ def _display_from_layout(layout: str, formatted: str) -> str:
|
|
|
24
24
|
|
|
25
25
|
def decode_frame(frame: bytes) -> dict:
|
|
26
26
|
try:
|
|
27
|
-
logger.debug(f"Starting frame decoding. Frame length: {len(frame)}, Frame contents: {frame.hex()}")
|
|
28
|
-
|
|
29
27
|
to_address = frame[0]
|
|
30
28
|
from_address = frame[1]
|
|
31
29
|
body_size = frame[2]
|
|
@@ -34,76 +32,75 @@ def decode_frame(frame: bytes) -> dict:
|
|
|
34
32
|
body = frame[5:-1]
|
|
35
33
|
body_checksum = frame[-1]
|
|
36
34
|
|
|
37
|
-
logger.debug(
|
|
38
|
-
f"Parsed header: to_address=0x{to_address:02X}, from_address=0x{from_address:02X}, "
|
|
39
|
-
f"body_size={body_size}, command=0x{command:02X}, header_checksum=0x{header_checksum:02X}"
|
|
40
|
-
)
|
|
41
|
-
|
|
42
35
|
if calculate_checksum(frame[:4]) != header_checksum:
|
|
43
|
-
logger.debug(f"
|
|
36
|
+
logger.debug(f"FRAME discard header-checksum [{frame.hex()[:16]}...]")
|
|
44
37
|
return {"error": "Header checksum mismatch"}
|
|
45
38
|
|
|
46
39
|
if calculate_checksum(body) != body_checksum:
|
|
47
|
-
logger.debug(f"
|
|
40
|
+
logger.debug(f"FRAME discard body-checksum [{frame.hex()[:16]}...]")
|
|
48
41
|
return {"error": "Body checksum mismatch"}
|
|
49
42
|
|
|
50
43
|
if len(body) < 2 or len(body) != body_size:
|
|
51
|
-
logger.debug(f"
|
|
44
|
+
logger.debug(f"FRAME discard body-size expected={body_size} actual={len(body)}")
|
|
52
45
|
return {"error": "Invalid body size"}
|
|
53
46
|
|
|
54
|
-
logger.debug("Header and body checksums are valid.")
|
|
55
|
-
|
|
56
47
|
decoded_data = {
|
|
57
|
-
"to_address":
|
|
48
|
+
"to_address": ADDRESS_LOOKUP.get(to_address, f"Unknown (0x{to_address:02X})"),
|
|
58
49
|
"from_address": ADDRESS_LOOKUP.get(from_address, f"Unknown (0x{from_address:02X})"),
|
|
59
|
-
"command":
|
|
60
|
-
"values":
|
|
50
|
+
"command": COMMAND_LOOKUP.get(command, f"Unknown (0x{command:02X})"),
|
|
51
|
+
"values": {}
|
|
61
52
|
}
|
|
62
53
|
|
|
63
54
|
index = 0
|
|
64
55
|
while index < len(body):
|
|
65
56
|
if index + 1 >= len(body):
|
|
66
|
-
logger.debug(
|
|
67
|
-
f"Insufficient bytes to decode channel ID and format byte at index {index}. "
|
|
68
|
-
f"Remaining length: {len(body) - index}"
|
|
69
|
-
)
|
|
57
|
+
logger.debug(f" CH incomplete header at index={index}")
|
|
70
58
|
return {"error": "Insufficient bytes for channel header"}
|
|
71
59
|
|
|
72
|
-
channel_id
|
|
73
|
-
format_byte
|
|
74
|
-
|
|
60
|
+
channel_id = body[index]
|
|
61
|
+
format_byte = body[index + 1]
|
|
62
|
+
channel_name = CHANNEL_LOOKUP.get(channel_id, f"Unknown (0x{channel_id:02X})")
|
|
63
|
+
index += 2
|
|
75
64
|
|
|
76
65
|
data_length = FORMAT_SIZE_MAP.get(format_byte & 0x0F, 0)
|
|
77
66
|
if index + data_length > len(body):
|
|
78
67
|
logger.debug(
|
|
79
|
-
f"
|
|
80
|
-
f"
|
|
68
|
+
f" CH 0x{channel_id:02X} {channel_name} "
|
|
69
|
+
f"incomplete need={data_length}B have={len(body) - index}B"
|
|
81
70
|
)
|
|
82
71
|
return {"error": f"Incomplete data for channel 0x{channel_id:02X}"}
|
|
83
72
|
|
|
84
73
|
data_bytes = body[index:index + data_length]
|
|
85
74
|
index += data_length
|
|
86
75
|
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
logger.debug(
|
|
77
|
+
f" CH 0x{channel_id:02X} {channel_name} "
|
|
78
|
+
f"fmt=0x{format_byte:02X} data=[{data_bytes.hex()}]"
|
|
79
|
+
)
|
|
89
80
|
|
|
81
|
+
decoded_value = decode_format_and_data(channel_id, format_byte, data_bytes)
|
|
90
82
|
decoded_data["values"][channel_name] = decoded_value
|
|
91
|
-
|
|
83
|
+
|
|
84
|
+
if decoded_value:
|
|
85
|
+
logger.debug(
|
|
86
|
+
f" value={decoded_value['value']} "
|
|
87
|
+
f"display='{decoded_value['display_text']}' "
|
|
88
|
+
f"layout={decoded_value['layout']}"
|
|
89
|
+
)
|
|
92
90
|
|
|
93
91
|
return decoded_data
|
|
94
92
|
|
|
95
93
|
except Exception as e:
|
|
96
|
-
logger.error(f"Unexpected error
|
|
94
|
+
logger.error(f"Unexpected error decoding frame: {e} [{frame.hex()}]")
|
|
97
95
|
return {"error": "Decoding failure"}
|
|
98
96
|
|
|
99
97
|
|
|
100
98
|
def decode_ascii_frame(frame: bytes) -> dict:
|
|
101
99
|
try:
|
|
102
|
-
to_address
|
|
103
|
-
from_address
|
|
104
|
-
command
|
|
105
|
-
|
|
106
|
-
body = frame[5:-1]
|
|
100
|
+
to_address = frame[0]
|
|
101
|
+
from_address = frame[1]
|
|
102
|
+
command = frame[3]
|
|
103
|
+
body = frame[5:-1]
|
|
107
104
|
|
|
108
105
|
channel_id = body[0]
|
|
109
106
|
data_bytes = body[2:]
|
|
@@ -112,13 +109,15 @@ def decode_ascii_frame(frame: bytes) -> dict:
|
|
|
112
109
|
try:
|
|
113
110
|
ascii_text = data_bytes.decode("ascii").strip()
|
|
114
111
|
except UnicodeDecodeError as e:
|
|
115
|
-
logger.warning(f"
|
|
112
|
+
logger.warning(f" CH 0x{channel_id:02X} {channel_name} ASCII decode failed: {e}")
|
|
116
113
|
return {"error": "ASCII decode failed"}
|
|
117
114
|
|
|
115
|
+
logger.debug(f" CH 0x{channel_id:02X} {channel_name} ascii='{ascii_text}'")
|
|
116
|
+
|
|
118
117
|
return {
|
|
119
|
-
"to_address":
|
|
118
|
+
"to_address": ADDRESS_LOOKUP.get(to_address, f"Unknown (0x{to_address:02X})"),
|
|
120
119
|
"from_address": ADDRESS_LOOKUP.get(from_address, f"Unknown (0x{from_address:02X})"),
|
|
121
|
-
"command":
|
|
120
|
+
"command": COMMAND_LOOKUP.get(command, f"Unknown (0x{command:02X})"),
|
|
122
121
|
"values": {
|
|
123
122
|
channel_name: {
|
|
124
123
|
"channel_id": f"0x{channel_id:02X}",
|
|
@@ -136,14 +135,11 @@ def decode_ascii_frame(frame: bytes) -> dict:
|
|
|
136
135
|
|
|
137
136
|
def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
138
137
|
try:
|
|
139
|
-
logger.debug(f"Decoding channel ID: 0x{channel_id:02X}, format byte: 0x{format_byte:02X}, data: {data_bytes.hex()}")
|
|
140
|
-
|
|
141
138
|
divisor = _DIVISOR_MAP[(format_byte >> 6) & 0b11]
|
|
142
139
|
dp = _DP_MAP[divisor]
|
|
143
140
|
format_bits = format_byte & 0b1111
|
|
144
141
|
|
|
145
142
|
if len(data_bytes) == 0:
|
|
146
|
-
logger.debug("decode_format_and_data: Empty data bytes; cannot decode.")
|
|
147
143
|
return None
|
|
148
144
|
|
|
149
145
|
layout = None
|
|
@@ -193,7 +189,6 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
193
189
|
return None
|
|
194
190
|
value = None
|
|
195
191
|
display_text = "".join(SEGMENT_B.get(b, "?") for b in data_bytes)
|
|
196
|
-
logger.debug(f"Decoded 7-segment text: {display_text}")
|
|
197
192
|
|
|
198
193
|
elif format_bits == 0x07:
|
|
199
194
|
if len(data_bytes) != 4:
|
|
@@ -222,7 +217,7 @@ def decode_format_and_data(channel_id, format_byte, data_bytes):
|
|
|
222
217
|
display_text = f"{first:.{dp}f} / {second:.{dp}f}"
|
|
223
218
|
|
|
224
219
|
else:
|
|
225
|
-
logger.debug(f"
|
|
220
|
+
logger.debug(f" unsupported format 0x{format_bits:02X}")
|
|
226
221
|
return None
|
|
227
222
|
|
|
228
223
|
return {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from .utils import calculate_checksum
|
|
2
|
+
from .mappings import COMMAND_LOOKUP, IGNORED_COMMANDS
|
|
3
|
+
from .decode_fastnet import decode_frame, decode_ascii_frame
|
|
4
|
+
from .logger import logger
|
|
5
|
+
from queue import Queue, Full
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FrameBuffer:
|
|
9
|
+
"""
|
|
10
|
+
Manages an incoming byte stream, extracts valid FastNet frames,
|
|
11
|
+
decodes them, and queues the results.
|
|
12
|
+
|
|
13
|
+
Typical workflow:
|
|
14
|
+
1. Feed raw bytes via add_to_buffer()
|
|
15
|
+
2. Call get_complete_frames() to process the buffer
|
|
16
|
+
3. Pull decoded frames from frame_queue
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, max_buffer_size=8192, max_queue_size=1000):
|
|
20
|
+
self.buffer = bytearray()
|
|
21
|
+
self.max_buffer_size = max_buffer_size
|
|
22
|
+
self.frame_queue = Queue(maxsize=max_queue_size)
|
|
23
|
+
|
|
24
|
+
def add_to_buffer(self, new_data):
|
|
25
|
+
if not isinstance(new_data, (bytes, bytearray)):
|
|
26
|
+
logger.error("Invalid data type passed to add_to_buffer. Expected bytes or bytearray.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
self.buffer.extend(new_data)
|
|
30
|
+
logger.debug(f"BUF +{len(new_data)}B total={len(self.buffer)}B")
|
|
31
|
+
|
|
32
|
+
if len(self.buffer) > self.max_buffer_size:
|
|
33
|
+
logger.warning("Buffer size exceeded maximum limit. Trimming the oldest data.")
|
|
34
|
+
self.buffer = self.buffer[-self.max_buffer_size:]
|
|
35
|
+
|
|
36
|
+
def get_complete_frames(self):
|
|
37
|
+
"""Extract, validate, and queue complete frames from the buffer."""
|
|
38
|
+
while len(self.buffer) >= 6:
|
|
39
|
+
to_address = self.buffer[0]
|
|
40
|
+
from_address = self.buffer[1]
|
|
41
|
+
body_size = self.buffer[2]
|
|
42
|
+
command = self.buffer[3]
|
|
43
|
+
header_checksum = self.buffer[4]
|
|
44
|
+
|
|
45
|
+
command_name = COMMAND_LOOKUP.get(command, f"Unknown (0x{command:02X})")
|
|
46
|
+
full_frame_length = 5 + body_size + 1
|
|
47
|
+
|
|
48
|
+
if len(self.buffer) < full_frame_length:
|
|
49
|
+
logger.debug(f"FRAME wait need={full_frame_length}B have={len(self.buffer)}B")
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
frame = self.buffer[:full_frame_length]
|
|
53
|
+
body = self.buffer[5:full_frame_length - 1]
|
|
54
|
+
body_checksum = self.buffer[full_frame_length - 1]
|
|
55
|
+
frame_hex = bytes(frame[:8]).hex()
|
|
56
|
+
|
|
57
|
+
if calculate_checksum(self.buffer[:4]) != header_checksum:
|
|
58
|
+
logger.debug(f"FRAME discard header-checksum [{frame_hex}...]")
|
|
59
|
+
self.buffer = self.buffer[1:]
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if calculate_checksum(body) != body_checksum:
|
|
63
|
+
logger.debug(f"FRAME discard body-checksum [{frame_hex}...]")
|
|
64
|
+
self.buffer = self.buffer[1:]
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
self.buffer = self.buffer[full_frame_length:]
|
|
68
|
+
|
|
69
|
+
if command_name in IGNORED_COMMANDS:
|
|
70
|
+
logger.debug(f"FRAME skip cmd={command_name}")
|
|
71
|
+
continue
|
|
72
|
+
|
|
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
|
+
self.decode_and_queue_frame(frame, command_name)
|
|
79
|
+
|
|
80
|
+
def decode_and_queue_frame(self, frame, command_name):
|
|
81
|
+
"""Decode a frame and add it to the queue if valid."""
|
|
82
|
+
decoder = decode_ascii_frame if command_name == "LatLon" else decode_frame
|
|
83
|
+
decoded_frame = decoder(frame)
|
|
84
|
+
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
|
+
try:
|
|
90
|
+
self.frame_queue.put_nowait(decoded_frame)
|
|
91
|
+
logger.debug(f"QUEUE {len(channel_names)} channel(s) [{names_str}]")
|
|
92
|
+
except Full:
|
|
93
|
+
logger.warning("Frame queue full, dropping frame.")
|
|
94
|
+
else:
|
|
95
|
+
logger.debug(f"QUEUE fail decode error [{frame.hex()[:16]}...]")
|
|
96
|
+
|
|
97
|
+
def get_buffer_size(self):
|
|
98
|
+
return len(self.buffer)
|
|
99
|
+
|
|
100
|
+
def get_buffer_contents(self):
|
|
101
|
+
hex_contents = self.buffer.hex()
|
|
102
|
+
logger.debug(f"BUF contents [{hex_contents}]")
|
|
103
|
+
return hex_contents
|
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
logger = logging.getLogger("fastnet_decoder")
|
|
3
|
+
logger = logging.getLogger("pyfastnet")
|
|
5
4
|
|
|
6
|
-
# Default log level
|
|
7
5
|
DEFAULT_LOG_LEVEL = logging.INFO
|
|
8
6
|
|
|
9
|
-
# Avoid duplicate handlers
|
|
10
7
|
if not logger.hasHandlers():
|
|
11
8
|
handler = logging.StreamHandler()
|
|
12
|
-
formatter = logging.Formatter("%(asctime)s [
|
|
9
|
+
formatter = logging.Formatter("%(asctime)s [pyfastnet] %(levelname)-5s %(message)s")
|
|
13
10
|
handler.setFormatter(formatter)
|
|
14
11
|
logger.addHandler(handler)
|
|
15
12
|
|
|
16
13
|
logger.setLevel(DEFAULT_LOG_LEVEL)
|
|
17
14
|
|
|
15
|
+
|
|
18
16
|
def set_log_level(level_name: str):
|
|
19
|
-
"""
|
|
20
|
-
Sets the log level dynamically at runtime.
|
|
21
|
-
Args:
|
|
22
|
-
level_name (str): Name of the log level (e.g., "DEBUG", "INFO", "WARNING").
|
|
23
|
-
"""
|
|
17
|
+
"""Sets the log level dynamically at runtime."""
|
|
24
18
|
level = getattr(logging, level_name.upper(), DEFAULT_LOG_LEVEL)
|
|
25
19
|
logger.setLevel(level)
|
|
26
20
|
logger.info(f"Log level set to {level_name.upper()}.")
|
|
@@ -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.2", # 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.",
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
from .utils import calculate_checksum # Import checksum function from utils.py
|
|
2
|
-
from .mappings import COMMAND_LOOKUP, IGNORED_COMMANDS
|
|
3
|
-
from .decode_fastnet import decode_frame, decode_ascii_frame
|
|
4
|
-
from .logger import logger
|
|
5
|
-
from queue import Queue
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"""
|
|
9
|
-
FrameBuffer Class
|
|
10
|
-
=================
|
|
11
|
-
|
|
12
|
-
The `FrameBuffer` class is responsible for managing a stream of incoming data, validating and extracting complete
|
|
13
|
-
frames, and decoding those frames using the FastNet protocol. It is designed to handle real-time data input, making
|
|
14
|
-
it suitable for scenarios like serial communication with hardware devices.
|
|
15
|
-
|
|
16
|
-
Core Responsibilities:
|
|
17
|
-
-----------------------
|
|
18
|
-
1. **Buffer Management**:
|
|
19
|
-
- The class maintains an internal `bytearray` buffer to store incoming raw data.
|
|
20
|
-
- New data can be added to the buffer using the `add_to_buffer` method.
|
|
21
|
-
- The buffer automatically trims its size to a configurable maximum (`max_buffer_size`) to avoid unbounded memory usage.
|
|
22
|
-
|
|
23
|
-
2. **Frame Extraction**:
|
|
24
|
-
- The `get_complete_frames` method scans the buffer for complete frames based on the FastNet protocol structure:
|
|
25
|
-
- A frame consists of a 5-byte header, a variable-length body, and a checksum.
|
|
26
|
-
- Both the header and body checksums are validated before processing the frame.
|
|
27
|
-
- Frames that are incomplete or fail checksum validation are skipped, and the buffer is adjusted to search for the next valid frame.
|
|
28
|
-
|
|
29
|
-
3. **Frame Decoding**:
|
|
30
|
-
- After extracting a valid frame, the method determines the appropriate decoder (ASCII or standard) based on the command type.
|
|
31
|
-
- Decoded frames are added to an internal queue (`frame_queue`) for further processing.
|
|
32
|
-
|
|
33
|
-
4. **Command Filtering**:
|
|
34
|
-
- Certain command types, such as "Keep Alive" or "Light Intensity," can be ignored based on a configurable list of ignored commands
|
|
35
|
-
(managed in the `mappings.py` file).
|
|
36
|
-
|
|
37
|
-
Key Features:
|
|
38
|
-
-------------
|
|
39
|
-
- **Thread-Safe Queue**:
|
|
40
|
-
The `frame_queue` is implemented using Python's `queue.Queue`, enabling safe concurrent access from multiple threads
|
|
41
|
-
(if needed).
|
|
42
|
-
|
|
43
|
-
- **Error Handling**:
|
|
44
|
-
- The class is robust against malformed or corrupted data. Frames with invalid checksums are discarded without crashing the application.
|
|
45
|
-
- Warnings and errors are logged for debugging purposes.
|
|
46
|
-
|
|
47
|
-
- **Modularity**:
|
|
48
|
-
- The decoding logic is separated from the frame extraction, adhering to the single-responsibility principle.
|
|
49
|
-
- The list of ignored commands is managed in `mappings.py`, promoting modularity and ease of configuration.
|
|
50
|
-
|
|
51
|
-
Typical Workflow:
|
|
52
|
-
-----------------
|
|
53
|
-
1. Raw data (e.g., from a serial port) is fed into the buffer using the `add_to_buffer` method.
|
|
54
|
-
2. The `get_complete_frames` method scans the buffer for valid frames, processes them, and adds decoded frames to the `frame_queue`.
|
|
55
|
-
3. Another part of the application (e.g., a main loop) retrieves and processes frames from the queue for further action (e.g., broadcasting NMEA sentences).
|
|
56
|
-
|
|
57
|
-
Configuration Parameters:
|
|
58
|
-
-------------------------
|
|
59
|
-
- `max_buffer_size`: Limits the size of the internal buffer. Older data is discarded when the buffer exceeds this size.
|
|
60
|
-
- `max_queue_size`: Specifies the maximum number of frames that can be stored in the `frame_queue`.
|
|
61
|
-
|
|
62
|
-
Use Cases:
|
|
63
|
-
----------
|
|
64
|
-
The `FrameBuffer` class is ideal for:
|
|
65
|
-
- Real-time data processing systems where incoming data must be validated and decoded before use.
|
|
66
|
-
- Applications where reliability is critical, such as navigation systems or hardware communication protocols.
|
|
67
|
-
|
|
68
|
-
Example:
|
|
69
|
-
--------
|
|
70
|
-
# Initialize the FrameBuffer
|
|
71
|
-
frame_buffer = FrameBuffer(max_buffer_size=8192, max_queue_size=1000)
|
|
72
|
-
|
|
73
|
-
# Add raw data to the buffer
|
|
74
|
-
frame_buffer.add_to_buffer(new_data)
|
|
75
|
-
|
|
76
|
-
# Extract and decode complete frames
|
|
77
|
-
frame_buffer.get_complete_frames()
|
|
78
|
-
|
|
79
|
-
# Process frames from the queue
|
|
80
|
-
while not frame_buffer.frame_queue.empty():
|
|
81
|
-
frame = frame_buffer.frame_queue.get()
|
|
82
|
-
# Process the decoded frame
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class FrameBuffer:
|
|
87
|
-
"""
|
|
88
|
-
A class that manages an incoming data stream, extracts valid frames,
|
|
89
|
-
and decodes them using the FastNet protocol.
|
|
90
|
-
"""
|
|
91
|
-
def __init__(self, max_buffer_size=8192, max_queue_size=1000):
|
|
92
|
-
self.buffer = bytearray()
|
|
93
|
-
self.max_buffer_size = max_buffer_size
|
|
94
|
-
self.frame_queue = Queue(maxsize=max_queue_size) # Shared instance for frames
|
|
95
|
-
|
|
96
|
-
def add_to_buffer(self, new_data):
|
|
97
|
-
"""
|
|
98
|
-
Adds new data to the buffer.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
new_data (bytes): New data from the serial input.
|
|
102
|
-
"""
|
|
103
|
-
if not isinstance(new_data, (bytes, bytearray)):
|
|
104
|
-
logger.error("Invalid data type passed to add_to_buffer. Expected bytes or bytearray.")
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
self.buffer.extend(new_data)
|
|
108
|
-
logger.debug(f"Added {len(new_data)} bytes to buffer. Buffer size: {len(self.buffer)} bytes.")
|
|
109
|
-
|
|
110
|
-
# Prevent the buffer from growing indefinitely
|
|
111
|
-
if len(self.buffer) > self.max_buffer_size:
|
|
112
|
-
logger.warning("Buffer size exceeded maximum limit. Trimming the oldest data.")
|
|
113
|
-
self.buffer = self.buffer[-self.max_buffer_size:] # Keep the latest data only
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def get_complete_frames(self):
|
|
117
|
-
"""
|
|
118
|
-
Extract and validate complete frames from the buffer, then add them to the internal queue.
|
|
119
|
-
"""
|
|
120
|
-
while len(self.buffer) >= 6: # Minimum frame size (5 header + 1 body checksum)
|
|
121
|
-
to_address = self.buffer[0]
|
|
122
|
-
from_address = self.buffer[1]
|
|
123
|
-
body_size = self.buffer[2]
|
|
124
|
-
command = self.buffer[3]
|
|
125
|
-
header_checksum = self.buffer[4]
|
|
126
|
-
|
|
127
|
-
# Identify command name from lookup
|
|
128
|
-
command_name = COMMAND_LOOKUP.get(command, f"Unknown (0x{command:02X})")
|
|
129
|
-
|
|
130
|
-
# Calculate full frame length
|
|
131
|
-
full_frame_length = 5 + body_size + 1 # Header (5 bytes) + body + body checksum
|
|
132
|
-
if len(self.buffer) < full_frame_length:
|
|
133
|
-
logger.debug(f"Incomplete frame: waiting for more bytes (needed {full_frame_length}, got {len(self.buffer)})")
|
|
134
|
-
break
|
|
135
|
-
|
|
136
|
-
# Extract frame data
|
|
137
|
-
frame = self.buffer[:full_frame_length]
|
|
138
|
-
body = self.buffer[5:full_frame_length - 1]
|
|
139
|
-
body_checksum = self.buffer[full_frame_length - 1]
|
|
140
|
-
|
|
141
|
-
# Verify header and body checksums
|
|
142
|
-
if calculate_checksum(self.buffer[:4]) != header_checksum:
|
|
143
|
-
logger.debug("Header checksum mismatch. Dropping first byte.")
|
|
144
|
-
self.buffer = self.buffer[1:]
|
|
145
|
-
continue
|
|
146
|
-
|
|
147
|
-
if calculate_checksum(body) != body_checksum:
|
|
148
|
-
logger.debug("Body checksum mismatch. Dropping first byte.")
|
|
149
|
-
self.buffer = self.buffer[1:]
|
|
150
|
-
continue
|
|
151
|
-
|
|
152
|
-
# Remove frame from buffer after validation
|
|
153
|
-
self.buffer = self.buffer[full_frame_length:]
|
|
154
|
-
|
|
155
|
-
# Skip ignored commands
|
|
156
|
-
if command_name in IGNORED_COMMANDS:
|
|
157
|
-
logger.debug(f"Skipping ignored command: {command_name}")
|
|
158
|
-
continue
|
|
159
|
-
|
|
160
|
-
# Decode the frame
|
|
161
|
-
self.decode_and_queue_frame(frame, command_name)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def decode_and_queue_frame(self, frame, command_name):
|
|
165
|
-
"""Decode a frame and add it to the queue if valid."""
|
|
166
|
-
decoder = decode_ascii_frame if command_name == "LatLon" else decode_frame
|
|
167
|
-
decoded_frame = decoder(frame)
|
|
168
|
-
if decoded_frame:
|
|
169
|
-
try:
|
|
170
|
-
self.frame_queue.put_nowait(decoded_frame)
|
|
171
|
-
logger.debug(f"Added frame to queue: {decoded_frame}")
|
|
172
|
-
except queue.Full:
|
|
173
|
-
logger.warning("Frame queue is full. Dropping frame.")
|
|
174
|
-
else:
|
|
175
|
-
logger.debug(f"Failed to decode frame: {frame.hex()}")
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def get_buffer_size(self):
|
|
180
|
-
"""
|
|
181
|
-
Returns the current size of the buffer.
|
|
182
|
-
|
|
183
|
-
Returns:
|
|
184
|
-
int: The number of bytes currently in the buffer.
|
|
185
|
-
"""
|
|
186
|
-
return len(self.buffer)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def get_buffer_contents(self):
|
|
190
|
-
"""
|
|
191
|
-
Returns the contents of the buffer as a hex string.
|
|
192
|
-
|
|
193
|
-
Returns:
|
|
194
|
-
str: The hexadecimal representation of the buffer contents.
|
|
195
|
-
"""
|
|
196
|
-
hex_contents = self.buffer.hex()
|
|
197
|
-
logger.debug(f"Buffer contents: {hex_contents}")
|
|
198
|
-
return hex_contents
|
|
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
|
|
File without changes
|