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
# 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:
@ -24,16 +25,18 @@ except ImportError:
import ConfigParser as configparser
import cmd, serial, socket, sys, threading, readline, time, logging, os, \
argparse, re, codecs, signal
import cmd, serial, socket, sys, threading, readline, time, logging, \
os, argparse, re, codecs, signal
### import twisted if available, define dummy classes otherwise
try:
from twisted.internet import reactor
from twisted.internet.protocol import Protocol, ReconnectingClientFactory
from twisted.internet.protocol import Protocol, \
ReconnectingClientFactory
except ImportError:
logging.getLogger("").warn("Twisted not available, please install it if you"
"want to use pyterm's JSON capabilities")
logging.getLogger("").warn("Twisted not available, please install "
"it if you want to use pyterm's JSON "
"capabilities")
class Protocol():
def __init__(self):
@ -67,11 +70,12 @@ default_fmt_str = '%(asctime)s - %(levelname)s # %(message)s'
class SerCmd(cmd.Cmd):
"""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,
conffile=None, host=None, run_name=None):
def __init__(self, port=None, baudrate=None, tcp_serial=None,
confdir=None, conffile=None, host=None, run_name=None):
"""Constructor.
Args:
@ -123,7 +127,8 @@ class SerCmd(cmd.Cmd):
### create Logging object
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
# create formatter
formatter = logging.Formatter(self.fmt_str)
@ -131,8 +136,10 @@ class SerCmd(cmd.Cmd):
directory = self.configdir + os.path.sep + self.host
if not os.path.exists(directory):
os.makedirs(directory)
logging.basicConfig(filename=directory + os.path.sep + self.run_name + '.log', \
level=logging.DEBUG, format=self.fmt_str)
logging.basicConfig(filename = directory + os.path.sep +
self.run_name + '.log',
level=logging.DEBUG,
format=self.fmt_str)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
@ -152,13 +159,16 @@ class SerCmd(cmd.Cmd):
# if no serial or TCP is specified use default serial port
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
# if a TCP port is specified try to connect
if self.tcp_serial:
self.logger.info("Connect to localhost:%s" % self.tcp_serial)
for res in socket.getaddrinfo('localhost', self.tcp_serial, \
socket.AF_UNSPEC, socket.SOCK_STREAM):
self.logger.info("Connect to localhost:%s"
% self.tcp_serial)
for res in socket.getaddrinfo('localhost', self.tcp_serial,
socket.AF_UNSPEC,
socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
try:
s = fdsocket(af, socktype, proto)
@ -175,15 +185,16 @@ class SerCmd(cmd.Cmd):
if s:
self.ser = s
else:
self.logger.error("Something went wrong connecting to localhost:%s"
% self.tcp_serial)
self.logger.error("Something went wrong connecting to "
"localhost:%s" % self.tcp_serial)
sys.exit(1)
# otherwise go for the serial port
elif self.port:
self.logger.info("Connect to serial port %s" % self.port)
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)
for cmd in self.init_cmd:
self.logger.debug("WRITE ----->>>>>> '" + cmd + "'\n")
@ -195,8 +206,8 @@ class SerCmd(cmd.Cmd):
receiver_thread.start()
def precmd(self, line):
"""Check for command prefixes to distinguish between Pyterm interal
commands and commands that should be send to the node.
"""Check for command prefixes to distinguish between Pyterm
interal commands and commands that should be send to the node.
"""
self.logger.debug("processing line #%s#" % line)
if (line.startswith("/")):
@ -204,16 +215,18 @@ class SerCmd(cmd.Cmd):
return line
def default(self, line):
"""In case of no Pyterm specific prefix is detected, split string by
colons and send it to the node.
"""In case of no Pyterm specific prefix is detected, split
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(';'):
tok = self.get_alias(tok)
self.ser.write((tok.strip() + "\n").encode("utf-8"))
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"))
@ -257,12 +270,14 @@ class SerCmd(cmd.Cmd):
if not self.config.has_section("triggers"):
self.config.add_section("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 not self.config.has_section("json_regs"):
self.config.add_section("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 not self.config.has_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)
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.logger.info("Config saved")
@ -296,18 +312,21 @@ class SerCmd(cmd.Cmd):
print(str(key) + ": " + str(self.__dict__[key]))
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"):
for alias in self.aliases:
self.logger.info("{} = {}".format(alias, self.aliases[alias]))
self.logger.info("{} = {}".format(alias,
self.aliases[alias]))
return
if not line.count("="):
sys.stderr.write("Usage: /alias <ALIAS> = <CMD>\n")
return
alias = line.split('=')[0].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
def do_PYTERM_rmalias(self, line):
@ -332,7 +351,8 @@ class SerCmd(cmd.Cmd):
return
trigger = line.split('=')[0].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
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())
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):
"""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)
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())
@ -391,25 +414,31 @@ class SerCmd(cmd.Cmd):
"""Internal function to laod configuration from file.
"""
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():
if sec == "filters":
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":
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":
for opt in self.config.options(sec):
self.logger.info("add json regex for %s" % self.config.get(sec, opt))
self.json_regs[opt] = re.compile(self.config.get(sec, opt))
self.logger.info("add json regex for %s"
% self.config.get(sec, opt))
self.json_regs[opt] = \
re.compile(self.config.get(sec, opt))
if sec == "aliases":
for opt in self.config.options(sec):
self.aliases[opt] = self.config.get(sec, opt)
if sec == "triggers":
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":
for opt in self.config.options(sec):
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)
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:
line (str): input from node.
@ -427,13 +457,16 @@ class SerCmd(cmd.Cmd):
self.logger.info(line)
# check if line matches a trigger and fire the command(s)
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)
if m:
self.onecmd(self.precmd(self.triggers[trigger]))
# ckecking if the line should be sent as JSON object to a tcp server
if (len(self.json_regs)) and self.factory and self.factory.myproto:
# ckecking if the line should be sent as JSON object to a tcp
# server
if (len(self.json_regs)) and self.factory and \
self.factory.myproto:
for j in self.json_regs:
m = self.json_regs[j].search(line)
if m:
@ -446,9 +479,11 @@ class SerCmd(cmd.Cmd):
json_obj += '"date":%s, ' % int(time.time()*1000)
for g in m.groupdict():
try:
json_obj += '"%s":%d, ' % (g, int(m.groupdict()[g]))
json_obj += '"%s":%d, ' \
% (g, int(m.groupdict()[g]))
except ValueError:
json_obj += '"%s":"%s", ' % (g, m.groupdict()[g])
json_obj += '"%s":"%s", ' \
% (g, m.groupdict()[g])
# eliminate the superfluous last ", "
json_obj = json_obj[:-2]
@ -457,7 +492,8 @@ class SerCmd(cmd.Cmd):
self.factory.myproto.sendMessage(json_obj)
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:
line (str): input line from node.
@ -494,15 +530,18 @@ class SerCmd(cmd.Cmd):
while (1):
# check if serial port can be accessed.
try:
sr = codecs.getreader("UTF-8")(self.ser, errors='replace')
sr = codecs.getreader("UTF-8")(self.ser,
errors='replace')
c = sr.read(1)
# try to re-open it with a timeout of 1s otherwise
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()
time.sleep(1)
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()
continue
if c == '\n' or c == '\r':
@ -534,11 +573,13 @@ class PytermClientFactory(ReconnectingClientFactory):
def clientConnectionLost(self, connector, reason):
if reactor.running:
print('Lost connection. Reason:', reason)
ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
ReconnectingClientFactory.clientConnectionLost(self, connector,
reason)
def clientConnectionFailed(self, connector, reason):
print('Connection failed. Reason:', reason)
ReconnectingClientFactory.clientConnectionFailed(self, connector,
ReconnectingClientFactory.clientConnectionFailed(self,
connector,
reason)
def __stop_reactor(signum, stackframe):
@ -552,29 +593,36 @@ class fdsocket(socket.socket):
try:
return self.sendall(string)
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()
sys.exit(0)
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",
help="Specifies the serial port to use, default is %s" % defaultport,
help="Specifies the serial port to use, default is %s"
% defaultport,
default=defaultport)
parser.add_argument("-ts", "--tcp-serial",
help="Connect to a TCP port instead of a serial port")
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)
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)
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)
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,
help="Port at the JSON server")
parser.add_argument("-H", "--host",
@ -583,15 +631,17 @@ if __name__ == "__main__":
help="Run name, used for logfile")
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 = ''
if args.server and args.tcp_port:
myfactory = PytermClientFactory()
reactor.connectTCP(args.server, args.tcp_port, 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)
reactor.run()
else:
myshell.cmdloop("Welcome to pyterm!\nType 'exit' to exit.")
myshell.cmdloop("Welcome to pyterm!\nType '/exit' to exit.")