Skip to content

Commit 2960c83

Browse files
committed
pylon 485 implementation inprogress
1 parent 38820f4 commit 2960c83

File tree

5 files changed

+235
-13
lines changed

5 files changed

+235
-13
lines changed

classes/Object.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#hack to allow generic objects
2+
class Object(object):
3+
pass

classes/protocol_settings.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ def __init__(self, protocol : str, settings_dir : str = 'protocols'):
199199
for registry_type in Registry_Type:
200200
self.load_registry_map(registry_type)
201201

202-
def get_registry_map(self, registry_type : Registry_Type) -> list[registry_map_entry]:
202+
def get_registry_map(self, registry_type : Registry_Type = Registry_Type.ZERO) -> list[registry_map_entry]:
203203
return self.registry_map[registry_type]
204204

205205
def get_registry_ranges(self, registry_type : Registry_Type) -> list[registry_map_entry]:
@@ -394,6 +394,9 @@ def determine_delimiter(first_row) -> str:
394394
else:
395395
range_match = range_regex.search(row['register'])
396396
if not range_match:
397+
if row['register'][0] == 'x':
398+
register = int.from_bytes(bytes.fromhex(row['register'][1:]), byteorder='big')
399+
397400
register = int(row['register'])
398401
else:
399402
reverse = range_match.group('reverse')

classes/transports/serial_frame_client.py

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Callable
12
import serial
23
import threading
34
import time
@@ -6,7 +7,7 @@
67
class serial_frame_client():
78
''' basic serial client implenting a empty SOI/EOI frame'''
89
client : serial.Serial
9-
running : bool
10+
running : bool = False
1011
soi : bytes
1112
'''start of information'''
1213
eoi : bytes
@@ -15,22 +16,109 @@ class serial_frame_client():
1516

1617
max_frame_size : int = 256
1718

19+
port : str = '/dev/ttyUSB0'
20+
baud : int = 9600
1821

19-
def __init__(self) -> None:
20-
self.client = serial.Serial('/dev/ttyUSB0', 9600)
21-
pass
22+
timeout : float = 30
23+
''' timeout in seconds '''
2224

23-
def checksum(self, data : bytes) -> bytes:
24-
return b''
25+
#region asyncronous
26+
asynchronous : bool = False
27+
''' if set, runs main loop'''
28+
29+
on_message : Callable[[bytes], None] = None
30+
''' async mode only'''
31+
32+
thread : threading.Thread
33+
''' main thread for read loop'''
34+
35+
callback_lock : threading.Lock = threading.Lock()
36+
'''lock for callback'''
37+
#endregion asyncronous
38+
39+
40+
def __init__(self, port : str , baud : int , soi : bytes, eoi : bytes) -> None:
41+
self.port = port
42+
self.baud = baud
43+
self.client = serial.Serial(port, baud)
44+
45+
def connect(self):
46+
if self.asynchronous:
47+
self.running = True
48+
self.pending_frames = []
49+
self.thread = threading.Thread(target=self.read_thread)
50+
self.thread.daemon = True
51+
self.thread.start()
52+
return True
2553

2654
def write(self, data : bytes):
2755
''' write data, excluding SOI and EOI bytes'''
2856
data = self.soi + data + self.eoi
2957
self.client.write()
3058

31-
def read(self):
59+
def read(self, reset_buffer = True, frames = 1) -> list[bytes] | bytes:
60+
''' returns list of frames, if frames > 1 '''
3261
buffer = bytearray()
62+
self.pending_frames.clear()
63+
64+
#for shatty order sensitive protocols.
65+
# Clear input buffers
66+
if reset_buffer:
67+
self.client.reset_input_buffer()
68+
69+
timedout = time.time() + self.timeout
70+
frameCount = 0
71+
72+
while time.time() < timedout:
73+
# Read data from serial port
74+
data = self.client.read()
75+
76+
# Check if data is available
77+
if data:
78+
# Append data to buffer
79+
buffer += data
80+
81+
# Find SOI index in buffer
82+
soi_index = buffer.find(self.soi)
83+
84+
# Process all occurrences of SOI in buffer
85+
while soi_index != -1:
86+
# Remove data before SOI sequence
87+
buffer = buffer[soi_index:]
88+
89+
# Find EOI index in buffer
90+
eoi_index = buffer.find(self.eoi)
91+
92+
if eoi_index != -1:
93+
94+
frame = buffer[len(self.soi):eoi_index]
95+
if frames == 1:
96+
return frame
97+
98+
if frameCount > 1:
99+
# Extract and store the complete frame
100+
self.pending_frames.append(frame)
101+
102+
if self.pending_frames.count() == frames:
103+
return self.pending_frames
104+
105+
# Remove the processed data from the buffer
106+
buffer = buffer[eoi_index + len(self.eoi) : ]
107+
108+
# Find next SOI index in the remaining buffer
109+
soi_index = buffer.find(self.soi)
110+
111+
else:
112+
# If no EOI is found and buffer size exceeds max_frame_size, clear buffer
113+
if len(buffer) > self.max_frame_size:
114+
buffer.clear()
115+
break #no eoi, continue waiting
116+
117+
time.sleep(0.01)
33118

119+
def read_thread(self):
120+
buffer = bytearray()
121+
self.running = True
34122
while self.running:
35123
# Read data from serial port
36124
data = self.client.read()
@@ -66,6 +154,12 @@ def read(self):
66154
buffer.clear()
67155
break #no eoi, continue waiting
68156

157+
#can probably be in the loop, but being cautious
158+
for frame in self.pending_frames:
159+
with self.callback_lock:
160+
if self.on_message:
161+
self.on_message(frame)
162+
69163
time.sleep(0.01)
70164

71165

classes/transports/serial_pylon.py

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
1+
from enum import Enum
2+
import struct
3+
4+
from ..Object import Object
5+
16
from .serial_frame_client import serial_frame_client
27
from .transport_base import transport_base
38

49

10+
511
from typing import TYPE_CHECKING
612
if TYPE_CHECKING:
713
from configparser import SectionProxy
8-
from classes.protocol_settings import protocol_settings
14+
from classes.protocol_settings import protocol_settings, registry_map_entry
15+
16+
17+
class return_codes(Enum):
18+
NORMAL = 0x00
19+
VERSION_ERROR = 0x01
20+
CHECKSUM_ERROR = 0x02
21+
LCHECKSUM_ERROR = 0x03
22+
INVALID_CID2 = 0x04
23+
COMMAND_FORMAT_ERROR = 0X05
24+
INVALID_DATA = 0X06
25+
ADDRESS_ERROR = 0X90
26+
COMMUNICATION_ERROR = 0X91
27+
UNKNOWN_ERROR = -1
28+
29+
@classmethod
30+
def fromByte(cls, value : bytes):
31+
try:
32+
return cls(value) # Attempt to access the Enum member
33+
except ValueError:
34+
return return_codes.UNKNOWN_ERROR
935

1036
class serial_pylon(transport_base):
1137
''' for a lack of a better name'''
@@ -17,13 +43,14 @@ class serial_pylon(transport_base):
1743
client : serial_frame_client
1844

1945
#this format is pretty common; i need a name for it.
20-
SOI : bytes = b'\x7e'
21-
VER : bytes = b'\x00'
46+
SOI : bytes = b'\x7e' # aka b"~"
47+
VER : bytes = b'\x00' # aka b"\r"
2248
''' version has to be fetched first '''
2349
ADR : bytes
2450
CID1 : bytes
2551
CID2 : bytes
2652
LENGTH : bytes
53+
''' 2 bytes - include LENID & LCHKSUM'''
2754
INFO : bytes
2855
CHKSUM : bytes
2956
EOI : bytes = b'\x0d'
@@ -39,12 +66,107 @@ def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_setti
3966

4067
address : int = settings.getint("address", 0)
4168
self.addresses = [address]
69+
70+
self.ADR = struct.pack('B', address)
71+
#todo, multi address support later
72+
73+
self.client = serial_frame_client(self.port, self.baudrate, self.SOI, self.EOI)
74+
4275
pass
4376

4477
def connect(self):
78+
self.client.connect()
4579
#3.1 Get protocol version
4680
if self.VER == b'\x00':
4781
#get VER for communicating
4882
#SOI VER ADR 46H 4FH LENGT INFO CHKSUM EOI
83+
version = self.read_variable('version')
84+
if version:
85+
self.VER = version
86+
pass
87+
88+
def read_variable(self, variable_name : str, entry : 'registry_map_entry' = None):
89+
##clean for convinecne
90+
if variable_name:
91+
variable_name = variable_name.strip().lower().replace(' ', '_')
92+
93+
registry_map = self.protocolSettings.get_registry_map()
94+
95+
if entry == None:
96+
for e in registry_map:
97+
if e.variable_name == variable_name:
98+
entry = e
99+
break
49100

50-
101+
102+
if entry:
103+
#entry.concatenate this protocol probably doesnt require concatenate, since info is variable length.
104+
command = entry.register #CID1 and CID2 combined creates a single ushort
105+
self.send_command(command)
106+
return self.decode_frame(self.client.read())
107+
108+
return None
109+
110+
def checksum(self, frame: bytes):
111+
assert isinstance(frame, bytes)
112+
113+
sum = 0
114+
for byte in frame:
115+
sum += byte
116+
sum = ~sum
117+
sum %= 0x10000
118+
sum += 1
119+
return sum
120+
121+
122+
def decode_frame(self, raw_frame: bytes) -> bytes:
123+
frame_data = raw_frame[0:len(raw_frame) - 5]
124+
frame_chksum = raw_frame[len(raw_frame) - 5:-1]
125+
126+
got_frame_checksum = self.checksum(frame_data)
127+
assert got_frame_checksum == int(frame_chksum, 16)
128+
129+
data = Object()
130+
data.ver = frame_data[0:2]
131+
data.adr = frame_data[2:4]
132+
data.cid1 = frame_data[4:6]
133+
data.cid2 = frame_data[6:8]
134+
data.infolength = frame_data[8:12]
135+
data.info = frame_data[12:]
136+
137+
#on return, cid2 holds a return error code. so reads are time sensitive. will have to write syncronis functions in client
138+
#fromByte
139+
returnCode = return_codes.fromByte(data.cid2)
140+
if returnCode != return_codes.NORMAL:
141+
self._log.warning(f"Serial Pylon Error code {returnCode}")
142+
143+
#todo, process info
144+
return data.info
145+
146+
147+
def build_frame(self, command : int, info: bytes = b''):
148+
''' builds frame without soi and eoi; that is left for frame client'''
149+
150+
info_length = 0
151+
152+
lenid = len(info)
153+
if lenid != 0:
154+
lenid_sum = (lenid & 0xf) + ((lenid >> 4) & 0xf) + ((lenid >> 8) & 0xf)
155+
lenid_modulo = lenid_sum % 16
156+
lenid_invert_plus_one = 0b1111 - lenid_modulo + 1
157+
158+
info_length = (lenid_invert_plus_one << 12) + lenid
159+
160+
self.LENGTH = struct.pack('<H', info_length)
161+
162+
frame : bytes = self.VER + self.ADR +struct.pack('<H', command) + self.LENGTH + info
163+
164+
frame_chksum = self.checksum(frame)
165+
frame = frame + struct.pack('<H', frame_chksum)
166+
167+
168+
def send_command(self, cmd, info: bytes = b''):
169+
data = self.build_frame(cmd, info)
170+
self.client.write(data)
171+
172+
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
variable name,data type,register,documented name,description,writable,values,unit,note
2-
,,0,version,protocol version,,0~255,,
2+
,BYTE,x464F,version,protocol version,,0~255,,

0 commit comments

Comments
 (0)