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+
1520from openlcb .canbus .canphysicallayer import CanPhysicalLayer
1621from openlcb .canbus .canframe import CanFrame
1722from openlcb .frameencoder import FrameEncoder
1823from openlcb .portinterface import PortInterface
1924
25+ logger = getLogger (__name__ )
26+
27+
2028GC_START_BYTE = 0x3a # :
2129GC_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