From: Philipp Klaus Date: Thu, 2 Jul 2015 09:30:50 +0000 (+0200) Subject: more cmds in CLI, network discovery, fake server X-Git-Url: https://jspc29.x-matter.uni-frankfurt.de/git/?a=commitdiff_plain;h=3cd257dd0eba0a161b6c69d12d5b9233c396ee52;p=labtools.git more cmds in CLI, network discovery, fake server --- diff --git a/opus20/opus20/__init__.py b/opus20/opus20/__init__.py index 5a35af4..3545074 100644 --- a/opus20/opus20/__init__.py +++ b/opus20/opus20/__init__.py @@ -3,6 +3,8 @@ from .opus20 import Opus20, Frame, PickleStore from .opus20 import CHANNEL_SPEC as OPUS20_CHANNEL_SPEC from .opus20 import Opus20Exception, Opus20ConnectionException from .opus20 import Object +from .opus20 import discover_OPUS20_devices +from .fakeserver import Opus20FakeServer from .webapp import PlotWebServer diff --git a/opus20/opus20/fakeserver.py b/opus20/opus20/fakeserver.py new file mode 100644 index 0000000..4b06b8e --- /dev/null +++ b/opus20/opus20/fakeserver.py @@ -0,0 +1,104 @@ + + +from datetime import datetime, timedelta +import socket + +from .opus20 import Frame + +class Opus20FakeServer(object): + """ + A TCP server imitating (faking) the + behaviour of a Lufft OPUS20 device. + """ + def __init__(self, host='', port=52015): + self.host = host + self.port = port + self.communication_samples = [] + + def bind_and_serve(self): + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.bind((self.host, self.port)) + self.s.listen(1) + try: + while True: + conn, addr = self.s.accept() + print('Connected by', addr) + while True: + data = conn.recv(1024) + if not data: break + input_frame = Frame(data) + output_frame = self.react_to_input_frame(input_frame) + conn.sendall(output_frame.data) + conn.close() + except KeyboardInterrupt: + pass + finally: + self.s.close() + + def react_to_input_frame(self, input_frame): + output_frame = None + input_frame.validate() + in_props = input_frame.props + for sample in self.communication_samples: + sample_props = sample['in'].props + if in_props.cmd != sample_props.cmd: + continue + if in_props.payload != sample_props.payload: + continue + output_frame = sample['out'] + break + if output_frame is None: + # 0x10 - Unknown CMD + output_frame = Frame.from_cmd_and_payload(in_props.cmd, b"\x10") + return output_frame + + def feed_with_communication_log(self, l2p_frames_file): + """ Feed the fake server with l2p frames stored in a communication log file """ + num_incoming, num_outgoing = 0, 0 + num_short, num_long = 0, 0 + + + def gt(dt_str): + dt, _, us= dt_str.partition(".") + dt= datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S") + us= int(us.rstrip("Z"), 10) + return dt + timedelta(microseconds=us) + #return dt + + with open(l2p_frames_file) as fp: + + timestamp = None + incoming = None + outgoing = None + + for line in fp: + + line = line.strip() + if not line: continue + + kind = None + if line.startswith('Timestamp'): + kind = 'timestamp' + timestamp = gt(line.split(' ')[1].replace(' ', 'T')) + elif line.startswith('<- '): + kind = 'incoming' + num_incoming += 1 + elif line.startswith('-> '): + kind = 'outgoing' + num_outgoing += 1 + + if kind in ('incoming', 'outgoing'): + frame_bytes = bytes(int(byte, 16) for byte in line[3:].split()) + frm = Frame(frame_bytes) + try: + frm.validate() + except Exception as e: + print(e) + pdb.set_trace() + + if kind == 'incoming': + incoming = frm + elif kind == 'outgoing': + outgoing = frm + self.communication_samples.append({'ts': timestamp, 'in': incoming, 'out': outgoing}) + diff --git a/opus20/opus20/opus20.py b/opus20/opus20/opus20.py index 6be8409..1c9aaf0 100644 --- a/opus20/opus20/opus20.py +++ b/opus20/opus20/opus20.py @@ -8,7 +8,10 @@ import time import logging from datetime import datetime, timedelta import pickle +import threading +import ipaddress +clock = time.perf_counter logger = logging.getLogger(__name__) class Opus20(object): @@ -98,6 +101,80 @@ class Opus20(object): self.device_id = ''.join("{:02X}".format(byte) for byte in answer.props.payload[2:2+6]) logger.info("Connected to device with ID: " + self.device_id) + def sync_datetime(self, new_datetime=None, tz_offset=None): + if not new_datetime: new_datetime = datetime.now().replace(microsecond=0) + if not tz_offset: tz_offset = round((datetime.now() - datetime.utcnow()).total_seconds()) + offset_sign = '+' if tz_offset >= 0 else '-' + offset_hours = abs(tz_offset) // 3600; + offset_minutes = (abs(tz_offset) % 3600) // 60; + logger.info("Setting date & time on device to {}{:+03}{:02}".format(new_datetime.isoformat(), offset_hours, offset_minutes)) + new_datetime = int(new_datetime.timestamp()) + frame = Frame.from_cmd_and_payload(0x27, struct.pack('= 3, 'message too short for an answer containing the available channels' - assert props.cmd == 0x31 and props.verc == 0x10 and props.payload[1] == 0x16 + assert props.cmd == 0x31 and props.payload[1] == 0x16 self.assert_status() logger.debug("Channel Query (31 10 16)") @@ -444,8 +536,9 @@ class Frame(object): def channel_properties(self): props = self.props assert len(props.payload) == 85 - assert props.cmd == 0x31 and props.verc == 0x10 - assert props.payload[0:2] == b"\x00\x30" + assert props.cmd == 0x31 + self.assert_status() + assert props.payload[1] == 0x30 channel, group, name, unit, kind, min, max = struct.unpack('= 3, 'message too short for an online data request with a single channel' - assert props.cmd == 0x23 and props.verc == 0x10 + assert props.cmd == 0x23 self.assert_status() logger.debug("Online Data Request (single channel) (23 10)") channel_value = Frame.read_channel_value(props.payload, 1, length=7, status=self.status) @@ -471,7 +564,7 @@ class Frame(object): props = self.props assert props.length >= 3, 'message too short for an online data request with multiple channels' - assert props.cmd == 0x2F and props.verc == 0x10 + assert props.cmd == 0x2F self.assert_status() logger.debug("Online Data Request (multiple channels) (2F 10)") @@ -493,7 +586,8 @@ class Frame(object): props = self.props assert props.length >= 21, 'message too short for a log data message' - assert props.cmd == 0x24 and props.verc == 0x10 and props.payload[0:2] == b"\x00\x20" + assert props.cmd == 0x24 and props.payload[1] == 0x20 + self.assert_status() is_final, begin, end, interval, num_blocks = struct.unpack('',52010) + myUDPintsockThread = UdpListenerThread(52005, callback) + myUDPintsockThread.start() + + UDPoutsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # to allow broadcast communication: + UDPoutsock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + UDPoutsock.bind((bind_addr,0)) + frm = Frame.from_cmd_and_payload(0x1e, b"") + UDPoutsock.sendto(frm.data, dest) + + myUDPintsockThread.join() + def crc16(data : bytes): """ Calculates a CRC-16 CCITT checksum. data should be of type bytes() """ # https://en.wikipedia.org/wiki/Cyclic_redundancy_check diff --git a/opus20/opus20/webapp/__init__.py b/opus20/opus20/webapp/__init__.py index 4a307de..3d525b2 100755 --- a/opus20/opus20/webapp/__init__.py +++ b/opus20/opus20/webapp/__init__.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) # Find out where our resource files are located: try: - from pkg_resources import resource_filename, Requirement + from pkg_resources import resource_filename, Requirement, require PATH = resource_filename("opus20", "webapp") except: PATH = './' @@ -72,7 +72,8 @@ class PlotWebServer(Bottle): @view('about.jinja2') def _about_page(self): - return self._atg({'active': 'about'}) + version = require("opus20")[0].version + return self._atg({'active': 'about', 'opus20_version': version}) @view('plots.jinja2') def _plots_page(self): diff --git a/opus20/opus20/webapp/views/about.jinja2 b/opus20/opus20/webapp/views/about.jinja2 index 28f480e..6e4f815 100644 --- a/opus20/opus20/webapp/views/about.jinja2 +++ b/opus20/opus20/webapp/views/about.jinja2 @@ -7,6 +7,7 @@

Environmental Monitoring with OPUS20

+

Version: v{{opus20_version}}

The Python package opus20 allows for easy environmental monitoring using the OPUS20 logger by Lufft. It was written by Philipp Klaus for use in the clean room laboratory of the X-Matter Group at the University of Frankfurt. diff --git a/opus20/opus20/webapp/views/plots.jinja2 b/opus20/opus20/webapp/views/plots.jinja2 index 22a9294..8941a95 100644 --- a/opus20/opus20/webapp/views/plots.jinja2 +++ b/opus20/opus20/webapp/views/plots.jinja2 @@ -6,6 +6,10 @@ var device_id = "{{ device_id }}"; +{% endblock %} + +{% block bottom_javascript %} + $(function(){ $('#fetchData').on('click', function(){ var $btn = $(this); diff --git a/opus20/scripts/opus20_cli b/opus20/scripts/opus20_cli index 3453eab..2b36ab8 100755 --- a/opus20/scripts/opus20_cli +++ b/opus20/scripts/opus20_cli @@ -9,7 +9,7 @@ import logging from opus20 import Opus20, OPUS20_CHANNEL_SPEC, PickleStore, Opus20ConnectionException clock = time.perf_counter -logger = logging.getLogger('opus_cli') +logger = logging.getLogger('opus20_cli') def extended_int(string): if string.startswith('0x'): @@ -19,20 +19,31 @@ def extended_int(string): def main(): - parser = argparse.ArgumentParser(description="CLI for the Lufft Opus20") + parser = argparse.ArgumentParser(description="CLI for the Lufft OPUS20") parser.add_argument('host', help='hostname of the device') - parser.add_argument('--port', '-p', type=int, help='port for TCP connections') - parser.add_argument('--timeout', '-t', type=float, help='timeout for the TCP connection') - parser.add_argument('--loglevel', '-l', choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], help='log level') - subparsers = parser.add_subparsers(help='cmd help', dest='cmd') - parser_a = subparsers.add_parser('list', help='list all possible channels') - parser_b = subparsers.add_parser('get', help='get the value(s) of specific channel(s)') - parser_b.add_argument('channel', type=extended_int, nargs='+', help='The selected channel(s)') - parser_c = subparsers.add_parser('download', help='download the logs') - parser_c.add_argument('persistance_file', help='file to store the logs in') + parser.add_argument('--port', '-p', type=int, help='TCP port of the OPUS20') + parser.add_argument('--timeout', '-t', type=float, help='Timeout of the TCP connection in seconds') + parser.add_argument('--loglevel', choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], help='Sets the verbosity of this script') + subparsers = parser.add_subparsers(title='commands', help='', dest='cmd') + parser_list = subparsers.add_parser('list', help='list all possible measurement channels') + parser_get = subparsers.add_parser('get', help='get the value(s) of specific channel(s)') + parser_get.add_argument('channel', type=extended_int, nargs='+', help='The selected channel(s)') + parser_download = subparsers.add_parser('download', help='download the logs and store them locally') + parser_download.add_argument('persistance_file', help='file to store the logs in') + parser_logging = subparsers.add_parser('logging', help='change or query global logging settings') + subsubparsers = parser_logging.add_subparsers(help='Action to perform w/ respect to logging', dest='action') + parser_logging_action_status = subsubparsers.add_parser('status', help='Query the current logging status of the device') + parser_logging_action_start = subsubparsers.add_parser('start', help='Start logging altogether on the device') + parser_logging_action_stop = subsubparsers.add_parser('stop', help='Stop logging altogether on the device') + parser_logging_action_clear = subsubparsers.add_parser('clear', help='Clear the log history on the device') + parser_enable = subparsers.add_parser('enable', help='enable logging for a specific channel') + parser_enable.add_argument('channel', type=extended_int, nargs='+', help='The selected channel(s)') + parser_disable = subparsers.add_parser('disable', help='disable logging for a specific channel') + parser_disable.add_argument('channel', type=extended_int, nargs='+', help='The selected channel(s)') args = parser.parse_args() if not args.cmd: parser.error('please select a command') + if args.cmd == 'logging' and not args.action: parser.error('please select a logging action') if args.loglevel: logging.basicConfig(level=getattr(logging, args.loglevel.upper())) @@ -63,6 +74,22 @@ def main(): log_data = o20.download_logs(start_datetime=max_ts) ps.add_data(o20.device_id, log_data) ps.persist() + if args.cmd == 'logging': + def logging_in_words(): return 'enabled' if o20.get_logging_state() else 'disabled' + if args.action == 'status': + print("Logging is currently " + logging_in_words() + ".") + elif args.action in ('start', 'stop'): + o20.set_logging_state(args.action == 'start') + logger.info("Logging is now " + logging_in_words() + ".") + elif args.action == 'clear': + o20.clear_log() + print('Clearing the log now. This will take a couple of minutes.') + print('You cannot make requests to the device during that time.') + o20.disconnect() + if args.cmd in ('enable', 'disable'): + enable = args.cmd == 'enable' + for channel in args.channel: + o20.set_channel_logging_state(enable) except Opus20ConnectionException as e: parser.error(str(e)) diff --git a/opus20/scripts/opus20_discovery b/opus20/scripts/opus20_discovery new file mode 100755 index 0000000..eee2973 --- /dev/null +++ b/opus20/scripts/opus20_discovery @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import sys +import time +import argparse +import logging +import functools + +from opus20 import Opus20, OPUS20_CHANNEL_SPEC, PickleStore, Opus20ConnectionException, discover_OPUS20_devices + +clock = time.perf_counter +logger = logging.getLogger('opus20_discovery') + +def main(): + + parser = argparse.ArgumentParser(description="Discovery of Lufft OPUS20 devices on the local network") + parser.add_argument('bind_address', default="", nargs="?", help='The IP to bind to') + parser.add_argument('--loglevel', '-l', default="WARNING", choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], help='log level') + args = parser.parse_args() + + if args.loglevel: + logging.basicConfig(level=getattr(logging, args.loglevel.upper())) + + found_devices = [] + def full_callback(found_devices, answer): + frm, host, answer_time = answer + dev_props = frm.kind.func() + dev_props['answer_time'] = answer_time + found_devices.append(dev_props) + callback = functools.partial(full_callback, found_devices) + + start = clock() + + print("\nTrying to find devices on interface {}...\n".format(args.bind_address)) + discover_OPUS20_devices(callback, bind_addr=args.bind_address) + for device in found_devices: + print("[{answer_time:.2f} ms] Device ID: {device_id}, IP: {ip}, Gateway: {gw}, Network: {net}".format(**device)) + print("\nFound a total number of {} devices.\n".format(len(found_devices))) + + end = clock() + logger.info("script running time (net): {:.6f} seconds.".format(end-start)) + + sys.exit(0 if found_devices else 1) + +if __name__ == "__main__": main() diff --git a/opus20/scripts/opus20_fakeserver b/opus20/scripts/opus20_fakeserver new file mode 100755 index 0000000..f456ef4 --- /dev/null +++ b/opus20/scripts/opus20_fakeserver @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import argparse +import logging +import pdb + +from opus20 import Opus20FakeServer + +logger = logging.getLogger('opus20_fakeserver') + +def main(): + + parser = argparse.ArgumentParser(description="Discovery of Lufft OPUS20 devices on the local network") + parser.add_argument('bind_address', default="", nargs="?", help='The IP to bind to') + parser.add_argument('--feed_logfile', help='A log file to feed the fake server with l2p frames') + parser.add_argument('--loglevel', '-l', default="INFO", choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'], help='log level') + args = parser.parse_args() + + if args.loglevel: + logging.basicConfig(level=getattr(logging, args.loglevel.upper())) + + fs = Opus20FakeServer(args.bind_address) + if args.feed_logfile: + logger.info("Feeding server with l2p example communication...".format(args.bind_address)) + fs.feed_with_communication_log(args.feed_logfile) + logger.info("Starting fake OPUS20 server on interface {}...".format(args.bind_address)) + fs.bind_and_serve() + logger.info("Fake OPUS20 server closed.") + +if __name__ == "__main__": main() diff --git a/opus20/scripts/opus20_web b/opus20/scripts/opus20_web index 2f7538e..cd0bc77 100755 --- a/opus20/scripts/opus20_web +++ b/opus20/scripts/opus20_web @@ -7,7 +7,7 @@ from opus20 import PlotWebServer import argparse import logging -logger = logging.getLogger('opus_web') +logger = logging.getLogger('opus20_web') def main(): diff --git a/opus20/setup.py b/opus20/setup.py index 069246b..6cf42c7 100644 --- a/opus20/setup.py +++ b/opus20/setup.py @@ -17,7 +17,12 @@ setup(name='opus20', url = '', license = 'GPL', packages = ['opus20', 'opus20.webapp'], - scripts = ['scripts/opus20_cli', 'scripts/opus20_web'], + scripts = [ + 'scripts/opus20_cli', + 'scripts/opus20_web', + 'scripts/opus20_discovery', + 'scripts/opus20_fakeserver', + ], include_package_data = True, zip_safe = True, platforms = 'any',