Skip to content

Commit 2b735cf

Browse files
committed
Fix: clear old reservations (related to #35). Fix: Set WaitingForSendReserveID state.
1 parent dc7960e commit 2b735cf

1 file changed

Lines changed: 156 additions & 3 deletions

File tree

openlcb/canbus/canphysicallayergridconnect.py

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,20 @@
1111
'''
1212

1313

14-
from typing import Union
14+
import struct
15+
import sys
16+
17+
from logging import getLogger
18+
from typing import Tuple, Union
19+
1520
from openlcb.canbus.canphysicallayer import CanPhysicalLayer
1621
from openlcb.canbus.canframe import CanFrame
1722
from openlcb.frameencoder import FrameEncoder
1823
from openlcb.portinterface import PortInterface
1924

25+
logger = getLogger(__name__)
26+
27+
2028
GC_START_BYTE = 0x3a # :
2129
GC_END_BYTE = 0x3b # ;
2230

@@ -154,11 +162,70 @@ def handleDataString(self, string: str) -> int:
154162
# formerly pushString formerly receiveString
155163
return self.handleData(string.encode("utf-8"))
156164

157-
def handleData(self, data: Union[bytes, bytearray], verbose=False) -> int:
165+
@classmethod
166+
def nextPacketRange(cls, data: Union[bytes, bytearray],
167+
start: int=0) -> Tuple[int, int]:
168+
"""Get the packet slice if any.
169+
Returns:
170+
tuple(int, int): Position of ':' and ';', otherwise None.
171+
"""
172+
firstI = data.find(GC_START_BYTE, start)
173+
if firstI < 0:
174+
return None
175+
lastI = data.find(GC_END_BYTE, firstI+1)
176+
if lastI < 0:
177+
return None
178+
return (firstI, lastI+1)
179+
180+
@classmethod
181+
def nextPacket(cls, data: Union[bytes, bytearray]) -> bytearray:
182+
"""Get the packet including ':' and ';'."""
183+
indices = cls.nextPacketRange(data)
184+
if indices is None:
185+
return None
186+
return data[indices[0]:indices[1]]
187+
188+
@staticmethod
189+
def readInt32(data, start):
190+
# chunk = data[start:start+8]
191+
# return int.from_bytes(chunk, 'big') - 0x30303030 + ((chunk & 0x40404040) >> 6) * 9
192+
"""Fast hex bytearray → 32-bit int (8 hex chars, ASCII)"""
193+
# branchless conversion for uppercase/lowercase or digits:
194+
# - (data[i] & 15): Gives the low 4 bits (0-9 for digits
195+
# '0'-'9', or 1-6 for 'A'-'F'/'a'-'f').
196+
# - ((data[i] >> 6) & 1): Detects letters—0 for digits (ASCII
197+
# 48-57), 1 for uppercase/lowercase letters (ASCII 65-70 or
198+
# 97-102).
199+
# - * 9: Adds an extra 9 only for letters (e.g., 'A' low bits=1
200+
# → 1+9=10; 'F'=6 → 6+9=15). This avoids if/else branches
201+
# for better performance in tight loops.
202+
v = 0
203+
for i in range(start, start+8):
204+
v = (v << 4) + (data[i] & 15) + ((data[i] >> 6) & 1) * 9
205+
return v
206+
207+
@staticmethod
208+
def fromHexBytes(b: bytearray, start: int, stop: int) -> bytearray:
209+
"""ASCII hex bytearray → binary bytearray (any even length)"""
210+
r = bytearray((stop-start) // 2)
211+
i = start
212+
while i < stop:
213+
x, y = b[i], b[i+1]
214+
r[i//2] = ((x & 15) + ((x >> 6) & 1) * 9) << 4 | \
215+
((y & 15) + ((y >> 6) & 1) * 9)
216+
i += 2
217+
return r
218+
219+
def handleDataOptimized(self, data: Union[bytes, bytearray],
220+
test_output=None, verbose=False) -> int:
158221
"""Provide characters from the outside link to be parsed
159222
160223
Args:
161224
data (Union[bytes,bytearray]): new data from outside link
225+
test_output (list, optional): List-like object to hold
226+
resulting frames--for testing only (In normal operation,
227+
this method only uses self.fireFrameReceived(cf) to
228+
queue frames).
162229
verbose (bool, optional): If True, print each frame
163230
detected.
164231
@@ -168,6 +235,7 @@ def handleData(self, data: Union[bytes, bytearray], verbose=False) -> int:
168235
frameCount = 0
169236
self.inboundBuffer += data
170237
lastByte = 0 # last index is at ';'
238+
171239
if GC_END_BYTE in self.inboundBuffer:
172240
# ^ ';' ends message so we have at least one (CR/LF not required)
173241
# found end, now find start of that same message, earlier in buffer
@@ -178,7 +246,7 @@ def handleData(self, data: Union[bytes, bytearray], verbose=False) -> int:
178246
if self.inboundBuffer[index] == 0x3A: # ':' starts message
179247
# now start to accumulate data from entire message
180248
header = 0
181-
for offset in range(2, 9+1):
249+
for offset in range(2, 9+1): # skip first 2 bytes (":X")
182250
nextChar = (self.inboundBuffer[index+offset])
183251
nextByte = (nextChar & 0xF)+9 if nextChar > 0x39 else nextChar & 0xF # noqa: E501
184252
header = (header << 4)+nextByte
@@ -190,6 +258,12 @@ def handleData(self, data: Union[bytes, bytearray], verbose=False) -> int:
190258
break
191259
# two characters are data
192260
byte1 = self.inboundBuffer[index+11+2*dataItem]
261+
# Convert from UTF-8 to ordinal (0x39 is
262+
# "9", so assume UTF-8 code higher than 39 is
263+
# "A"-"F" [NOTE: "a"="f" would also work due
264+
# to `& 0xF` but are N/A in GridConnect]):
265+
# - 0x30-0x39 & 0xF yields 0-9
266+
# - 0x41-0x46 or 0x61-0x66 & 0xF + 9 yields 10-15
193267
part1 = (byte1 & 0xF)+9 if byte1 > 0x39 else byte1 & 0xF # noqa: E501
194268
byte2 = self.inboundBuffer[index+11+2*dataItem+1]
195269
part2 = (byte2 & 0xF)+9 if byte2 > 0x39 else byte2 & 0xF # noqa: E501
@@ -206,6 +280,8 @@ def handleData(self, data: Union[bytes, bytearray], verbose=False) -> int:
206280
# lastByte is index of ; in this message
207281

208282
cf = CanFrame(header, outData)
283+
if test_output is not None:
284+
test_output.add(cf)
209285
frameCount += 1
210286
self.fireFrameReceived(cf)
211287
if verbose:
@@ -215,3 +291,80 @@ def handleData(self, data: Union[bytes, bytearray], verbose=False) -> int:
215291
# shorten buffer by removing the processed message
216292
self.inboundBuffer = self.inboundBuffer[lastByte:]
217293
return frameCount
294+
295+
def handleData(self, data: Union[bytes, bytearray],
296+
test_output=None, verbose=False) -> int:
297+
"""Provide characters from the outside link to be parsed
298+
299+
Args:
300+
data (Union[bytes,bytearray]): new data from outside link
301+
test_output (list, optional): List-like object to hold
302+
resulting frames--for testing only (In normal operation,
303+
this method only uses self.fireFrameReceived(cf) to
304+
queue frames).
305+
verbose (bool, optional): If True, print each frame
306+
detected.
307+
308+
Returns:
309+
int: The number of frames completed by inboundBuffer+data.
310+
"""
311+
cls = type(self)
312+
frameCount = 0
313+
self.inboundBuffer += data
314+
lastByte = 0 # last index is at ';'
315+
start = 0
316+
while True:
317+
positions = cls.nextPacketRange(self.inboundBuffer, start=start)
318+
if positions is None:
319+
break
320+
first, last = positions
321+
del positions
322+
if last - first < 8:
323+
logger.warning(
324+
"[handleData] Skipped malformed"
325+
" (< 8 pairs) packet {}"
326+
.format(repr(self.inboundBuffer[first:last])))
327+
start = last
328+
continue
329+
if self.inboundBuffer[first+1] != ord(b'X'): # 0x58 (88)
330+
logger.warning(
331+
"[handleData] Skipped malformed packet"
332+
" (No 'X' in {})"
333+
.format(repr(self.inboundBuffer[first:last])))
334+
start = last + 1 # +1 to skip ";"
335+
continue
336+
headerI = first + 2 # skip 2 chars: ":X"
337+
dataI = headerI + 8
338+
header_bytes = bytearray.fromhex(
339+
self.inboundBuffer[headerI:dataI])
340+
if last - dataI > 0:
341+
if (last - dataI) % 2 > 0:
342+
logger.warning(
343+
"[handleData] Skipped malformed packet"
344+
" (Incomplete pair in {})"
345+
.format(repr(self.inboundBuffer[first:last])))
346+
start = last + 1 # +1 to skip ";"
347+
continue
348+
outData = bytearray.fromhex(
349+
self.inboundBuffer[dataI:last])
350+
else:
351+
outData = bytearray()
352+
353+
# Convert 4-byte big-endian header to 29-bit integer
354+
header = struct.unpack('>I', header_bytes)[0]
355+
356+
cf = CanFrame(header, outData)
357+
if test_output is not None:
358+
test_output.add(cf)
359+
frameCount += 1
360+
self.fireFrameReceived(cf)
361+
if verbose:
362+
print("- RECV {}".format(
363+
self.inboundBuffer[first:last].strip()))
364+
365+
start = last + 1 # +1 to skip ";"
366+
367+
if lastByte > 0:
368+
del self.inboundBuffer[0:lastByte+1]
369+
370+
return frameCount

0 commit comments

Comments
 (0)