Skip to content

Commit

Permalink
UART upload enhancements. (ExpressLRS#261)
Browse files Browse the repository at this point in the history
* 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 <Rejdukien@users.noreply.github.com>

* UART upload cleanup and ReadLine renamed to SerialHelper

* UART upload script fix

* Passthrough init optimization

Co-authored-by: Rejd <Rejdukien@users.noreply.github.com>
  • Loading branch information
cruwaller and Rejdukien authored Dec 26, 2020
1 parent 4033e29 commit 149b8aa
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 125 deletions.
80 changes: 45 additions & 35 deletions src/python/BFinitPassthrough.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,78 @@
import serial, time, sys
import serial, time, sys, re
from xmodem import XMODEM
import serials_find
import SerialHelper

SCRIPT_DEBUG = 0


class PassthroughEnabled(Exception):
pass

class PassthroughFailed(Exception):
pass


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


def bf_passthrough_init(port, requestedBaudrate):
def bf_passthrough_init(port, requestedBaudrate, half_duplex=False):
debug = SCRIPT_DEBUG

sys.stdout.flush()
dbg_print("======== PASSTHROUGH INIT ========")
dbg_print(" Trying to initialize %s @ %s" % (port, requestedBaudrate))

s = serial.Serial(port=port, baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=5, xonxoff=0, rtscts=0)

s.write(chr(0x23).encode('utf-8'))
time.sleep(1)
s.write(("serial\n").encode('utf-8'))
time.sleep(1)
s = serial.Serial(port=port, baudrate=115200,
bytesize=8, parity='N', stopbits=1,
timeout=1, xonxoff=0, rtscts=0)

rl = SerialHelper.SerialHelper(s, 3., ['CCC', "# "])
rl.clear()
# Send start command '#'
rl.write("#\r\n", half_duplex)
start = rl.read_line(2.).strip()
#dbg_print("BF INIT: '%s'" % start.replace("\r", ""))
if "CCC" in start:
raise PassthroughEnabled("Passthrough already enabled and bootloader active")
elif not start or not start.endswith("#"):
raise PassthroughEnabled("No CLI available. Already in passthrough mode?")

inChars = ""
SerialRXindex = ""

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

while s.in_waiting:
try:
inChars += s.read().decode('utf-8')
except UnicodeDecodeError:
# just discard possible RC / TLM data
pass
rl.set_delimiters(["\n"])
rl.clear()
rl.write("serial\r\n")

for line in inChars.split('\n'):
if line.startswith("serial"):
line = line.strip()
dbg_print(line)
while True:
line = rl.read_line().strip()
#print("FC: '%s'" % line)
if not line or "#" in line:
break

# Searching: 'serial <index> 64 ...'
config = line.split()
if config[2] == "64":
if line.startswith("serial"):
if debug:
dbg_print(" '%s'" % line)
config = re.search('serial ([0-9]+) ([0-9]+) ', line)
if config and config.group(2) == "64":
dbg_print(" ** Serial RX config detected: '%s'" % line)
SerialRXindex = config[1]

dbg_print()
SerialRXindex = config.group(1)
if not debug:
break

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

cmd = "serialpassthrough %s %s " % (SerialRXindex, requestedBaudrate, )
cmd = "serialpassthrough %s %s" % (SerialRXindex, requestedBaudrate, )

dbg_print("Setting serial passthrough...")
dbg_print("Enabling serial passthrough...")
dbg_print(" CMD: '%s'" % cmd)
s.write((cmd + '\n').encode('utf-8'))
time.sleep(1)

s.flush()
rl.write(cmd + '\n')
time.sleep(.2)
s.close()

dbg_print("======== PASSTHROUGH DONE ========")
Expand Down
81 changes: 81 additions & 0 deletions src/python/SerialHelper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import time, serial

encoding = "utf-8"

class SerialHelper:
def __init__(self, serial, timeout=2, delimiters=["\n", "CCC"], half_duplex=False):
self.serial = serial
self.timeout = timeout
self.half_duplex = half_duplex
self.clear()
self.set_delimiters(delimiters)

def encode(self, data):
if type(data) == str:
return data.encode(encoding)
return data

def clear(self):
self.serial.reset_input_buffer()
self.buf = bytearray()

def set_serial(self, serial):
self.serial = serial

def set_timeout(self, timeout):
self.timeout = timeout

def set_delimiters(self, delimiters):
self.delimiters = [
bytes(d, encoding) if type(d) == str else d for d in delimiters]

def read_line(self, timeout=None):
if timeout is None or timeout <= 0.:
timeout = self.timeout
buf = self.buf
for delimiter in self.delimiters:
i = buf.find(delimiter)
if i >= 0:
offset = i+len(delimiter)
r = buf[:offset]
self.buf = buf[offset:]
return self.__convert_to_str(r)

start = time.time()
while ((time.time() - start) < timeout):
i = max(0, min(2048, self.serial.in_waiting))
data = self.serial.read(i)
if not data:
continue
for delimiter in self.delimiters:
i = bytearray(buf + data).find(delimiter)
if i >= 0:
offset = i+len(delimiter)
r = buf + data[:offset]
self.buf = bytearray(data[offset:])
return self.__convert_to_str(r)
# No match
buf.extend(data)

# Timeout, reset buffer and return empty string
#print("TIMEOUT! Got:\n>>>>>>\n{}\n<<<<<<\n".format(buf))
self.buf = bytearray()
return ""

def write(self, data, half_duplex=None):
if half_duplex is None:
half_duplex = self.half_duplex
serial = self.serial
data = self.encode(data)
cnt = serial.write(data)
serial.flush()
if half_duplex:
# Clean RX buffer in case of half duplex
# All written data is read into RX buffer
serial.read(cnt)

def __convert_to_str(self, data):
try:
return data.decode(encoding)
except UnicodeDecodeError:
return ""
Loading

0 comments on commit 149b8aa

Please sign in to comment.