pyterm: make Guido happy

This commit is contained in:
Oleg Hahm 2014-07-31 16:22:53 +02:00
parent 73f6a0c518
commit af24a947f6

View File

@ -15,7 +15,8 @@
# #
# You should have received a copy of the GNU Lesser General Public # You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
try: try:
@ -24,16 +25,18 @@ except ImportError:
import ConfigParser as configparser import ConfigParser as configparser
import cmd, serial, socket, sys, threading, readline, time, logging, os, \ import cmd, serial, socket, sys, threading, readline, time, logging, \
argparse, re, codecs, signal os, argparse, re, codecs, signal
### import twisted if available, define dummy classes otherwise ### import twisted if available, define dummy classes otherwise
try: try:
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.protocol import Protocol, ReconnectingClientFactory from twisted.internet.protocol import Protocol, \
ReconnectingClientFactory
except ImportError: except ImportError:
logging.getLogger("").warn("Twisted not available, please install it if you" logging.getLogger("").warn("Twisted not available, please install "
"want to use pyterm's JSON capabilities") "it if you want to use pyterm's JSON "
"capabilities")
class Protocol(): class Protocol():
def __init__(self): def __init__(self):
@ -67,11 +70,12 @@ default_fmt_str = '%(asctime)s - %(levelname)s # %(message)s'
class SerCmd(cmd.Cmd): class SerCmd(cmd.Cmd):
"""Main class for pyterm based on Python's Cmd class. """Main class for pyterm based on Python's Cmd class.
Runs an interactive terminal that transfer between stdio and serial port. Runs an interactive terminal that transfer between stdio and serial
port.
""" """
def __init__(self, port=None, baudrate=None, tcp_serial=None, confdir=None, def __init__(self, port=None, baudrate=None, tcp_serial=None,
conffile=None, host=None, run_name=None): confdir=None, conffile=None, host=None, run_name=None):
"""Constructor. """Constructor.
Args: Args:
@ -123,7 +127,8 @@ class SerCmd(cmd.Cmd):
### create Logging object ### create Logging object
my_millis = "{:.4f}".format(time.time()) my_millis = "{:.4f}".format(time.time())
date_str = '{}.{}'.format(time.strftime('%Y%m%d-%H:%M:%S'), my_millis[-4:]) date_str = '{}.{}'.format(time.strftime('%Y%m%d-%H:%M:%S'),
my_millis[-4:])
self.startup = date_str self.startup = date_str
# create formatter # create formatter
formatter = logging.Formatter(self.fmt_str) formatter = logging.Formatter(self.fmt_str)
@ -131,8 +136,10 @@ class SerCmd(cmd.Cmd):
directory = self.configdir + os.path.sep + self.host directory = self.configdir + os.path.sep + self.host
if not os.path.exists(directory): if not os.path.exists(directory):
os.makedirs(directory) os.makedirs(directory)
logging.basicConfig(filename=directory + os.path.sep + self.run_name + '.log', \ logging.basicConfig(filename = directory + os.path.sep +
level=logging.DEBUG, format=self.fmt_str) self.run_name + '.log',
level=logging.DEBUG,
format=self.fmt_str)
ch = logging.StreamHandler() ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG) ch.setLevel(logging.DEBUG)
@ -152,13 +159,16 @@ class SerCmd(cmd.Cmd):
# if no serial or TCP is specified use default serial port # if no serial or TCP is specified use default serial port
if not self.port and not self.tcp_serial: if not self.port and not self.tcp_serial:
sys.stderr.write("No port specified, using default (%s)!\n" % (defaultport)) sys.stderr.write("No port specified, using default (%s)!\n"
% (defaultport))
self.port = defaultport self.port = defaultport
# if a TCP port is specified try to connect # if a TCP port is specified try to connect
if self.tcp_serial: if self.tcp_serial:
self.logger.info("Connect to localhost:%s" % self.tcp_serial) self.logger.info("Connect to localhost:%s"
for res in socket.getaddrinfo('localhost', self.tcp_serial, \ % self.tcp_serial)
socket.AF_UNSPEC, socket.SOCK_STREAM): for res in socket.getaddrinfo('localhost', self.tcp_serial,
socket.AF_UNSPEC,
socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res af, socktype, proto, canonname, sa = res
try: try:
s = fdsocket(af, socktype, proto) s = fdsocket(af, socktype, proto)
@ -175,15 +185,16 @@ class SerCmd(cmd.Cmd):
if s: if s:
self.ser = s self.ser = s
else: else:
self.logger.error("Something went wrong connecting to localhost:%s" self.logger.error("Something went wrong connecting to "
% self.tcp_serial) "localhost:%s" % self.tcp_serial)
sys.exit(1) sys.exit(1)
# otherwise go for the serial port # otherwise go for the serial port
elif self.port: elif self.port:
self.logger.info("Connect to serial port %s" % self.port) self.logger.info("Connect to serial port %s" % self.port)
self.serial_connect() self.serial_connect()
# wait until connection is established and fire startup commands to the node # wait until connection is established and fire startup
# commands to the node
time.sleep(1) time.sleep(1)
for cmd in self.init_cmd: for cmd in self.init_cmd:
self.logger.debug("WRITE ----->>>>>> '" + cmd + "'\n") self.logger.debug("WRITE ----->>>>>> '" + cmd + "'\n")
@ -195,8 +206,8 @@ class SerCmd(cmd.Cmd):
receiver_thread.start() receiver_thread.start()
def precmd(self, line): def precmd(self, line):
"""Check for command prefixes to distinguish between Pyterm interal """Check for command prefixes to distinguish between Pyterm
commands and commands that should be send to the node. interal commands and commands that should be send to the node.
""" """
self.logger.debug("processing line #%s#" % line) self.logger.debug("processing line #%s#" % line)
if (line.startswith("/")): if (line.startswith("/")):
@ -204,16 +215,18 @@ class SerCmd(cmd.Cmd):
return line return line
def default(self, line): def default(self, line):
"""In case of no Pyterm specific prefix is detected, split string by """In case of no Pyterm specific prefix is detected, split
colons and send it to the node. string by colons and send it to the node.
""" """
self.logger.debug("%s is no pyterm command, sending to default out" % line) self.logger.debug("%s is no pyterm command, sending to default "
"out" % line)
for tok in line.split(';'): for tok in line.split(';'):
tok = self.get_alias(tok) tok = self.get_alias(tok)
self.ser.write((tok.strip() + "\n").encode("utf-8")) self.ser.write((tok.strip() + "\n").encode("utf-8"))
def do_help(self, line): def do_help(self, line):
"""Do not use Cmd's internal help function, but redirect to the node. """Do not use Cmd's internal help function, but redirect to the
node.
""" """
self.ser.write("help\n".encode("utf-8")) self.ser.write("help\n".encode("utf-8"))
@ -257,12 +270,14 @@ class SerCmd(cmd.Cmd):
if not self.config.has_section("triggers"): if not self.config.has_section("triggers"):
self.config.add_section("triggers") self.config.add_section("triggers")
for trigger in self.triggers: for trigger in self.triggers:
self.config.set("triggers", trigger.pattern, self.triggers[trigger]) self.config.set("triggers", trigger.pattern,
self.triggers[trigger])
if len(self.json_regs): if len(self.json_regs):
if not self.config.has_section("json_regs"): if not self.config.has_section("json_regs"):
self.config.add_section("json_regs") self.config.add_section("json_regs")
for j in self.json_regs: for j in self.json_regs:
self.config.set("json_regs", j, self.json_regs[j].pattern) self.config.set("json_regs", j,
self.json_regs[j].pattern)
if len(self.filters): if len(self.filters):
if not self.config.has_section("filters"): if not self.config.has_section("filters"):
self.config.add_section("filters") self.config.add_section("filters")
@ -285,7 +300,8 @@ class SerCmd(cmd.Cmd):
self.config.set("init_cmd", "init_cmd%i" % i, ic) self.config.set("init_cmd", "init_cmd%i" % i, ic)
i += 1 i += 1
with open(self.configdir + os.path.sep + self.configfile, 'wb') as config_fd: with open(self.configdir + os.path.sep + self.configfile, 'wb')\
as config_fd:
self.config.write(config_fd) self.config.write(config_fd)
self.logger.info("Config saved") self.logger.info("Config saved")
@ -296,18 +312,21 @@ class SerCmd(cmd.Cmd):
print(str(key) + ": " + str(self.__dict__[key])) print(str(key) + ": " + str(self.__dict__[key]))
def do_PYTERM_alias(self, line): def do_PYTERM_alias(self, line):
"""Pyterm command: Register an alias or show an list of all registered aliases. """Pyterm command: Register an alias or show an list of all
registered aliases.
""" """
if line.endswith("list"): if line.endswith("list"):
for alias in self.aliases: for alias in self.aliases:
self.logger.info("{} = {}".format(alias, self.aliases[alias])) self.logger.info("{} = {}".format(alias,
self.aliases[alias]))
return return
if not line.count("="): if not line.count("="):
sys.stderr.write("Usage: /alias <ALIAS> = <CMD>\n") sys.stderr.write("Usage: /alias <ALIAS> = <CMD>\n")
return return
alias = line.split('=')[0].strip() alias = line.split('=')[0].strip()
command = line[line.index('=')+1:].strip() command = line[line.index('=')+1:].strip()
self.logger.info("adding command %s for alias %s" % (command, alias)) self.logger.info("adding command %s for alias %s"
% (command, alias))
self.aliases[alias] = command self.aliases[alias] = command
def do_PYTERM_rmalias(self, line): def do_PYTERM_rmalias(self, line):
@ -332,7 +351,8 @@ class SerCmd(cmd.Cmd):
return return
trigger = line.split('=')[0].strip() trigger = line.split('=')[0].strip()
action = line[line.index('=')+1:].strip() action = line[line.index('=')+1:].strip()
self.logger.info("adding action %s for trigger %s" % (action, trigger)) self.logger.info("adding action %s for trigger %s" % (action,
trigger))
self.triggers[re.compile(trigger)] = action self.triggers[re.compile(trigger)] = action
def do_PYTERM_rmtrigger(self, line): def do_PYTERM_rmtrigger(self, line):
@ -372,9 +392,11 @@ class SerCmd(cmd.Cmd):
sys.stderr.write("Filter for %s not found\n" % line.strip()) sys.stderr.write("Filter for %s not found\n" % line.strip())
def do_PYTERM_json(self, line): def do_PYTERM_json(self, line):
"""Pyterm command: Transfer lines matching this Regex as JSON object. """Pyterm command: Transfer lines matching this Regex as JSON
object.
""" """
self.json_regs[line.split(' ')[0].strip()] = re.compile(line.partition(' ')[2].strip()) self.json_regs[line.split(' ')[0].strip()] = \
re.compile(line.partition(' ')[2].strip())
def do_PYTERM_unjson(self, line): def do_PYTERM_unjson(self, line):
"""Pyterm command: Remove a JSON filter. """Pyterm command: Remove a JSON filter.
@ -383,7 +405,8 @@ class SerCmd(cmd.Cmd):
sys.stderr.write("JSON regex with ID %s not found" % line) sys.stderr.write("JSON regex with ID %s not found" % line)
def do_PYTERM_init(self, line): def do_PYTERM_init(self, line):
"""Pyterm command: Add an startup command. (Only useful in addition with /save). """Pyterm command: Add an startup command. (Only useful in
addition with /save).
""" """
self.init_cmd.append(line.strip()) self.init_cmd.append(line.strip())
@ -391,25 +414,31 @@ class SerCmd(cmd.Cmd):
"""Internal function to laod configuration from file. """Internal function to laod configuration from file.
""" """
self.config = configparser.SafeConfigParser() self.config = configparser.SafeConfigParser()
self.config.read([self.configdir + os.path.sep + self.configfile]) self.config.read([self.configdir + os.path.sep + \
self.configfile])
for sec in self.config.sections(): for sec in self.config.sections():
if sec == "filters": if sec == "filters":
for opt in self.config.options(sec): for opt in self.config.options(sec):
self.filters.append(re.compile(self.config.get(sec, opt))) self.filters.append(
re.compile(self.config.get(sec, opt)))
if sec == "ignores": if sec == "ignores":
for opt in self.config.options(sec): for opt in self.config.options(sec):
self.ignores.append(re.compile(self.config.get(sec, opt))) self.ignores.append(
re.compile(self.config.get(sec, opt)))
if sec == "json_regs": if sec == "json_regs":
for opt in self.config.options(sec): for opt in self.config.options(sec):
self.logger.info("add json regex for %s" % self.config.get(sec, opt)) self.logger.info("add json regex for %s"
self.json_regs[opt] = re.compile(self.config.get(sec, opt)) % self.config.get(sec, opt))
self.json_regs[opt] = \
re.compile(self.config.get(sec, opt))
if sec == "aliases": if sec == "aliases":
for opt in self.config.options(sec): for opt in self.config.options(sec):
self.aliases[opt] = self.config.get(sec, opt) self.aliases[opt] = self.config.get(sec, opt)
if sec == "triggers": if sec == "triggers":
for opt in self.config.options(sec): for opt in self.config.options(sec):
self.triggers[re.compile(opt)] = self.config.get(sec, opt) self.triggers[re.compile(opt)] = \
self.config.get(sec, opt)
if sec == "init_cmd": if sec == "init_cmd":
for opt in self.config.options(sec): for opt in self.config.options(sec):
self.init_cmd.append(self.config.get(sec, opt)) self.init_cmd.append(self.config.get(sec, opt))
@ -419,7 +448,8 @@ class SerCmd(cmd.Cmd):
self.__dict__[opt] = self.config.get(sec, opt) self.__dict__[opt] = self.config.get(sec, opt)
def process_line(self, line): def process_line(self, line):
"""Processes a valid line from node that should be printed and possibly forwarded. """Processes a valid line from node that should be printed and
possibly forwarded.
Args: Args:
line (str): input from node. line (str): input from node.
@ -427,13 +457,16 @@ class SerCmd(cmd.Cmd):
self.logger.info(line) self.logger.info(line)
# check if line matches a trigger and fire the command(s) # check if line matches a trigger and fire the command(s)
for trigger in self.triggers: for trigger in self.triggers:
self.logger.debug("comparing input %s to trigger %s" % (line, trigger.pattern)) self.logger.debug("comparing input %s to trigger %s"
% (line, trigger.pattern))
m = trigger.search(line) m = trigger.search(line)
if m: if m:
self.onecmd(self.precmd(self.triggers[trigger])) self.onecmd(self.precmd(self.triggers[trigger]))
# ckecking if the line should be sent as JSON object to a tcp server # ckecking if the line should be sent as JSON object to a tcp
if (len(self.json_regs)) and self.factory and self.factory.myproto: # server
if (len(self.json_regs)) and self.factory and \
self.factory.myproto:
for j in self.json_regs: for j in self.json_regs:
m = self.json_regs[j].search(line) m = self.json_regs[j].search(line)
if m: if m:
@ -446,9 +479,11 @@ class SerCmd(cmd.Cmd):
json_obj += '"date":%s, ' % int(time.time()*1000) json_obj += '"date":%s, ' % int(time.time()*1000)
for g in m.groupdict(): for g in m.groupdict():
try: try:
json_obj += '"%s":%d, ' % (g, int(m.groupdict()[g])) json_obj += '"%s":%d, ' \
% (g, int(m.groupdict()[g]))
except ValueError: except ValueError:
json_obj += '"%s":"%s", ' % (g, m.groupdict()[g]) json_obj += '"%s":"%s", ' \
% (g, m.groupdict()[g])
# eliminate the superfluous last ", " # eliminate the superfluous last ", "
json_obj = json_obj[:-2] json_obj = json_obj[:-2]
@ -457,7 +492,8 @@ class SerCmd(cmd.Cmd):
self.factory.myproto.sendMessage(json_obj) self.factory.myproto.sendMessage(json_obj)
def handle_line(self, line): def handle_line(self, line):
"""Handle line from node and check for further processing requirements. """Handle line from node and check for further processing
requirements.
Args: Args:
line (str): input line from node. line (str): input line from node.
@ -494,15 +530,18 @@ class SerCmd(cmd.Cmd):
while (1): while (1):
# check if serial port can be accessed. # check if serial port can be accessed.
try: try:
sr = codecs.getreader("UTF-8")(self.ser, errors='replace') sr = codecs.getreader("UTF-8")(self.ser,
errors='replace')
c = sr.read(1) c = sr.read(1)
# try to re-open it with a timeout of 1s otherwise # try to re-open it with a timeout of 1s otherwise
except (serial.SerialException, ValueError) as se: except (serial.SerialException, ValueError) as se:
self.logger.warn("Serial port disconnected, waiting to get reconnected...") self.logger.warn("Serial port disconnected, waiting to "
"get reconnected...")
self.ser.close() self.ser.close()
time.sleep(1) time.sleep(1)
if os.path.exists(self.port): if os.path.exists(self.port):
self.logger.warn("Try to reconnect to %s again..." % (self.port)) self.logger.warn("Try to reconnect to %s again..."
% (self.port))
self.serial_connect() self.serial_connect()
continue continue
if c == '\n' or c == '\r': if c == '\n' or c == '\r':
@ -534,11 +573,13 @@ class PytermClientFactory(ReconnectingClientFactory):
def clientConnectionLost(self, connector, reason): def clientConnectionLost(self, connector, reason):
if reactor.running: if reactor.running:
print('Lost connection. Reason:', reason) print('Lost connection. Reason:', reason)
ReconnectingClientFactory.clientConnectionLost(self, connector, reason) ReconnectingClientFactory.clientConnectionLost(self, connector,
reason)
def clientConnectionFailed(self, connector, reason): def clientConnectionFailed(self, connector, reason):
print('Connection failed. Reason:', reason) print('Connection failed. Reason:', reason)
ReconnectingClientFactory.clientConnectionFailed(self, connector, ReconnectingClientFactory.clientConnectionFailed(self,
connector,
reason) reason)
def __stop_reactor(signum, stackframe): def __stop_reactor(signum, stackframe):
@ -552,29 +593,36 @@ class fdsocket(socket.socket):
try: try:
return self.sendall(string) return self.sendall(string)
except socket.error as e: except socket.error as e:
logging.getLogger("").warn("Error in TCP connection (%s), closing down" % str(e)) logging.getLogger("").warn("Error in TCP connection (%s), "
"closing down" % str(e))
self.close() self.close()
sys.exit(0) sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Pyterm - The Python terminal program") parser = argparse.ArgumentParser(description="Pyterm - The Python "
"terminal program")
parser.add_argument("-p", "--port", parser.add_argument("-p", "--port",
help="Specifies the serial port to use, default is %s" % defaultport, help="Specifies the serial port to use, default is %s"
% defaultport,
default=defaultport) default=defaultport)
parser.add_argument("-ts", "--tcp-serial", parser.add_argument("-ts", "--tcp-serial",
help="Connect to a TCP port instead of a serial port") help="Connect to a TCP port instead of a serial port")
parser.add_argument("-b", "--baudrate", parser.add_argument("-b", "--baudrate",
help="Specifies baudrate for the serial port, default is %s" % defaultbaud, help="Specifies baudrate for the serial port, default is %s"
% defaultbaud,
default=defaultbaud) default=defaultbaud)
parser.add_argument('-d', '--directory', parser.add_argument('-d', '--directory',
help="Specify the Pyterm directory, default is %s" % defaultdir, help="Specify the Pyterm directory, default is %s"
% defaultdir,
default=defaultdir) default=defaultdir)
parser.add_argument("-c", "--config", parser.add_argument("-c", "--config",
help="Specify the config filename, default is %s" % defaultfile, help="Specify the config filename, default is %s"
% defaultfile,
default=defaultfile) default=defaultfile)
parser.add_argument("-s", "--server", parser.add_argument("-s", "--server",
help="Connect via TCP to this server to send output as JSON") help="Connect via TCP to this server to send output as "
"JSON")
parser.add_argument("-P", "--tcp_port", type=int, parser.add_argument("-P", "--tcp_port", type=int,
help="Port at the JSON server") help="Port at the JSON server")
parser.add_argument("-H", "--host", parser.add_argument("-H", "--host",
@ -583,15 +631,17 @@ if __name__ == "__main__":
help="Run name, used for logfile") help="Run name, used for logfile")
args = parser.parse_args() args = parser.parse_args()
myshell = SerCmd(args.port, args.baudrate, args.tcp_serial, args.directory, args.config, args.host, args.run_name) myshell = SerCmd(args.port, args.baudrate, args.tcp_serial,
args.directory, args.config, args.host, args.run_name)
myshell.prompt = '' myshell.prompt = ''
if args.server and args.tcp_port: if args.server and args.tcp_port:
myfactory = PytermClientFactory() myfactory = PytermClientFactory()
reactor.connectTCP(args.server, args.tcp_port, myfactory) reactor.connectTCP(args.server, args.tcp_port, myfactory)
myshell.factory = myfactory myshell.factory = myfactory
reactor.callInThread(myshell.cmdloop, "Welcome to pyterm!\nType '/exit' to exit.") reactor.callInThread(myshell.cmdloop, "Welcome to pyterm!\n"
"Type '/exit' to exit.")
signal.signal(signal.SIGINT, __stop_reactor) signal.signal(signal.SIGINT, __stop_reactor)
reactor.run() reactor.run()
else: else:
myshell.cmdloop("Welcome to pyterm!\nType 'exit' to exit.") myshell.cmdloop("Welcome to pyterm!\nType '/exit' to exit.")