Skip to content

Commit b5af75e

Browse files
authored
Merge pull request #67 from Hierosoft/delegate-state-to-link-and-flow-to-port
Delegate state to link and flow to port (Fix #62)
2 parents 075c533 + 045defd commit b5af75e

File tree

90 files changed

+7605
-1418
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+7605
-1418
lines changed

.github/workflows/run-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ jobs:
1313
- uses: actions/checkout@v3
1414
- name: check tests run
1515
run: |
16-
python3 test_all.py
16+
python3 -m unittest discover -s tests -p 'test_*.py'

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,5 @@ cython_debug/
183183
/build
184184
/doc/_autosummary
185185
/examples/settings.json
186+
/.venv-3.12/
187+
/cached-cdi.xml

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ If using VSCode (or fully open-source VSCodium):
5050
- (recommended, reduces commit diffs) **Trailing Spaces** by Shardul Mahadik
5151
- (recommended) **autoDocstring**: Type `"""` below a method or class and it will create a Sphinx-style template for you.
5252
- The workspace file has `"autoDocstring.docstringFormat": "google"` set since Google style is widely used and comprehensive (documents types etc).
53+
- (recommended) ms-python (VSCode will recommend it when opening and/or running a Python workspace or file typically):
54+
- You must install ms-python extension version 2025.0.0 or earlier of you are actually using Python 3.8, otherwise you will need a later version of Python.
5355

5456
#### Documentation
5557
The sources for building documentation are:

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import os
1010
import sys
11+
1112
sys.path.insert(0, os.path.abspath('..'))
1213

1314
project = 'python-openlcb'

examples/example_cdi_access.py

Lines changed: 169 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,36 @@
1111
address and port.
1212
'''
1313
# region same code as other examples
14+
import copy
15+
import sys
16+
import xml.sax
17+
import xml.sax.handler
18+
import xml.sax.xmlreader # for static type hints, autocomplete in this case
19+
20+
from logging import getLogger
21+
1422
from examples_settings import Settings # do 1st to fix path if no pip install
23+
from openlcb import precise_sleep
24+
from openlcb.xmldataprocessor import attrs_to_dict
25+
from openlcb.tcplink.tcpsocket import TcpSocket
1526
settings = Settings()
1627

1728
if __name__ == "__main__":
1829
settings.load_cli_args(docstring=__doc__)
30+
logger = getLogger(__file__)
31+
else:
32+
logger = getLogger(__name__)
1933
# endregion same code as other examples
2034

21-
from openlcb.canbus.tcpsocket import TcpSocket
22-
23-
from openlcb.canbus.canphysicallayergridconnect import (
35+
from openlcb.canbus.canphysicallayergridconnect import ( # noqa:E402
2436
CanPhysicalLayerGridConnect,
2537
)
26-
from openlcb.canbus.canlink import CanLink
27-
from openlcb.nodeid import NodeID
28-
from openlcb.datagramservice import (
38+
from openlcb.canbus.canlink import CanLink # noqa:E402
39+
from openlcb.nodeid import NodeID # noqa:E402
40+
from openlcb.datagramservice import ( # noqa:E402
2941
DatagramService,
3042
)
31-
from openlcb.memoryservice import (
43+
from openlcb.memoryservice import ( # noqa:E402
3244
MemoryReadMemo,
3345
MemoryService,
3446
)
@@ -42,18 +54,20 @@
4254
# farNodeID = "02.01.57.00.04.9C"
4355
# endregion moved to settings
4456

45-
s = TcpSocket()
57+
sock = TcpSocket()
4658
# s.settimeout(30)
47-
s.connect(settings['host'], settings['port'])
59+
sock.connect(settings['host'], settings['port'])
4860

4961

5062
# print("RR, SR are raw socket interface receive and send;"
5163
# " RL, SL are link interface; RM, SM are message interface")
5264

5365

54-
def sendToSocket(string):
55-
# print(" SR: {}".format(string.strip()))
56-
s.send(string)
66+
# def sendToSocket(frame: CanFrame):
67+
# string = frame.encodeAsString()
68+
# # print(" SR: {}".format(string.strip()))
69+
# sock.sendString(string)
70+
# physicalLayer.onFrameSent(frame)
5771

5872

5973
def printFrame(frame):
@@ -80,11 +94,10 @@ def printDatagram(memo):
8094
return False
8195

8296

83-
canPhysicalLayerGridConnect = CanPhysicalLayerGridConnect(sendToSocket)
84-
canPhysicalLayerGridConnect.registerFrameReceivedListener(printFrame)
97+
physicalLayer = CanPhysicalLayerGridConnect()
98+
physicalLayer.registerFrameReceivedListener(printFrame)
8599

86-
canLink = CanLink(NodeID(settings['localNodeID']))
87-
canLink.linkPhysicalLayer(canPhysicalLayerGridConnect)
100+
canLink = CanLink(physicalLayer, NodeID(settings['localNodeID']))
88101
canLink.registerMessageReceivedListener(printMessage)
89102

90103
datagramService = DatagramService(canLink)
@@ -100,6 +113,9 @@ def printDatagram(memo):
100113

101114
# callbacks to get results of memory read
102115

116+
complete_data = False
117+
read_failed = False
118+
103119

104120
def memoryReadSuccess(memo):
105121
"""Handle a successful read
@@ -113,11 +129,16 @@ def memoryReadSuccess(memo):
113129
# print("successful memory read: {}".format(memo.data))
114130

115131
global resultingCDI
132+
global complete_data
116133

117134
# is this done?
118135
if len(memo.data) == 64 and 0 not in memo.data:
119136
# save content
120137
resultingCDI += memo.data
138+
logger.debug(
139+
f"[{memo.address}] successful read"
140+
f" {MemoryService.arrayToString(memo.data, len(memo.data))}"
141+
"; next = address + 64")
121142
# update the address
122143
memo.address = memo.address+64
123144
# and read again
@@ -139,12 +160,15 @@ def memoryReadSuccess(memo):
139160

140161
# and process that
141162
processXML(cdiString)
163+
complete_data = True
142164

143165
# done
144166

145167

146168
def memoryReadFail(memo):
169+
global read_failed
147170
print("memory read failed: {}".format(memo.data))
171+
read_failed = True
148172

149173

150174
#######################
@@ -157,58 +181,97 @@ def memoryReadFail(memo):
157181
# in a row, we buffer up the characters until the `endElement`
158182
# call is invoked to indicate the text is complete
159183

160-
import xml.sax # noqa: E402
161-
162184

163185
class MyHandler(xml.sax.handler.ContentHandler):
164-
"""XML SAX callbacks in a handler object"""
165-
def __init__(self):
166-
self._charBuffer = bytearray()
167-
168-
def startElement(self, name, attrs):
169-
"""_summary_
170-
171-
Args:
172-
name (_type_): _description_
173-
attrs (_type_): _description_
174-
"""
175-
print("Start: ", name)
176-
if attrs is not None and attrs :
177-
print(" Attributes: ", attrs.getNames())
178-
179-
def endElement(self, name):
180-
"""_summary_
186+
"""XML SAX callbacks in a handler object
187+
188+
Attributes:
189+
_chunks (list[str]): Collects chunks of data.
190+
This is implementation-specific, and not
191+
required if streaming (parser.feed).
192+
_tmp_address (int|None): Where we are in the memory space (starting
193+
at origin, and calculated using offset and/or size of start
194+
tags).
195+
_tmp_space (int|None): What space we are currently on.
196+
"""
181197

182-
Args:
183-
name (_type_): _description_
184-
"""
185-
print(name, "content:", self._flushCharBuffer())
186-
print("End: ", name)
198+
def __init__(self):
199+
self._chunks = []
200+
self.stack = []
201+
self.cursorCol = 0
202+
self._tmp_space = None # type: int|None
203+
self._tmp_address = None # type: int|None
204+
205+
def startElement(self, name: str, attrs: xml.sax.xmlreader.AttributesImpl):
206+
"""See xml.sax.handler.ContentHandler documentation."""
207+
self.stack.append(name)
208+
if self.cursorCol != 0:
209+
self.print()
210+
self.write(name)
211+
if attrs is not None and attrs:
212+
self.print(" {}".format(attrs_to_dict(attrs)))
213+
214+
def endElement(self, name: str):
215+
"""See xml.sax.handler.ContentHandler documentation."""
216+
content = self._flushCharBuffer().strip()
217+
if self.cursorCol != 0:
218+
self.print()
219+
if content:
220+
self.print('/{} "{}"'.format(name, content))
221+
else:
222+
self.print('/{}'.format(name))
223+
self.stack.pop()
224+
# self.print("/", name)
187225
pass
188226

227+
def write(self, *args, **kwargs):
228+
args = list(args)
229+
if self.cursorCol == 0:
230+
tab = len(self.stack)*" "
231+
self.cursorCol += len(tab)
232+
args.insert(0, tab) # prepend indent
233+
for arg in args:
234+
sys.stdout.write(arg)
235+
self.cursorCol += len(arg)
236+
sys.stdout.flush()
237+
238+
def print(self, *args, **kwargs):
239+
if self.cursorCol == 0: # No indent yet, so use write.
240+
self.write(*args, **kwargs)
241+
print()
242+
else:
243+
print(*args, **kwargs)
244+
self.cursorCol = 0
245+
189246
def _flushCharBuffer(self):
190247
"""Decode the buffer, clear it, and return all content.
248+
See xml.sax.handler.ContentHandler documentation.
191249
192250
Returns:
193251
str: The content of the bytes buffer decoded as utf-8.
194252
"""
195-
s = self._charBuffer.decode("utf-8")
196-
self._charBuffer.clear()
253+
s = ''.join(self._chunks)
254+
self._chunks.clear()
197255
return s
198256

199-
def characters(self, data):
200-
"""Received characters handler
257+
def characters(self, content: str):
258+
"""Received characters handler.
259+
See xml.sax.handler.ContentHandler documentation.
260+
201261
Args:
202262
data (Union[bytearray, bytes, list[int]]): any
203263
data (any type accepted by bytearray extend).
204264
"""
205-
self._charBuffer.extend(data)
265+
if not isinstance(content, str):
266+
raise TypeError("Expected str, got {}"
267+
.format(type(content).__name__))
268+
self._chunks.append(content)
206269

207270

208271
handler = MyHandler()
209272

210273

211-
def processXML(content) :
274+
def processXML(content: str) :
212275
"""process the XML and invoke callbacks
213276
214277
Args:
@@ -218,15 +281,37 @@ def processXML(content) :
218281
# only called when there is a null terminator, which indicates the
219282
# last packet was reached for the requested read.
220283
# - See memoryReadSuccess comments for details.
284+
with open("cached-cdi.xml", 'w') as stream:
285+
# NOTE: Actual caching should key by all SNIP info that could
286+
# affect CDI/FDI: manufacturer, model, and version. Without
287+
# all 3 being present in SNIP, the cache may be incorrect.
288+
stream.write(content)
221289
xml.sax.parseString(content, handler)
222290
print("\nParser done")
223291

224292

225293
#######################
226294

227295
# have the socket layer report up to bring the link layer up and get an alias
228-
# print(" SL : link up")
229-
canPhysicalLayerGridConnect.physicalLayerUp()
296+
print(" QUEUE frames : link up...")
297+
physicalLayer.physicalLayerUp()
298+
print(" QUEUED frames : link up...waiting...")
299+
while canLink.pollState() != CanLink.State.Permitted:
300+
# provides incoming data to physicalLayer & sends queued:
301+
physicalLayer.receiveAll(sock, verbose=True)
302+
physicalLayer.sendAll(sock)
303+
304+
if canLink.getState() == CanLink.State.WaitForAliases:
305+
# physicalLayer.receiveAll(sock, verbose=True)
306+
physicalLayer.sendAll(sock)
307+
# ^ prevent assertion error below, proceed to send.
308+
if canLink.pollState() == CanLink.State.Permitted:
309+
break
310+
assert canLink.getWaitForAliasResponseStart() is not None, \
311+
("openlcb didn't send the 7,6,5,4 CID frames (state={})"
312+
.format(canLink.getState()))
313+
precise_sleep(.02)
314+
print(" SENT frames : link up")
230315

231316

232317
def memoryRead():
@@ -236,8 +321,16 @@ def memoryRead():
236321
to AME
237322
"""
238323
import time
239-
time.sleep(1)
240-
324+
time.sleep(.21)
325+
# ^ 200ms: See section 6.2.1 of CAN Frame Transfer Standard
326+
# (CanLink.State.Permitted will only occur after that, but waiting
327+
# now will reduce output & delays below in this example).
328+
while canLink.getState() != CanLink.State.Permitted:
329+
print("Waiting for connection sequence to complete...")
330+
# This delay could be .2 (per alias collision), but longer to
331+
# reduce console messages:
332+
time.sleep(.5)
333+
print("Requesting memory read. Please wait...")
241334
# read 64 bytes from the CDI space starting at address zero
242335
memMemo = MemoryReadMemo(NodeID(settings['farNodeID']), 64, 0xFF, 0,
243336
memoryReadFail, memoryReadSuccess)
@@ -247,10 +340,30 @@ def memoryRead():
247340
import threading # noqa E402
248341
thread = threading.Thread(target=memoryRead)
249342
thread.start()
250-
343+
previous_nodes = copy.deepcopy(canLink.nodeIdToAlias)
251344
# process resulting activity
252-
while True:
253-
received = s.receive()
254-
# print(" RR: {}".format(received.strip()))
255-
# pass to link processor
256-
canPhysicalLayerGridConnect.receiveString(received)
345+
print()
346+
print("This example will exit on failure or complete data.")
347+
while not complete_data and not read_failed:
348+
# In this example, requests are initiate by the
349+
# memoryRead thread, and receiveAll actually
350+
# receives the data from the requested memory space (CDI in this
351+
# case) and offset (incremental position in the file/data,
352+
# incremented by this example's memoryReadSuccess handler).
353+
count = 0
354+
count += physicalLayer.receiveAll(sock)
355+
count += physicalLayer.sendAll(sock)
356+
if canLink.nodeIdToAlias != previous_nodes:
357+
print("nodeIdToAlias updated: {}".format(canLink.nodeIdToAlias))
358+
if count < 1:
359+
precise_sleep(.01)
360+
# else skip sleep to avoid latency (port already delayed)
361+
if canLink.nodeIdToAlias != previous_nodes:
362+
previous_nodes = copy.deepcopy(canLink.nodeIdToAlias)
363+
364+
physicalLayer.physicalLayerDown()
365+
366+
if read_failed:
367+
print("Read complete (FAILED)")
368+
else:
369+
print("Read complete (OK)")

0 commit comments

Comments
 (0)