Skip to content

Commit bdc4a92

Browse files
committed
Fix bobjacobsen#81: Clear others from maps if AME has no data, even if not Permitted, as per Standard; Test exact length of data when matching AME as per Standard. Move decodeControlFrameFormat to CanFrame as static method for modularity (no CanLink instance is required). Add noise rejection (move old handleData code to handleDataOptimized) (fix bobjacobsen#82 and fix bobjacobsen#83 before committing). Add reusable from*_hex_bytes functions. Make handleData and CanFrame constructor messages more explicit. Fix bobjacobsen#86: Add minimumState option to CanFrame so transition to Inhibited can send AMR after CIDs are queued but not sent, since enqueuing since enqueuing changes CanLink state immediately. Fix bobjacobsen#77: Always increment nextInternallyAssignedNodeID when used. Add explicit blockedReason function for tracing.
1 parent d6ab727 commit bdc4a92

10 files changed

Lines changed: 373 additions & 121 deletions

openlcb/__init__.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,29 @@
88
Union, # in case `|` doesn't support 'type' in this Python version
99
)
1010

11-
12-
hex_pairs_rc = re.compile(r"^([0-9A-Fa-f]{2})+$")
11+
hex_pairs_re = r"^([0-9A-Fa-f]{2})+$"
12+
hex_pairs_rc = re.compile(hex_pairs_re)
13+
hex_pairs_brc = re.compile(hex_pairs_re.encode("utf-8"))
1314
# {2}: Exactly two characters found (only match if pair)
1415
# +: at least one match plus 0 or more additional matches
16+
ORD_0 = 0x30
17+
ORD_9 = 0x39
18+
ORD_A = 0x41
19+
ORD_F = 0x46
20+
ORD_Z = 0x5A
21+
ORD_a = 0x61
22+
ORD_f = 0x66
23+
ORD_z = 0x7A
1524

1625

1726
def only_hex_pairs(value: str) -> bool:
1827
"""Check if string contains only machine-readable hex pairs.
1928
See openlcb.conventions submodule for LCC ID dot notation
2029
functions (less restrictive).
2130
"""
31+
if isinstance(value, (bytearray, bytes)):
32+
return hex_pairs_brc.fullmatch(value)
33+
assert isinstance(value, str)
2234
return hex_pairs_rc.fullmatch(value)
2335

2436

@@ -77,3 +89,41 @@ def precise_sleep(seconds: Union[float, int], start: float = None) -> None:
7789

7890
def formatted_ex(ex) -> str:
7991
return "{}: {}".format(type(ex).__name__, ex)
92+
93+
94+
def from_hex_bytes(b: bytearray, start: int, stop: int, assertValid=True) -> bytearray:
95+
"""ASCII hex bytearray (even length) → binary bytearray"""
96+
# like bytearray.fromhex, except accepts bytes rather than str only
97+
r = bytearray((stop-start) // 2)
98+
if assertValid:
99+
if (stop-start) % 2 > 0:
100+
raise IndexError("Only hex pairs are accepted, got odd count: start={} stop={}".format(start, stop))
101+
if start < 0 or start > len(b):
102+
raise IndexError("start={} len={}".format(start, len(b)))
103+
if stop < 0 or stop > len(b):
104+
raise IndexError("stop={} len={}".format(start, len(b)))
105+
if stop - start < 2:
106+
raise IndexError("start={} stop={}".format(start, stop))
107+
assert len(r) == (stop - start) // 2
108+
i = start
109+
rel = 0
110+
while i < stop:
111+
x, y = b[i], b[i+1]
112+
if assertValid:
113+
if not ((x >= ORD_A and x <= ORD_F) or (x >= ORD_a and x <= ORD_f) or (x >= ORD_0 and x <= ORD_9)):
114+
raise ValueError("Got character {}, expected hex digit".format((bytearray([x])).decode("utf-8")))
115+
if not ((y >= ORD_A and y <= ORD_F) or (y >= ORD_a and y <= ORD_f) or (y >= ORD_0 and y <= ORD_9)):
116+
raise ValueError("Got character {}, expected hex digit".format((bytearray([y])).decode("utf-8")))
117+
# v =
118+
# NOTE: below will still raise exception if over 255 even if assertValid is False
119+
r[rel] = ((x & 15) + ((x >> 6) & 1) * 9) << 4 | \
120+
((y & 15) + ((y >> 6) & 1) * 9)
121+
# assert v < 256, str(b[i:i+1])
122+
# r[rel] = v
123+
i += 2
124+
rel += 1
125+
return r
126+
127+
128+
def from_all_hex_bytes(b: bytearray) -> bytearray:
129+
return from_hex_bytes(b, 0, len(b))

openlcb/canbus/canframe.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from collections import OrderedDict
44
from logging import getLogger
55

6+
from openlcb.canbus.controlframe import ControlFrame
67
from openlcb.nodeid import NodeID
78

89
logger = getLogger(__name__)
@@ -83,6 +84,28 @@ def constructor_help():
8384
result += "), "
8485
return result[:-2] # -1 to remove last ", " from list
8586

87+
@staticmethod
88+
def decodeControlFrameFormat(frame) -> ControlFrame:
89+
# type: (CanFrame) -> ControlFrame
90+
if (frame.header & 0x0800_0000) == 0x0800_0000:
91+
# data case; not checking leading 1 bit
92+
# NOTE: handleReceivedData can get all header bits via frame
93+
return ControlFrame.Data
94+
if (frame.header & 0x4_000_000) != 0: # CID case
95+
# NOTE: handleReceivedCID can get all header bits via frame
96+
return ControlFrame.CID
97+
98+
try:
99+
retval = ControlFrame((frame.header >> 12) & 0x2_FF_FF)
100+
return retval # top 1 bit for out-of-band messages
101+
except KeyboardInterrupt:
102+
raise
103+
except:
104+
logger.warning(
105+
"Could not decode header 0x{:08X}"
106+
.format(frame.header))
107+
return ControlFrame.UnknownFormat
108+
86109
def __str__(self):
87110
return "CanFrame header: 0x{:08X} {}".format(
88111
self.header,
@@ -99,10 +122,12 @@ def encodeAsBytes(self) -> bytes:
99122
def alias(self) -> int:
100123
return self._alias
101124

102-
def __init__(self, *args, afterSendState=None, reservation=None):
125+
def __init__(self, *args, afterSendState=None, reservation=None,
126+
minimumState=None):
103127
self.afterSendState = afterSendState
104128
self.encoder = NoEncoder()
105129
self.reservation = reservation
130+
self.minimumState = minimumState
106131
arg1 = None
107132
arg2 = None
108133
arg3 = None
@@ -144,9 +169,12 @@ def __init__(self, *args, afterSendState=None, reservation=None):
144169
# TODO: decode (header?) if self._alias is necessary in this case,
145170
# otherwise is remains None!
146171
if not isinstance(arg1, int):
147-
args_error = "Expected int since 2nd argument is bytearray."
172+
args_error = "Expected int(header) since 2nd argument is bytearray."
148173
# Types of both args are enforced by this point.
149174
self.header = arg1
175+
self._alias = arg1 & 0xFFF
176+
if self._alias == 0:
177+
logger.warning("Alias is {}".format(self._alias))
150178
self.data = arg2
151179
if len(args) > 2:
152180
args_error = "2nd argument is data, but got extra argument(s)"

0 commit comments

Comments
 (0)