Skip to content

Commit 149b8aa

Browse files
cruwallerRejdukien
andauthored
UART upload enhancements. (ExpressLRS#261)
* UART upload enhancements. New serial read helper intruduced to help timeout handling. PySerial class does not handle timeouts correctly in all cases. This also makes delimiter definitions much easier which helps to detect different situations. Co-Authored-By: Rejd <[email protected]> * UART upload cleanup and ReadLine renamed to SerialHelper * UART upload script fix * Passthrough init optimization Co-authored-by: Rejd <[email protected]>
1 parent 4033e29 commit 149b8aa

3 files changed

Lines changed: 258 additions & 125 deletions

File tree

src/python/BFinitPassthrough.py

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,78 @@
1-
import serial, time, sys
1+
import serial, time, sys, re
22
from xmodem import XMODEM
33
import serials_find
4+
import SerialHelper
5+
6+
SCRIPT_DEBUG = 0
47

58

69
class PassthroughEnabled(Exception):
710
pass
811

12+
class PassthroughFailed(Exception):
13+
pass
14+
915

1016
def dbg_print(line=''):
1117
sys.stdout.write(line + '\n')
1218
sys.stdout.flush()
1319

1420

15-
def bf_passthrough_init(port, requestedBaudrate):
21+
def bf_passthrough_init(port, requestedBaudrate, half_duplex=False):
22+
debug = SCRIPT_DEBUG
23+
1624
sys.stdout.flush()
1725
dbg_print("======== PASSTHROUGH INIT ========")
1826
dbg_print(" Trying to initialize %s @ %s" % (port, requestedBaudrate))
1927

20-
s = serial.Serial(port=port, baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=5, xonxoff=0, rtscts=0)
21-
22-
s.write(chr(0x23).encode('utf-8'))
23-
time.sleep(1)
24-
s.write(("serial\n").encode('utf-8'))
25-
time.sleep(1)
28+
s = serial.Serial(port=port, baudrate=115200,
29+
bytesize=8, parity='N', stopbits=1,
30+
timeout=1, xonxoff=0, rtscts=0)
31+
32+
rl = SerialHelper.SerialHelper(s, 3., ['CCC', "# "])
33+
rl.clear()
34+
# Send start command '#'
35+
rl.write("#\r\n", half_duplex)
36+
start = rl.read_line(2.).strip()
37+
#dbg_print("BF INIT: '%s'" % start.replace("\r", ""))
38+
if "CCC" in start:
39+
raise PassthroughEnabled("Passthrough already enabled and bootloader active")
40+
elif not start or not start.endswith("#"):
41+
raise PassthroughEnabled("No CLI available. Already in passthrough mode?")
2642

27-
inChars = ""
2843
SerialRXindex = ""
2944

3045
dbg_print("\nAttempting to detect FC UART configuration...")
3146

32-
while s.in_waiting:
33-
try:
34-
inChars += s.read().decode('utf-8')
35-
except UnicodeDecodeError:
36-
# just discard possible RC / TLM data
37-
pass
47+
rl.set_delimiters(["\n"])
48+
rl.clear()
49+
rl.write("serial\r\n")
3850

39-
for line in inChars.split('\n'):
40-
if line.startswith("serial"):
41-
line = line.strip()
42-
dbg_print(line)
51+
while True:
52+
line = rl.read_line().strip()
53+
#print("FC: '%s'" % line)
54+
if not line or "#" in line:
55+
break
4356

44-
# Searching: 'serial <index> 64 ...'
45-
config = line.split()
46-
if config[2] == "64":
57+
if line.startswith("serial"):
58+
if debug:
59+
dbg_print(" '%s'" % line)
60+
config = re.search('serial ([0-9]+) ([0-9]+) ', line)
61+
if config and config.group(2) == "64":
4762
dbg_print(" ** Serial RX config detected: '%s'" % line)
48-
SerialRXindex = config[1]
49-
50-
dbg_print()
63+
SerialRXindex = config.group(1)
64+
if not debug:
65+
break
5166

5267
if not SerialRXindex:
53-
dbg_print("Failed to make contact with FC, possibly already in passthrough mode?")
54-
dbg_print("If the next step fails please reboot FC")
55-
dbg_print()
56-
raise PassthroughEnabled("FC connection failed");
68+
raise PassthroughFailed("!!! RX Serial not found !!!!\n Check configuration and try again...")
5769

58-
cmd = "serialpassthrough %s %s " % (SerialRXindex, requestedBaudrate, )
70+
cmd = "serialpassthrough %s %s" % (SerialRXindex, requestedBaudrate, )
5971

60-
dbg_print("Setting serial passthrough...")
72+
dbg_print("Enabling serial passthrough...")
6173
dbg_print(" CMD: '%s'" % cmd)
62-
s.write((cmd + '\n').encode('utf-8'))
63-
time.sleep(1)
64-
65-
s.flush()
74+
rl.write(cmd + '\n')
75+
time.sleep(.2)
6676
s.close()
6777

6878
dbg_print("======== PASSTHROUGH DONE ========")

src/python/SerialHelper.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import time, serial
2+
3+
encoding = "utf-8"
4+
5+
class SerialHelper:
6+
def __init__(self, serial, timeout=2, delimiters=["\n", "CCC"], half_duplex=False):
7+
self.serial = serial
8+
self.timeout = timeout
9+
self.half_duplex = half_duplex
10+
self.clear()
11+
self.set_delimiters(delimiters)
12+
13+
def encode(self, data):
14+
if type(data) == str:
15+
return data.encode(encoding)
16+
return data
17+
18+
def clear(self):
19+
self.serial.reset_input_buffer()
20+
self.buf = bytearray()
21+
22+
def set_serial(self, serial):
23+
self.serial = serial
24+
25+
def set_timeout(self, timeout):
26+
self.timeout = timeout
27+
28+
def set_delimiters(self, delimiters):
29+
self.delimiters = [
30+
bytes(d, encoding) if type(d) == str else d for d in delimiters]
31+
32+
def read_line(self, timeout=None):
33+
if timeout is None or timeout <= 0.:
34+
timeout = self.timeout
35+
buf = self.buf
36+
for delimiter in self.delimiters:
37+
i = buf.find(delimiter)
38+
if i >= 0:
39+
offset = i+len(delimiter)
40+
r = buf[:offset]
41+
self.buf = buf[offset:]
42+
return self.__convert_to_str(r)
43+
44+
start = time.time()
45+
while ((time.time() - start) < timeout):
46+
i = max(0, min(2048, self.serial.in_waiting))
47+
data = self.serial.read(i)
48+
if not data:
49+
continue
50+
for delimiter in self.delimiters:
51+
i = bytearray(buf + data).find(delimiter)
52+
if i >= 0:
53+
offset = i+len(delimiter)
54+
r = buf + data[:offset]
55+
self.buf = bytearray(data[offset:])
56+
return self.__convert_to_str(r)
57+
# No match
58+
buf.extend(data)
59+
60+
# Timeout, reset buffer and return empty string
61+
#print("TIMEOUT! Got:\n>>>>>>\n{}\n<<<<<<\n".format(buf))
62+
self.buf = bytearray()
63+
return ""
64+
65+
def write(self, data, half_duplex=None):
66+
if half_duplex is None:
67+
half_duplex = self.half_duplex
68+
serial = self.serial
69+
data = self.encode(data)
70+
cnt = serial.write(data)
71+
serial.flush()
72+
if half_duplex:
73+
# Clean RX buffer in case of half duplex
74+
# All written data is read into RX buffer
75+
serial.read(cnt)
76+
77+
def __convert_to_str(self, data):
78+
try:
79+
return data.decode(encoding)
80+
except UnicodeDecodeError:
81+
return ""

0 commit comments

Comments
 (0)