urst-mpy 1.0.2__py3-none-any.whl
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.
- urst/__init__.py +18 -0
- urst/codec_layer.py +175 -0
- urst/constants.py +28 -0
- urst/core_handler.py +136 -0
- urst/protocol_layer.py +296 -0
- urst_mpy-1.0.2.dist-info/METADATA +172 -0
- urst_mpy-1.0.2.dist-info/RECORD +9 -0
- urst_mpy-1.0.2.dist-info/WHEEL +4 -0
- urst_mpy-1.0.2.dist-info/licenses/LICENSE.md +73 -0
urst/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import logging
|
|
3
|
+
except ImportError:
|
|
4
|
+
# Minimal MicroPython fallback: swallow all log calls via __getattr__.
|
|
5
|
+
class _NoLog:
|
|
6
|
+
def __getattr__(self, _):
|
|
7
|
+
return lambda *a, **k: None
|
|
8
|
+
|
|
9
|
+
class _NoLogging:
|
|
10
|
+
def getLogger(self, _):
|
|
11
|
+
return _NoLog()
|
|
12
|
+
|
|
13
|
+
logging = _NoLogging()
|
|
14
|
+
|
|
15
|
+
from .core_handler import Urst
|
|
16
|
+
|
|
17
|
+
__version__ = "1.0.2"
|
|
18
|
+
__all__ = ["Urst"]
|
urst/codec_layer.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import logging
|
|
3
|
+
except ImportError:
|
|
4
|
+
from . import logging
|
|
5
|
+
|
|
6
|
+
# MicroPython compatibility for typing
|
|
7
|
+
try:
|
|
8
|
+
from typing import Any
|
|
9
|
+
except ImportError:
|
|
10
|
+
# Minimal fallback for MicroPython
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
# MicroPython compatibility for time
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
_ = time.ticks_ms
|
|
18
|
+
except AttributeError:
|
|
19
|
+
# Desktop Python shim
|
|
20
|
+
def ticks_ms():
|
|
21
|
+
return int(time.time() * 1000)
|
|
22
|
+
|
|
23
|
+
def ticks_diff(later, earlier):
|
|
24
|
+
return later - earlier
|
|
25
|
+
|
|
26
|
+
time.ticks_ms = ticks_ms
|
|
27
|
+
time.ticks_diff = ticks_diff
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def calculate_crc16(data: bytes | bytearray) -> int:
|
|
33
|
+
"""
|
|
34
|
+
Calculate the CRC16/CCITT-FALSE for the given data.
|
|
35
|
+
|
|
36
|
+
Poly: 0x1021, Init: 0xFFFF, No final XOR, MSB-first.
|
|
37
|
+
"""
|
|
38
|
+
crc = 0xFFFF
|
|
39
|
+
for byte in data:
|
|
40
|
+
crc ^= byte << 8
|
|
41
|
+
for _ in range(8):
|
|
42
|
+
crc = (crc << 1 ^ 4129) & 65535 if crc & 32768 else crc << 1 & 65535
|
|
43
|
+
return crc
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def serialize_crc(crc):
|
|
47
|
+
"""Serialize CRC into 2 bytes, little-endian (no struct dependency)."""
|
|
48
|
+
return bytes([crc & 0xFF, (crc >> 8) & 0xFF])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cobs_encode(data: bytes | bytearray) -> bytes:
|
|
52
|
+
"""
|
|
53
|
+
Encode data using Consistent Overhead Byte Stuffing (COBS).
|
|
54
|
+
"""
|
|
55
|
+
output = bytearray()
|
|
56
|
+
code_index = 0
|
|
57
|
+
code = 1
|
|
58
|
+
output.append(0)
|
|
59
|
+
|
|
60
|
+
for byte in data:
|
|
61
|
+
if byte == 0:
|
|
62
|
+
output[code_index] = code
|
|
63
|
+
code_index = len(output)
|
|
64
|
+
output.append(0)
|
|
65
|
+
code = 1
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
output.append(byte)
|
|
69
|
+
code += 1
|
|
70
|
+
|
|
71
|
+
if code == 0xFF:
|
|
72
|
+
output[code_index] = code
|
|
73
|
+
code_index = len(output)
|
|
74
|
+
output.append(0)
|
|
75
|
+
code = 1
|
|
76
|
+
|
|
77
|
+
output[code_index] = code
|
|
78
|
+
return bytes(output)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cobs_decode(data: bytes | bytearray) -> bytes | None:
|
|
82
|
+
"""
|
|
83
|
+
Decode COBS-encoded data.
|
|
84
|
+
"""
|
|
85
|
+
if len(data) == 0:
|
|
86
|
+
return None
|
|
87
|
+
if 0x00 in data:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
output = bytearray()
|
|
91
|
+
index = 0
|
|
92
|
+
size = len(data)
|
|
93
|
+
|
|
94
|
+
while index < size:
|
|
95
|
+
code = data[index]
|
|
96
|
+
if code == 0:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
index += 1
|
|
100
|
+
end = index + code - 1
|
|
101
|
+
if end > size:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
output.extend(data[index:end])
|
|
105
|
+
index = end
|
|
106
|
+
|
|
107
|
+
if code < 0xFF and index < size:
|
|
108
|
+
output.append(0x00)
|
|
109
|
+
|
|
110
|
+
return bytes(output)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CodecLayer:
|
|
114
|
+
"""
|
|
115
|
+
Handles encoding and decoding of URST packets at the byte level.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, ser: Any):
|
|
119
|
+
self.ser = ser
|
|
120
|
+
self._rx_buffer = bytearray()
|
|
121
|
+
logger.debug("Initializing Codec Layer")
|
|
122
|
+
|
|
123
|
+
def write_frame(self, frame: bytes) -> int:
|
|
124
|
+
"""
|
|
125
|
+
Write a complete physical frame to the serial port.
|
|
126
|
+
"""
|
|
127
|
+
sent = self.ser.write(frame)
|
|
128
|
+
if hasattr(self.ser, "flush"):
|
|
129
|
+
self.ser.flush()
|
|
130
|
+
return sent
|
|
131
|
+
|
|
132
|
+
def read_frame(self, timeout_ms: int = 1000) -> bytes | None:
|
|
133
|
+
"""
|
|
134
|
+
Read from serial until a complete frame is found (between two 0x00 delimiters).
|
|
135
|
+
"""
|
|
136
|
+
start_time = time.ticks_ms()
|
|
137
|
+
|
|
138
|
+
while time.ticks_diff(time.ticks_ms(), start_time) < timeout_ms:
|
|
139
|
+
# Check if we already have a frame in the buffer
|
|
140
|
+
if b"\x00" in self._rx_buffer:
|
|
141
|
+
# Find the first 0x00
|
|
142
|
+
first_zero = self._rx_buffer.find(b"\x00")
|
|
143
|
+
|
|
144
|
+
# If there's another 0x00 after it, we might have a frame
|
|
145
|
+
second_zero = self._rx_buffer.find(b"\x00", first_zero + 1)
|
|
146
|
+
if second_zero != -1:
|
|
147
|
+
# Extract the frame
|
|
148
|
+
frame = bytes(self._rx_buffer[first_zero : second_zero + 1])
|
|
149
|
+
# Remove from buffer by reassigning to remaining slice
|
|
150
|
+
self._rx_buffer = self._rx_buffer[second_zero + 1 :]
|
|
151
|
+
|
|
152
|
+
# If it's just two 0x00s with nothing between, it's not a valid frame
|
|
153
|
+
if len(frame) <= 2:
|
|
154
|
+
continue
|
|
155
|
+
return frame
|
|
156
|
+
|
|
157
|
+
# If no second zero, but we have a lot of junk before first zero, clear it
|
|
158
|
+
if first_zero > 0:
|
|
159
|
+
self._rx_buffer = self._rx_buffer[first_zero:]
|
|
160
|
+
|
|
161
|
+
# Read more data
|
|
162
|
+
if hasattr(self.ser, "in_waiting"):
|
|
163
|
+
bytes_to_read = max(1, self.ser.in_waiting)
|
|
164
|
+
elif hasattr(self.ser, "any"):
|
|
165
|
+
bytes_to_read = max(1, self.ser.any())
|
|
166
|
+
else:
|
|
167
|
+
bytes_to_read = 1
|
|
168
|
+
|
|
169
|
+
data = self.ser.read(bytes_to_read)
|
|
170
|
+
if data:
|
|
171
|
+
self._rx_buffer.extend(data)
|
|
172
|
+
else:
|
|
173
|
+
time.sleep(0.01) # Avoid busy waiting
|
|
174
|
+
|
|
175
|
+
return None
|
urst/constants.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# fmt: off
|
|
2
|
+
try:
|
|
3
|
+
from micropython import const
|
|
4
|
+
except ImportError:
|
|
5
|
+
def const(x): return x # noqa: E731
|
|
6
|
+
|
|
7
|
+
# Protocol Constants
|
|
8
|
+
MAX_RETRIES = const(3) # Maximum transmission attempts
|
|
9
|
+
ACK_TIMEOUT_MS = const(1000) # ACK timeout in milliseconds
|
|
10
|
+
MAX_PAYLOAD_SIZE = const(200) # Maximum payload bytes
|
|
11
|
+
RX_BUFFER_SIZE = const(512) # Receive buffer size in bytes
|
|
12
|
+
MAX_MSG_BYTES = const(8192) # Maximum message bytes advertised
|
|
13
|
+
MAX_FRAGMENTS = const(32) # Maximum fragments per message
|
|
14
|
+
CONSECUTIVE_COBS_FAILS = const(5) # Consecutive COBS fails threshold
|
|
15
|
+
|
|
16
|
+
# Frame Types
|
|
17
|
+
FRAME_DATA = const(0x01) # Application data frame
|
|
18
|
+
FRAME_ACK = const(0x02) # Acknowledgment (success)
|
|
19
|
+
FRAME_NAK = const(0x03) # Negative acknowledgment
|
|
20
|
+
FRAME_FRAG = const(0x04) # Fragmented message chunk
|
|
21
|
+
FRAME_CONNECT = const(0x05) # Connection establishment + caps
|
|
22
|
+
FRAME_CONNECT_ACK = const(0x06) # Connection acknowledgment + caps
|
|
23
|
+
FRAME_ERROR = const(0x07) # Receiver error / capability info
|
|
24
|
+
FRAME_ABORT = const(0x08) # Abort transmission of message
|
|
25
|
+
FRAME_BUSY = const(0x09) # Receiver busy (pause sending)
|
|
26
|
+
FRAME_READY = const(0x0A) # Receiver ready (resume sending)
|
|
27
|
+
|
|
28
|
+
FRAME_DELIMITER = const(0x00)
|
urst/core_handler.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import logging
|
|
3
|
+
except ImportError:
|
|
4
|
+
from . import logging
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# MicroPython compatibility for typing
|
|
8
|
+
try:
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
except ImportError:
|
|
11
|
+
TYPE_CHECKING = False
|
|
12
|
+
# Minimal fallback
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
from . import constants
|
|
16
|
+
from .codec_layer import CodecLayer
|
|
17
|
+
from .protocol_layer import ProtocolLayer
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
pass # type: ignore
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Urst:
|
|
26
|
+
"""
|
|
27
|
+
Main interface for the Universal Reliable Serial Transport (URST) protocol.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, port: Any, baud: int = 57600, *, timeout: float = 1.0):
|
|
31
|
+
logger.debug("Initializing Urst")
|
|
32
|
+
self.port = port
|
|
33
|
+
self.baud = baud
|
|
34
|
+
self.timeout = timeout
|
|
35
|
+
|
|
36
|
+
if sys.implementation.name == "micropython":
|
|
37
|
+
import machine
|
|
38
|
+
|
|
39
|
+
if isinstance(port, machine.UART):
|
|
40
|
+
self.ser = port
|
|
41
|
+
else:
|
|
42
|
+
# port could be id (int)
|
|
43
|
+
self.ser = machine.UART(port, baudrate=baud) # type: ignore
|
|
44
|
+
else:
|
|
45
|
+
# Desktop implementation
|
|
46
|
+
if hasattr(port, "write") and hasattr(port, "read"):
|
|
47
|
+
# Already a serial-like object (e.g. mock or already opened serial)
|
|
48
|
+
self.ser = port
|
|
49
|
+
else:
|
|
50
|
+
try:
|
|
51
|
+
from serial import Serial as SerialImpl # type: ignore
|
|
52
|
+
|
|
53
|
+
self.ser = SerialImpl(
|
|
54
|
+
port=port, baudrate=baud, timeout=timeout
|
|
55
|
+
)
|
|
56
|
+
except ImportError as exc:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
"pyserial is required to use Urst on desktop Python"
|
|
59
|
+
) from exc
|
|
60
|
+
|
|
61
|
+
self.codec = CodecLayer(self.ser)
|
|
62
|
+
self.protocol = ProtocolLayer(self.codec)
|
|
63
|
+
self._msg_id = 0
|
|
64
|
+
self._reassembly: dict[int, Any] = {}
|
|
65
|
+
|
|
66
|
+
def send(self, data: bytes) -> int:
|
|
67
|
+
"""
|
|
68
|
+
Send data over the URST transport with automatic fragmentation and reliability.
|
|
69
|
+
"""
|
|
70
|
+
max_frag_data = constants.MAX_PAYLOAD_SIZE - 6 # 194 bytes
|
|
71
|
+
|
|
72
|
+
if len(data) <= max_frag_data:
|
|
73
|
+
if self.protocol.send_reliable(constants.FRAME_DATA, data):
|
|
74
|
+
return len(data)
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
total_frags = (len(data) + max_frag_data - 1) // max_frag_data
|
|
78
|
+
msg_id = self._msg_id
|
|
79
|
+
self._msg_id = (self._msg_id + 1) & 0xFF
|
|
80
|
+
|
|
81
|
+
for i in range(total_frags):
|
|
82
|
+
chunk = data[i * max_frag_data : (i + 1) * max_frag_data]
|
|
83
|
+
# Fragment payload structure (§6.2)
|
|
84
|
+
header = bytes([msg_id, i, total_frags, len(chunk)])
|
|
85
|
+
if not self.protocol.send_reliable(
|
|
86
|
+
constants.FRAME_FRAG, header + chunk
|
|
87
|
+
):
|
|
88
|
+
return i * max_frag_data
|
|
89
|
+
|
|
90
|
+
return len(data)
|
|
91
|
+
|
|
92
|
+
def read(self, bytes_to_read: int = -1) -> bytes:
|
|
93
|
+
"""
|
|
94
|
+
Read a complete URST message (reassembled if necessary).
|
|
95
|
+
"""
|
|
96
|
+
while True:
|
|
97
|
+
frame = self.protocol.receive_frame()
|
|
98
|
+
if not frame:
|
|
99
|
+
return b"" # Timeout or duplicate frame (already ACKed)
|
|
100
|
+
|
|
101
|
+
frame_type = frame["type"]
|
|
102
|
+
payload = frame["payload"]
|
|
103
|
+
|
|
104
|
+
if frame_type == constants.FRAME_DATA:
|
|
105
|
+
return payload
|
|
106
|
+
|
|
107
|
+
if frame_type == constants.FRAME_FRAG:
|
|
108
|
+
if len(payload) < 4:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
msg_id = payload[0]
|
|
112
|
+
frag_num = payload[1]
|
|
113
|
+
total = payload[2]
|
|
114
|
+
data_len = payload[3]
|
|
115
|
+
data = payload[4 : 4 + data_len]
|
|
116
|
+
|
|
117
|
+
if msg_id not in self._reassembly:
|
|
118
|
+
self._reassembly[msg_id] = {"total": total, "fragments": {}}
|
|
119
|
+
|
|
120
|
+
self._reassembly[msg_id]["fragments"][frag_num] = data
|
|
121
|
+
|
|
122
|
+
if len(self._reassembly[msg_id]["fragments"]) == total:
|
|
123
|
+
# Reassemble message
|
|
124
|
+
msg = b"".join(
|
|
125
|
+
self._reassembly[msg_id]["fragments"][j]
|
|
126
|
+
for j in range(total)
|
|
127
|
+
)
|
|
128
|
+
del self._reassembly[msg_id]
|
|
129
|
+
return msg
|
|
130
|
+
|
|
131
|
+
# Handle other frame types or continue waiting
|
|
132
|
+
if (
|
|
133
|
+
frame_type == constants.FRAME_CONNECT
|
|
134
|
+
or frame_type == constants.FRAME_CONNECT_ACK
|
|
135
|
+
):
|
|
136
|
+
continue
|
urst/protocol_layer.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import logging
|
|
3
|
+
except ImportError:
|
|
4
|
+
from . import logging
|
|
5
|
+
|
|
6
|
+
import struct
|
|
7
|
+
|
|
8
|
+
# Pre-computed CONNECT capability payload (fixed protocol constants).
|
|
9
|
+
# Hoisted to avoid re-running struct.pack on every handshake call.
|
|
10
|
+
_CONNECT_PAYLOAD = struct.pack("<BHBBHBB", 4, 8192, 32, 1, 1000, 3, 0)
|
|
11
|
+
|
|
12
|
+
# MicroPython compatibility for typing
|
|
13
|
+
try: # noqa: SIM105
|
|
14
|
+
from typing import Any
|
|
15
|
+
except ImportError:
|
|
16
|
+
# Minimal fallback for MicroPython
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
# MicroPython compatibility for time
|
|
20
|
+
import time # noqa: E402
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
_ = time.ticks_ms # type: ignore
|
|
24
|
+
except AttributeError:
|
|
25
|
+
# Desktop Python shim
|
|
26
|
+
def ticks_ms():
|
|
27
|
+
return int(time.time() * 1000)
|
|
28
|
+
|
|
29
|
+
def ticks_diff(later, earlier):
|
|
30
|
+
return later - earlier
|
|
31
|
+
|
|
32
|
+
time.ticks_ms = ticks_ms # type: ignore
|
|
33
|
+
time.ticks_diff = ticks_diff # type: ignore
|
|
34
|
+
|
|
35
|
+
from collections import deque # noqa: E402
|
|
36
|
+
|
|
37
|
+
from . import constants # noqa: E402
|
|
38
|
+
from .codec_layer import ( # noqa: E402
|
|
39
|
+
calculate_crc16,
|
|
40
|
+
cobs_decode,
|
|
41
|
+
cobs_encode,
|
|
42
|
+
serialize_crc,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
# Module-level tuples avoid re-allocating set objects on each membership test.
|
|
48
|
+
# Using tuple for MicroPython portability (frozenset availability varies by port).
|
|
49
|
+
_VALID_FRAME_TYPES = (
|
|
50
|
+
constants.FRAME_DATA,
|
|
51
|
+
constants.FRAME_ACK,
|
|
52
|
+
constants.FRAME_NAK,
|
|
53
|
+
constants.FRAME_FRAG,
|
|
54
|
+
constants.FRAME_CONNECT,
|
|
55
|
+
constants.FRAME_CONNECT_ACK,
|
|
56
|
+
constants.FRAME_ERROR,
|
|
57
|
+
constants.FRAME_ABORT,
|
|
58
|
+
constants.FRAME_BUSY,
|
|
59
|
+
constants.FRAME_READY,
|
|
60
|
+
)
|
|
61
|
+
_EMPTY_PAYLOAD_TYPES = (
|
|
62
|
+
constants.FRAME_ACK,
|
|
63
|
+
constants.FRAME_NAK,
|
|
64
|
+
constants.FRAME_BUSY,
|
|
65
|
+
constants.FRAME_READY,
|
|
66
|
+
)
|
|
67
|
+
_DATA_FRAG_TYPES = (constants.FRAME_DATA, constants.FRAME_FRAG)
|
|
68
|
+
_PAYLOAD_FRAME_TYPES = (
|
|
69
|
+
constants.FRAME_DATA,
|
|
70
|
+
constants.FRAME_FRAG,
|
|
71
|
+
constants.FRAME_CONNECT,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_empty_payload_only_type(frame_type):
|
|
76
|
+
return frame_type in _EMPTY_PAYLOAD_TYPES
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_frame(frame_type: int, seq: int, payload: bytes = b"") -> bytes:
|
|
80
|
+
"""
|
|
81
|
+
Build a complete physical frame (delimiter + COBS + delimiter).
|
|
82
|
+
|
|
83
|
+
Order: Header (type, seq) -> Payload -> CRC -> COBS -> Delimiters.
|
|
84
|
+
"""
|
|
85
|
+
if frame_type not in _VALID_FRAME_TYPES:
|
|
86
|
+
raise ValueError(f"Unknown frame type: {frame_type}")
|
|
87
|
+
if not 0 <= seq <= 0xFF:
|
|
88
|
+
raise ValueError("Sequence number must be in range 0..255")
|
|
89
|
+
|
|
90
|
+
logical = bytes([frame_type, seq]) + payload
|
|
91
|
+
crc = calculate_crc16(logical)
|
|
92
|
+
with_crc = logical + serialize_crc(crc)
|
|
93
|
+
encoded = cobs_encode(with_crc)
|
|
94
|
+
delimiter = bytes([constants.FRAME_DELIMITER])
|
|
95
|
+
return delimiter + encoded + delimiter
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_frame(raw: bytes) -> dict | None:
|
|
99
|
+
"""
|
|
100
|
+
Strips delimiters, COBS-decodes, validates CRC, and parses the header.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
{'type': int, 'seq': int, 'payload': bytes} or None on failure.
|
|
104
|
+
"""
|
|
105
|
+
if len(raw) < 3:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
delimiter = constants.FRAME_DELIMITER
|
|
109
|
+
if raw[0] != delimiter or raw[-1] != delimiter:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
encoded = raw[1:-1]
|
|
113
|
+
decoded = cobs_decode(encoded)
|
|
114
|
+
if decoded is None or len(decoded) < 4:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
payload_with_header = decoded[:-2]
|
|
118
|
+
received_crc = int.from_bytes(decoded[-2:], "little")
|
|
119
|
+
expected_crc = calculate_crc16(payload_with_header)
|
|
120
|
+
if received_crc != expected_crc:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
frame_type = payload_with_header[0]
|
|
124
|
+
seq = payload_with_header[1]
|
|
125
|
+
payload = payload_with_header[2:]
|
|
126
|
+
|
|
127
|
+
if frame_type not in _VALID_FRAME_TYPES:
|
|
128
|
+
return None
|
|
129
|
+
if (
|
|
130
|
+
len(payload) > constants.MAX_PAYLOAD_SIZE
|
|
131
|
+
and len(payload) != 252
|
|
132
|
+
and frame_type == constants.FRAME_DATA
|
|
133
|
+
):
|
|
134
|
+
return None
|
|
135
|
+
if _is_empty_payload_only_type(frame_type) and payload:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
return {"type": frame_type, "seq": seq, "payload": payload}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ProtocolLayer:
|
|
142
|
+
"""
|
|
143
|
+
Handles the URST protocol logic, including sequence management and reliable delivery.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, codec: Any):
|
|
147
|
+
self.codec = codec
|
|
148
|
+
self.next_send_seq = 0
|
|
149
|
+
self.expected_recv_seq = 0
|
|
150
|
+
self.last_received_seq = -1
|
|
151
|
+
self.is_connected = False
|
|
152
|
+
self._recv_queue = deque(
|
|
153
|
+
(), constants.MAX_FRAGMENTS
|
|
154
|
+
) # O(1) popleft on MicroPython
|
|
155
|
+
logger.debug("Initializing Protocol Layer")
|
|
156
|
+
|
|
157
|
+
def connect(self) -> bool:
|
|
158
|
+
"""Perform the CONNECT handshake with retries (§5.6)."""
|
|
159
|
+
payload = _CONNECT_PAYLOAD
|
|
160
|
+
for attempt in range(constants.MAX_RETRIES + 1):
|
|
161
|
+
logger.debug(f"Handshake attempt {attempt + 1}")
|
|
162
|
+
self.codec.write_frame(
|
|
163
|
+
build_frame(constants.FRAME_CONNECT, 0, payload)
|
|
164
|
+
)
|
|
165
|
+
# Handshake must not use the queue as it needs fresh response
|
|
166
|
+
resp = self.codec.read_frame(constants.ACK_TIMEOUT_MS)
|
|
167
|
+
if resp:
|
|
168
|
+
p = parse_frame(resp)
|
|
169
|
+
if p and p["type"] == constants.FRAME_CONNECT_ACK:
|
|
170
|
+
(
|
|
171
|
+
self.next_send_seq,
|
|
172
|
+
self.expected_recv_seq,
|
|
173
|
+
self.last_received_seq,
|
|
174
|
+
) = 0, 0, -1
|
|
175
|
+
self.is_connected = True
|
|
176
|
+
logger.debug("URST Connected (received CONNECT_ACK)")
|
|
177
|
+
return True
|
|
178
|
+
if p and p["type"] == constants.FRAME_CONNECT:
|
|
179
|
+
self.codec.write_frame(
|
|
180
|
+
build_frame(
|
|
181
|
+
constants.FRAME_CONNECT_ACK, p["seq"], payload
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
(
|
|
185
|
+
self.next_send_seq,
|
|
186
|
+
self.expected_recv_seq,
|
|
187
|
+
self.last_received_seq,
|
|
188
|
+
) = 0, 0, -1
|
|
189
|
+
self.is_connected = True
|
|
190
|
+
logger.debug(
|
|
191
|
+
"URST Connected (simultaneous CONNECT resolved)"
|
|
192
|
+
)
|
|
193
|
+
return True
|
|
194
|
+
if p:
|
|
195
|
+
logger.warning(
|
|
196
|
+
f"Unexpected frame during handshake: {p['type']}"
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
logger.warning("Handshake timeout")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def send_reliable(self, frame_type: int, payload: bytes) -> bool:
|
|
203
|
+
"""Send a frame reliably using stop-and-wait (§5.1.1)."""
|
|
204
|
+
if (
|
|
205
|
+
not self.is_connected
|
|
206
|
+
and frame_type != constants.FRAME_CONNECT
|
|
207
|
+
and not self.connect()
|
|
208
|
+
):
|
|
209
|
+
logger.error("Failed to establish connection before sending")
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
seq = self.next_send_seq
|
|
213
|
+
frame = build_frame(frame_type, seq, payload)
|
|
214
|
+
|
|
215
|
+
for attempt in range(constants.MAX_RETRIES + 1):
|
|
216
|
+
logger.debug(
|
|
217
|
+
f"Sending frame type {frame_type:#x}, seq {seq}, attempt {attempt + 1}"
|
|
218
|
+
)
|
|
219
|
+
self.codec.write_frame(frame)
|
|
220
|
+
|
|
221
|
+
start_wait = time.ticks_ms() # type: ignore
|
|
222
|
+
while (
|
|
223
|
+
time.ticks_diff(time.ticks_ms(), start_wait)
|
|
224
|
+
< constants.ACK_TIMEOUT_MS
|
|
225
|
+
): # type: ignore
|
|
226
|
+
# Read fresh frames only, bypassing the queue
|
|
227
|
+
p = self.receive_frame(timeout_ms=100, use_queue=False)
|
|
228
|
+
if p:
|
|
229
|
+
if p["type"] == constants.FRAME_ACK and p["seq"] == seq:
|
|
230
|
+
self.next_send_seq = (self.next_send_seq + 1) & 0xFF
|
|
231
|
+
logger.debug(f"Received ACK for seq {seq}")
|
|
232
|
+
return True
|
|
233
|
+
if p["type"] == constants.FRAME_NAK and p["seq"] == seq:
|
|
234
|
+
logger.warning(
|
|
235
|
+
f"Received NAK for seq {seq}, retrying..."
|
|
236
|
+
)
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
# If it's a payload frame, it's already been ACKed by receive_frame.
|
|
240
|
+
# We must queue it so Urst.read() can find it later.
|
|
241
|
+
if p["type"] in _DATA_FRAG_TYPES:
|
|
242
|
+
logger.debug(
|
|
243
|
+
f"Queuing payload frame type {p['type']} received during wait"
|
|
244
|
+
)
|
|
245
|
+
self._recv_queue.append(p)
|
|
246
|
+
else:
|
|
247
|
+
logger.warning(f"Timeout waiting for ACK for seq {seq}")
|
|
248
|
+
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
def receive_frame(
|
|
252
|
+
self, timeout_ms: int | None = None, use_queue: bool = True
|
|
253
|
+
) -> dict | None:
|
|
254
|
+
"""Receive a frame and handle ACKs/seq checks (§5.1.2, §5.6.2)."""
|
|
255
|
+
if use_queue and self._recv_queue:
|
|
256
|
+
return self._recv_queue.popleft()
|
|
257
|
+
|
|
258
|
+
if timeout_ms is None:
|
|
259
|
+
timeout_ms = constants.ACK_TIMEOUT_MS
|
|
260
|
+
|
|
261
|
+
raw = self.codec.read_frame(timeout_ms)
|
|
262
|
+
if not raw:
|
|
263
|
+
return None
|
|
264
|
+
p = parse_frame(raw)
|
|
265
|
+
if not p:
|
|
266
|
+
return None
|
|
267
|
+
ft, seq = p["type"], p["seq"]
|
|
268
|
+
|
|
269
|
+
if ft == constants.FRAME_ACK or ft == constants.FRAME_NAK:
|
|
270
|
+
return p
|
|
271
|
+
|
|
272
|
+
if ft in _PAYLOAD_FRAME_TYPES:
|
|
273
|
+
if ft == constants.FRAME_CONNECT or seq == self.expected_recv_seq:
|
|
274
|
+
if ft == constants.FRAME_CONNECT:
|
|
275
|
+
payload = _CONNECT_PAYLOAD
|
|
276
|
+
self.codec.write_frame(
|
|
277
|
+
build_frame(constants.FRAME_CONNECT_ACK, seq, payload)
|
|
278
|
+
)
|
|
279
|
+
(
|
|
280
|
+
self.next_send_seq,
|
|
281
|
+
self.expected_recv_seq,
|
|
282
|
+
self.last_received_seq,
|
|
283
|
+
) = 0, 0, -1
|
|
284
|
+
self.is_connected = True
|
|
285
|
+
return p
|
|
286
|
+
self.codec.write_frame(build_frame(constants.FRAME_ACK, seq))
|
|
287
|
+
self.last_received_seq, self.expected_recv_seq = (
|
|
288
|
+
seq,
|
|
289
|
+
(self.expected_recv_seq + 1) & 0xFF,
|
|
290
|
+
)
|
|
291
|
+
return p
|
|
292
|
+
if seq == self.last_received_seq:
|
|
293
|
+
self.codec.write_frame(build_frame(constants.FRAME_ACK, seq))
|
|
294
|
+
return None
|
|
295
|
+
self.codec.write_frame(build_frame(constants.FRAME_NAK, seq))
|
|
296
|
+
return p
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: urst-mpy
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: MicroPython implementation of the Universal Reliable Serial Transport protocol [URST](https://github.com/simonl65/URST-Specification/blob/main/URST-Specification.md)
|
|
5
|
+
Author: Simon R. Lincoln
|
|
6
|
+
Author-email: Simon R. Lincoln <oss@codeability.co.uk>
|
|
7
|
+
License-Expression: SUL-1.0
|
|
8
|
+
License-File: LICENSE.md
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
11
|
+
Classifier: Topic :: System :: Networking
|
|
12
|
+
Classifier: Topic :: Communications
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Dist: pyserial>=3.5
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Project-URL: Repository, https://github.com/simonl65/URST-mpy
|
|
18
|
+
Project-URL: Bug Tracker, https://github.com/simonl65/URST-mpy/issues
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# URST (Universal Reliable Serial Transport) for MicroPython
|
|
22
|
+
|
|
23
|
+
[](LICENSE.md)
|
|
24
|
+
|
|
25
|
+
**URST for MicroPython** is a professional-grade implementation of the [Universal Reliable Serial Transport (URST) protocol](URST-Specification.md). It provides reliable, error-checked, and fragmented data transmission over unreliable serial (UART/XBee) connections, specifically optimized for MicroPython devices like the Raspberry Pi Pico and ESP32.
|
|
26
|
+
|
|
27
|
+
## Key Features
|
|
28
|
+
|
|
29
|
+
- **Reliable Delivery**: Strict stop-and-wait ARQ (Automatic Repeat Request) with configurable timeouts and retries.
|
|
30
|
+
- **MicroPython Optimized**: Native support for `machine.UART` and `utime.ticks_ms()` for precise timing on hardware.
|
|
31
|
+
- **Hardware Agnostic**: Works on Desktop Python (via `pyserial`) and MicroPython seamlessly.
|
|
32
|
+
- **Error Detection**: Robust CRC-16/CCITT_FALSE validation for every frame.
|
|
33
|
+
- **Robust Framing**: Uses COBS (Consistent Overhead Byte Stuffing) for zero-byte-free encoding, ensuring unambiguous frame delimiting via `0x00`.
|
|
34
|
+
- **Message Fragmentation**: Automatically handles messages larger than the physical MTU (up to 8KB+ reassembly).
|
|
35
|
+
- **Connection Handshake**: Built-in capability negotiation and sequence synchronization.
|
|
36
|
+
- **Simple API**: Clean `send()` and `read()` interface that abstracts away the complexity of serial framing and retransmission.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### For MicroPython Devices
|
|
41
|
+
|
|
42
|
+
**Option A — Source install via mip (simplest):**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
mpremote mip install github:simonl65/URST-mpy
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Option B — Pre-compiled .mpy (smallest flash footprint, fastest startup):**
|
|
49
|
+
|
|
50
|
+
Pre-compiling with `mpy-cross` reduces the package from ~19.7 KB to ~6.4 KB on flash
|
|
51
|
+
and eliminates the parse-and-compile step at boot time.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 1. Install mpy-cross (once)
|
|
55
|
+
pip install mpy-cross
|
|
56
|
+
|
|
57
|
+
# 2. Clone the repo and build
|
|
58
|
+
git clone https://github.com/simonl65/urst-mpy.git
|
|
59
|
+
cd urst-mpy
|
|
60
|
+
make mpy # produces dist/urst/*.mpy
|
|
61
|
+
|
|
62
|
+
# 3. Deploy the compiled files to your device
|
|
63
|
+
mpremote cp -r dist/urst :
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Option C — Copy source directly:**
|
|
67
|
+
|
|
68
|
+
Copy the `urst/` directory from this repository to the root of your MicroPython device's filesystem.
|
|
69
|
+
|
|
70
|
+
### For Desktop Development
|
|
71
|
+
|
|
72
|
+
If you want to use it on your PC (e.g., for testing or gateway applications), install `pyserial` first:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install pyserial
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick Start (MicroPython)
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import urst
|
|
82
|
+
import machine
|
|
83
|
+
import time
|
|
84
|
+
|
|
85
|
+
# 1. Initialize UART on your device (e.g., Raspberry Pi Pico)
|
|
86
|
+
uart = machine.UART(0, baudrate=57600, tx=machine.Pin(0), rx=machine.Pin(1))
|
|
87
|
+
|
|
88
|
+
# 2. Initialize URST with the UART object
|
|
89
|
+
transport = urst.Urst(uart)
|
|
90
|
+
|
|
91
|
+
# 3. Send a message (automatically handles framing, CRC, and ACK waiting)
|
|
92
|
+
# It will fragment large data into ~194 byte chunks automatically.
|
|
93
|
+
transport.send(b"Hello from Pico!")
|
|
94
|
+
|
|
95
|
+
# 4. Read a complete message (handles reassembly of fragments)
|
|
96
|
+
while True:
|
|
97
|
+
message = transport.read()
|
|
98
|
+
if message:
|
|
99
|
+
print(f"Received: {message.decode()}")
|
|
100
|
+
time.sleep(0.1)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Quick Start (Desktop Python)
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from urst import Urst
|
|
107
|
+
|
|
108
|
+
# Initialize URST on your serial port (requires pyserial)
|
|
109
|
+
transport = Urst(port="/dev/ttyUSB0", baud=57600)
|
|
110
|
+
|
|
111
|
+
transport.send(b"Hello from Desktop!")
|
|
112
|
+
message = transport.read()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Protocol Architecture
|
|
116
|
+
|
|
117
|
+
URST follows a strictly layered architecture to ensure separation of concerns:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
┌───────────────────────────────────┐
|
|
121
|
+
│ Handler Layer (Application) │ User API: send(), read()
|
|
122
|
+
├───────────────────────────────────┤
|
|
123
|
+
│ Protocol Layer (Reliable) │ CONNECT/ACK/NAK, Retransmission
|
|
124
|
+
├───────────────────────────────────┤
|
|
125
|
+
│ Transport Layer (Framing) │ Frame Type, Sequence Numbers
|
|
126
|
+
├───────────────────────────────────┤
|
|
127
|
+
│ Codec Layer (Encoding/IO) │ COBS, CRC, UART (machine/pyserial)
|
|
128
|
+
└───────────────────────────────────┘
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For full technical details, please refer to the [URST Specification](URST-Specification.md).
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
### Setup
|
|
136
|
+
|
|
137
|
+
This project uses [uv](https://docs.astral.sh/uv/) for local development and testing.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
git clone https://github.com/simonl65/urst-mpy.git
|
|
141
|
+
cd urst-mpy
|
|
142
|
+
uv sync
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Running Tests
|
|
146
|
+
|
|
147
|
+
If using UV:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv run pytest
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If you aren't using UV you should set `PYTHONPATH` to ensure the tests find the package correctly:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
PYTHONPATH=. pytest
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Linting & Formatting
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
uv run ruff check .
|
|
163
|
+
uv run ruff format .
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
This project is licensed under the **Sustainable Use License (SUL-1.0)**. See the [LICENSE.md](LICENSE.md) file for details.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
**Author:** Simon R. Lincoln ([oss@codeability.co.uk](mailto:oss@codeability.co.uk))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
urst/__init__.py,sha256=ajM8hU9IlRsgGsaxBLU_uSg7YYy25YPfLnFAzvwL09Q,396
|
|
2
|
+
urst/codec_layer.py,sha256=RVf-Oo-0EhuJmSNVjSF_SolT2CFFA-WNl26-k6Ml4Ds,4697
|
|
3
|
+
urst/constants.py,sha256=kaSz_78ORW01EYXEUr6Pdew8ZgrC7EsJCWf8b2yyFh8,1339
|
|
4
|
+
urst/core_handler.py,sha256=3jYAd_P4zCBmAgjNKzdB1_cmBxD7IGjBVHxElMLWRPw,4408
|
|
5
|
+
urst/protocol_layer.py,sha256=VY7RQ49RGWUMWLYa3zgSriZj0G7XHoVANDfaHNMfX3g,10111
|
|
6
|
+
urst_mpy-1.0.2.dist-info/licenses/LICENSE.md,sha256=5eiqjQdY743BuzG_mL_H2XX-KrdaBnspDKR1qw5Fyjs,3629
|
|
7
|
+
urst_mpy-1.0.2.dist-info/WHEEL,sha256=bu7Cckf7DKpj8ztfOQHBiWtXM4xigujiTkrhvV6U2aU,81
|
|
8
|
+
urst_mpy-1.0.2.dist-info/METADATA,sha256=fL0zwgnD8AQWYmtMDIkVNTrLuSURkeFpMMGUCGmzHCs,5924
|
|
9
|
+
urst_mpy-1.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Sustainable Use License
|
|
2
|
+
|
|
3
|
+
Version 1.0
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
|
12
|
+
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
|
13
|
+
to the limitations below.
|
|
14
|
+
|
|
15
|
+
## Limitations
|
|
16
|
+
|
|
17
|
+
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
|
18
|
+
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
|
19
|
+
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
|
20
|
+
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
|
21
|
+
|
|
22
|
+
## Patents
|
|
23
|
+
|
|
24
|
+
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
|
25
|
+
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
|
26
|
+
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
|
27
|
+
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
|
28
|
+
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
|
29
|
+
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
|
30
|
+
license ends immediately for work on behalf of your company.
|
|
31
|
+
|
|
32
|
+
## Notices
|
|
33
|
+
|
|
34
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
|
35
|
+
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
|
36
|
+
stating that you have modified the software.
|
|
37
|
+
|
|
38
|
+
## No Other Rights
|
|
39
|
+
|
|
40
|
+
These terms do not imply any licenses other than those expressly granted in these terms.
|
|
41
|
+
|
|
42
|
+
## Termination
|
|
43
|
+
|
|
44
|
+
If you use the software in violation of these terms, such use is not licensed, and your license will
|
|
45
|
+
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
|
46
|
+
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
|
47
|
+
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
|
48
|
+
terms will cause your license to terminate automatically and permanently.
|
|
49
|
+
|
|
50
|
+
## No Liability
|
|
51
|
+
|
|
52
|
+
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
|
53
|
+
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
|
54
|
+
any kind of legal claim.
|
|
55
|
+
|
|
56
|
+
## Definitions
|
|
57
|
+
|
|
58
|
+
The “licensor” is the entity offering these terms.
|
|
59
|
+
|
|
60
|
+
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
|
61
|
+
|
|
62
|
+
“You” refers to the individual or entity agreeing to these terms.
|
|
63
|
+
|
|
64
|
+
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
|
65
|
+
all organizations that have control over, are under the control of, or are under common control with that
|
|
66
|
+
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
|
67
|
+
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
68
|
+
|
|
69
|
+
“Your license” is the license granted to you for the software under these terms.
|
|
70
|
+
|
|
71
|
+
“Use” means anything you do with the software requiring your license.
|
|
72
|
+
|
|
73
|
+
“Trademark” means trademarks, service marks, and similar rights.
|