2017-06-28 02:23:30 +02:00
|
|
|
#!/usr/bin/env python2
|
2016-05-25 17:37:40 +02:00
|
|
|
# Script to implement a test console with firmware over serial port
|
|
|
|
#
|
2021-02-18 20:01:40 +01:00
|
|
|
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
|
2016-05-25 17:37:40 +02:00
|
|
|
#
|
|
|
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
|
|
|
import sys, optparse, os, re, logging
|
2017-09-19 03:41:00 +02:00
|
|
|
import reactor, serialhdl, pins, util, msgproto, clocksync
|
2016-05-25 17:37:40 +02:00
|
|
|
|
2017-08-08 02:05:19 +02:00
|
|
|
help_txt = """
|
|
|
|
This is a debugging console for the Klipper micro-controller.
|
|
|
|
In addition to mcu commands, the following artificial commands are
|
|
|
|
available:
|
|
|
|
PINS : Load pin name aliases (eg, "PINS arduino")
|
|
|
|
DELAY : Send a command at a clock time (eg, "DELAY 9999 get_uptime")
|
2017-09-06 21:40:23 +02:00
|
|
|
FLOOD : Send a command many times (eg, "FLOOD 22 .01 get_uptime")
|
2018-11-16 23:16:14 +01:00
|
|
|
SUPPRESS : Suppress a response message (eg, "SUPPRESS analog_in_state 4")
|
2017-08-08 02:05:19 +02:00
|
|
|
SET : Create a local variable (eg, "SET myvar 123.4")
|
2017-08-08 02:49:42 +02:00
|
|
|
STATS : Report serial statistics
|
2017-08-08 02:40:32 +02:00
|
|
|
LIST : List available mcu commands, local commands, and local variables
|
2017-08-08 02:05:19 +02:00
|
|
|
HELP : Show this text
|
|
|
|
All commands also support evaluation by enclosing an expression in { }.
|
|
|
|
For example, "reset_step_clock oid=4 clock={clock + freq}". In addition
|
|
|
|
to user defined variables (via the SET command) the following builtin
|
|
|
|
variables may be used in expressions:
|
|
|
|
clock : The current mcu clock time (as estimated by the host)
|
|
|
|
freq : The mcu clock frequency
|
|
|
|
"""
|
|
|
|
|
2016-05-25 17:37:40 +02:00
|
|
|
re_eval = re.compile(r'\{(?P<eval>[^}]*)\}')
|
|
|
|
|
|
|
|
class KeyboardReader:
|
|
|
|
def __init__(self, ser, reactor):
|
|
|
|
self.ser = ser
|
|
|
|
self.reactor = reactor
|
2018-05-16 15:27:50 +02:00
|
|
|
self.start_time = reactor.monotonic()
|
2017-09-19 03:41:00 +02:00
|
|
|
self.clocksync = clocksync.ClockSync(self.reactor)
|
2016-05-25 17:37:40 +02:00
|
|
|
self.fd = sys.stdin.fileno()
|
|
|
|
util.set_nonblock(self.fd)
|
2017-03-13 00:55:56 +01:00
|
|
|
self.mcu_freq = 0
|
2019-08-20 05:52:02 +02:00
|
|
|
self.pins = pins.PinResolver(validate_aliases=False)
|
2016-05-25 17:37:40 +02:00
|
|
|
self.data = ""
|
2016-11-27 23:45:58 +01:00
|
|
|
reactor.register_fd(self.fd, self.process_kbd)
|
2018-09-02 18:31:36 +02:00
|
|
|
reactor.register_callback(self.connect)
|
2017-06-21 21:50:41 +02:00
|
|
|
self.local_commands = {
|
2017-08-08 02:05:19 +02:00
|
|
|
"PINS": self.command_PINS, "SET": self.command_SET,
|
2017-09-06 21:40:23 +02:00
|
|
|
"DELAY": self.command_DELAY, "FLOOD": self.command_FLOOD,
|
2017-09-06 22:34:55 +02:00
|
|
|
"SUPPRESS": self.command_SUPPRESS, "STATS": self.command_STATS,
|
|
|
|
"LIST": self.command_LIST, "HELP": self.command_HELP,
|
2017-06-21 21:50:41 +02:00
|
|
|
}
|
2016-05-25 17:37:40 +02:00
|
|
|
self.eval_globals = {}
|
2016-11-27 23:45:58 +01:00
|
|
|
def connect(self, eventtime):
|
2017-08-08 02:05:19 +02:00
|
|
|
self.output(help_txt)
|
|
|
|
self.output("="*20 + " attempting to connect " + "="*20)
|
2016-11-27 23:45:58 +01:00
|
|
|
self.ser.connect()
|
2019-06-21 04:08:38 +02:00
|
|
|
msgparser = self.ser.get_msgparser()
|
2021-02-18 20:01:40 +01:00
|
|
|
message_count = len(msgparser.get_messages())
|
|
|
|
version, build_versions = msgparser.get_version_info()
|
|
|
|
self.output("Loaded %d commands (%s / %s)"
|
|
|
|
% (message_count, version, build_versions))
|
2018-04-03 18:13:06 +02:00
|
|
|
self.output("MCU config: %s" % (" ".join(
|
2021-02-18 20:01:40 +01:00
|
|
|
["%s=%s" % (k, v) for k, v in msgparser.get_constants().items()])))
|
2017-09-19 03:41:00 +02:00
|
|
|
self.clocksync.connect(self.ser)
|
2017-04-25 17:15:15 +02:00
|
|
|
self.ser.handle_default = self.handle_default
|
2019-06-20 23:36:46 +02:00
|
|
|
self.ser.register_response(self.handle_output, '#output')
|
2018-04-03 18:13:06 +02:00
|
|
|
self.mcu_freq = msgparser.get_constant_float('CLOCK_FREQ')
|
2017-08-08 02:05:19 +02:00
|
|
|
self.output("="*20 + " connected " + "="*20)
|
2016-11-27 23:45:58 +01:00
|
|
|
return self.reactor.NEVER
|
2017-04-25 17:16:02 +02:00
|
|
|
def output(self, msg):
|
|
|
|
sys.stdout.write("%s\n" % (msg,))
|
|
|
|
sys.stdout.flush()
|
2017-04-25 17:15:15 +02:00
|
|
|
def handle_default(self, params):
|
2018-05-16 15:27:50 +02:00
|
|
|
tdiff = params['#receive_time'] - self.start_time
|
2019-06-21 04:08:38 +02:00
|
|
|
msg = self.ser.get_msgparser().format_params(params)
|
|
|
|
self.output("%07.3f: %s" % (tdiff, msg))
|
2018-05-16 15:27:50 +02:00
|
|
|
def handle_output(self, params):
|
|
|
|
tdiff = params['#receive_time'] - self.start_time
|
|
|
|
self.output("%07.3f: %s: %s" % (tdiff, params['#name'], params['#msg']))
|
2017-09-06 22:34:55 +02:00
|
|
|
def handle_suppress(self, params):
|
|
|
|
pass
|
2016-05-25 17:37:40 +02:00
|
|
|
def update_evals(self, eventtime):
|
2017-03-13 00:55:56 +01:00
|
|
|
self.eval_globals['freq'] = self.mcu_freq
|
2017-09-19 03:41:00 +02:00
|
|
|
self.eval_globals['clock'] = self.clocksync.get_clock(eventtime)
|
2017-08-08 02:05:19 +02:00
|
|
|
def command_PINS(self, parts):
|
2019-08-09 17:42:50 +02:00
|
|
|
mcu_type = self.ser.get_msgparser().get_constant('MCU')
|
|
|
|
self.pins.add_pin_mapping(mcu_type, parts[1])
|
2017-08-08 02:05:19 +02:00
|
|
|
def command_SET(self, parts):
|
2016-06-12 04:31:05 +02:00
|
|
|
val = parts[2]
|
|
|
|
try:
|
2017-04-25 17:19:35 +02:00
|
|
|
val = float(val)
|
2016-06-12 04:31:05 +02:00
|
|
|
except ValueError:
|
2017-04-25 17:19:35 +02:00
|
|
|
pass
|
2016-06-12 04:31:05 +02:00
|
|
|
self.eval_globals[parts[1]] = val
|
2017-08-08 02:05:19 +02:00
|
|
|
def command_DELAY(self, parts):
|
2017-06-21 21:50:41 +02:00
|
|
|
try:
|
|
|
|
val = int(parts[1])
|
|
|
|
except ValueError as e:
|
|
|
|
self.output("Error: %s" % (str(e),))
|
|
|
|
return
|
|
|
|
try:
|
2018-02-27 20:16:16 +01:00
|
|
|
self.ser.send(' '.join(parts[2:]), minclock=val)
|
2017-06-21 21:50:41 +02:00
|
|
|
except msgproto.error as e:
|
|
|
|
self.output("Error: %s" % (str(e),))
|
|
|
|
return
|
2017-09-06 21:40:23 +02:00
|
|
|
def command_FLOOD(self, parts):
|
|
|
|
try:
|
|
|
|
count = int(parts[1])
|
|
|
|
delay = float(parts[2])
|
|
|
|
except ValueError as e:
|
|
|
|
self.output("Error: %s" % (str(e),))
|
|
|
|
return
|
2018-02-27 20:16:16 +01:00
|
|
|
msg = ' '.join(parts[3:])
|
|
|
|
delay_clock = int(delay * self.mcu_freq)
|
|
|
|
msg_clock = int(self.clocksync.get_clock(self.reactor.monotonic())
|
|
|
|
+ self.mcu_freq * .200)
|
2017-09-06 21:40:23 +02:00
|
|
|
try:
|
2018-02-27 20:16:16 +01:00
|
|
|
for i in range(count):
|
|
|
|
next_clock = msg_clock + delay_clock
|
|
|
|
self.ser.send(msg, minclock=msg_clock, reqclock=next_clock)
|
|
|
|
msg_clock = next_clock
|
2017-09-06 21:40:23 +02:00
|
|
|
except msgproto.error as e:
|
|
|
|
self.output("Error: %s" % (str(e),))
|
|
|
|
return
|
2017-09-06 22:34:55 +02:00
|
|
|
def command_SUPPRESS(self, parts):
|
|
|
|
oid = None
|
|
|
|
try:
|
|
|
|
name = parts[1]
|
|
|
|
if len(parts) > 2:
|
|
|
|
oid = int(parts[2])
|
|
|
|
except ValueError as e:
|
|
|
|
self.output("Error: %s" % (str(e),))
|
|
|
|
return
|
2019-06-20 23:36:46 +02:00
|
|
|
self.ser.register_response(self.handle_suppress, name, oid)
|
2017-08-08 02:49:42 +02:00
|
|
|
def command_STATS(self, parts):
|
2017-09-19 03:41:00 +02:00
|
|
|
curtime = self.reactor.monotonic()
|
|
|
|
self.output(' '.join([self.ser.stats(curtime),
|
|
|
|
self.clocksync.stats(curtime)]))
|
2017-08-08 02:40:32 +02:00
|
|
|
def command_LIST(self, parts):
|
|
|
|
self.update_evals(self.reactor.monotonic())
|
2019-06-21 04:08:38 +02:00
|
|
|
mp = self.ser.get_msgparser()
|
2021-02-18 20:01:40 +01:00
|
|
|
cmds = [msgformat for msgid, msgtype, msgformat in mp.get_messages()
|
|
|
|
if msgtype == 'command']
|
2017-08-08 02:40:32 +02:00
|
|
|
out = "Available mcu commands:"
|
2021-02-18 20:01:40 +01:00
|
|
|
out += "\n ".join([""] + sorted(cmds))
|
2017-08-08 02:40:32 +02:00
|
|
|
out += "\nAvailable artificial commands:"
|
|
|
|
out += "\n ".join([""] + [n for n in sorted(self.local_commands)])
|
|
|
|
out += "\nAvailable local variables:"
|
2019-02-27 19:00:30 +01:00
|
|
|
lvars = sorted(self.eval_globals.items())
|
|
|
|
out += "\n ".join([""] + ["%s: %s" % (k, v) for k, v in lvars])
|
2017-08-08 02:40:32 +02:00
|
|
|
self.output(out)
|
2017-08-08 02:05:19 +02:00
|
|
|
def command_HELP(self, parts):
|
|
|
|
self.output(help_txt)
|
2016-05-25 17:37:40 +02:00
|
|
|
def translate(self, line, eventtime):
|
|
|
|
evalparts = re_eval.split(line)
|
|
|
|
if len(evalparts) > 1:
|
|
|
|
self.update_evals(eventtime)
|
|
|
|
try:
|
|
|
|
for i in range(1, len(evalparts), 2):
|
2017-08-08 02:40:32 +02:00
|
|
|
e = eval(evalparts[i], dict(self.eval_globals))
|
2017-04-25 17:19:35 +02:00
|
|
|
if type(e) == type(0.):
|
|
|
|
e = int(e)
|
|
|
|
evalparts[i] = str(e)
|
2016-05-25 17:37:40 +02:00
|
|
|
except:
|
2017-04-25 17:16:02 +02:00
|
|
|
self.output("Unable to evaluate: %s" % (line,))
|
2016-05-25 17:37:40 +02:00
|
|
|
return None
|
|
|
|
line = ''.join(evalparts)
|
2017-04-25 17:16:02 +02:00
|
|
|
self.output("Eval: %s" % (line,))
|
2019-08-09 17:42:50 +02:00
|
|
|
try:
|
|
|
|
line = self.pins.update_command(line).strip()
|
|
|
|
except:
|
|
|
|
self.output("Unable to map pin: %s" % (line,))
|
|
|
|
return None
|
2016-05-25 17:37:40 +02:00
|
|
|
if line:
|
|
|
|
parts = line.split()
|
|
|
|
if parts[0] in self.local_commands:
|
|
|
|
self.local_commands[parts[0]](parts)
|
|
|
|
return None
|
2018-02-27 20:16:16 +01:00
|
|
|
return line
|
2016-05-25 17:37:40 +02:00
|
|
|
def process_kbd(self, eventtime):
|
|
|
|
self.data += os.read(self.fd, 4096)
|
|
|
|
|
|
|
|
kbdlines = self.data.split('\n')
|
|
|
|
for line in kbdlines[:-1]:
|
|
|
|
line = line.strip()
|
|
|
|
cpos = line.find('#')
|
|
|
|
if cpos >= 0:
|
|
|
|
line = line[:cpos]
|
|
|
|
if not line:
|
|
|
|
continue
|
|
|
|
msg = self.translate(line.strip(), eventtime)
|
|
|
|
if msg is None:
|
|
|
|
continue
|
2018-02-27 20:16:16 +01:00
|
|
|
try:
|
|
|
|
self.ser.send(msg)
|
|
|
|
except msgproto.error as e:
|
|
|
|
self.output("Error: %s" % (str(e),))
|
2016-05-25 17:37:40 +02:00
|
|
|
self.data = kbdlines[-1]
|
|
|
|
|
|
|
|
def main():
|
|
|
|
usage = "%prog [options] <serialdevice> <baud>"
|
|
|
|
opts = optparse.OptionParser(usage)
|
|
|
|
options, args = opts.parse_args()
|
|
|
|
serialport, baud = args
|
|
|
|
baud = int(baud)
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
r = reactor.Reactor()
|
|
|
|
ser = serialhdl.SerialReader(r, serialport, baud)
|
|
|
|
kbd = KeyboardReader(ser, r)
|
|
|
|
try:
|
|
|
|
r.run()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
sys.stdout.write("\n")
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|