From 673b8c36864239d1ca9cbdcadbe139dee5627913 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Mon, 15 Jan 2018 10:09:19 +0100 Subject: [PATCH] Initial commit --- .gitattributes | 1 + .gitignore | 13 +++ AUTHORS.MD | 2 + LICENSE | 21 ++++ Pipfile | 12 +++ Pipfile.lock | 81 ++++++++++++++ README.md | 11 ++ app.py | 236 +++++++++++++++++++++++++++++++++++++++++ resources/identify.wav | 3 + sensor_readout.py | 158 +++++++++++++++++++++++++++ utils.py | 74 +++++++++++++ 11 files changed, 612 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 AUTHORS.MD create mode 100644 LICENSE create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100644 app.py create mode 100644 resources/identify.wav create mode 100644 sensor_readout.py create mode 100644 utils.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d899f65 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.wav filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..059e414 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +*.pyc +__pycache__ +Testing +*.cfg +server.info +server.cfg +wallaby_testing* +Binaries +Source +deleted*.db* +.fl0w +*.pickle diff --git a/AUTHORS.MD b/AUTHORS.MD new file mode 100644 index 0000000..46b196b --- /dev/null +++ b/AUTHORS.MD @@ -0,0 +1,2 @@ +**Authors:** +[Philip Trauner](http://github.com/PhilipTrauner) <[philip.trauner@arztpraxis.io](mailto:philip.trauner@arztpraxis.io)> diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..39c1275 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Philip Trauner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..1cd878b --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +"highway.py" = "==0.2.2" +Meh = "==1.2.1" +bottle = "==0.12.13" + +[dev-packages] \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..91df0ec --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,81 @@ +{ + "_meta": { + "hash": { + "sha256": "e3369dc6de17fcf7c41860f7590e3dd5c8e72f3dfce1f55a02ae96f079b01aa1" + }, + "host-environment-markers": { + "implementation_name": "cpython", + "implementation_version": "3.6.3", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "17.2.0", + "platform_system": "Darwin", + "platform_version": "Darwin Kernel Version 17.2.0: Fri Sep 29 18:27:05 PDT 2017; root:xnu-4570.20.62~3/RELEASE_X86_64", + "python_full_version": "3.6.3", + "python_version": "3.6", + "sys_platform": "darwin" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "bottle": { + "hashes": [ + "sha256:39b751aee0b167be8dffb63ca81b735bbf1dd0905b3bc42761efedee8f123355" + ], + "version": "==0.12.13" + }, + "highway.py": { + "hashes": [ + "sha256:0bf001ebaaed14590702dc90f18c6120957544ba0168a85a874023387125beea" + ], + "version": "==0.2.2" + }, + "meh": { + "hashes": [ + "sha256:c1dd9d92eda20a98f0fd91fe6889390ba5cf04107be8789c0368e178f11e8f43" + ], + "version": "==1.2.1" + }, + "ujson": { + "hashes": [ + "sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86" + ], + "version": "==1.35" + }, + "websockets": { + "hashes": [ + "sha256:e9c1cdbb591432c59d0b5ca64fd30b6d517024767f152fc169563b26e7bcc9da", + "sha256:85ae1e4b36aa2e90de56d211d2de36d7c093d00277a9afdd9b4f81e69c0214ab", + "sha256:2aa6d52264cecb08d39741e8fda49f5ac4872aef02617230c84d02e861f3cc5a", + "sha256:8a29100079f5b91a72bcd25d35a7354db985d3babae42d00b9d629f9a0aaa8ac", + "sha256:de743ef26b002efceea7d7756e99e5d38bf5d4f27563b8d27df2a9a5cc57340a", + "sha256:aa42ecef3aed807e23218c264b1e82004cdd131a6698a10b57fc3d8af8f651fc", + "sha256:c4c5b5ce2d66cb0cf193c14bc9726adca095febef0f7b2c04e5e3fa3487a97a4", + "sha256:e1e568136ad5cb6768504be36d470a136b072acbf3ea882303aee6361be01941", + "sha256:e8992f1db371f2a1c5af59e032d9dc7c1aa92f16241efcda695b7d955b4de0c2", + "sha256:3d38f76f71654268e5533b45df125ff208fee242a102d4b5ca958da5cf5fb345", + "sha256:4128212ab6f91afda03a0c697add261bdf6946b47928db83f07298ea2cd8d937", + "sha256:b19e7ede1ba80ee9de6f5b8ccd31beee25402e68bef7c13eeb0b8bc46bc4b7b7", + "sha256:7347af28fcc70eb45be409760c2a428f8199e7f73c04a621916c3c219ed7ad27", + "sha256:2f5b7f3920f29609086fb0b63552bb1f86a04b8cbdcc0dbf3775cc90d489dfc8", + "sha256:4a932c17cb11c361c286c04842dc2385cc7157019bbba8b64808acbc89a95584", + "sha256:5ddc5fc121eb76771e990f071071d9530e27d20e8cfb804d9f5823de055837af", + "sha256:a7e7585c8e3c0f9277ad7d6ee6ccddc69649cd216255d5e255d68f90482aeefa", + "sha256:3fcc7dfb365e81ff8206f950c86d1e73accdf3be2f9110c0cb73be32d2e7a9a5", + "sha256:09dfec40e9b73e8808c39ecdbc1733e33915a2b26b90c54566afc0af546a9ec3", + "sha256:43e5b9f51dd0000a4c6f646e2ade0c886bd14a784ffac08b9e079bd17a63bcc5" + ], + "version": "==3.4" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..a39b968 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +fl0w logo + +# c0re + +### Installation + +```bash +git clone https://github.com/robot0nfire/c0re.git --recursive --depth=1 +cd c0re +pip3 install -r requirements.txt +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000..183d087 --- /dev/null +++ b/app.py @@ -0,0 +1,236 @@ +from os.path import isfile, isdir +from threading import Lock +from re import compile as re_compile +from subprocess import check_output, CalledProcessError, Popen, PIPE, STDOUT +from _thread import start_new_thread +from time import sleep +from asyncio import get_event_loop, ensure_future + +# bottle (web-server) +from bottle import route, static_file +from bottle import run as run_bottle + +# highway +from highway import Server +from highway import Handler as Handler_ + +# highway utilities +from highway import log as logging +from highway import ConnectionClosed + +from meh import Config, Option, ExceptionInConfigError + +from sensor_readout import SensorReadout +from sensor_readout import REVERSE_NAMED_MODES +from sensor_readout import valid_port as valid_sensor_port + +from utils import play_sound, PlaybackFailure, valid_port + +CONFIG_PATH = "fl0w.cfg" + +INDEX_PATH = "dashb0ard" +STATIC_PATH = INDEX_PATH + "/static" + +config = Config() +config += Option("address", "127.0.0.1") +config += Option("ws_port", 3077, validator=valid_port) +config += Option("http_port", 8080, validator=valid_port) +config += Option("highway_debug", False) +config += Option("bottle_debug", False) +config += Option("fl0w_debug", False) +config += Option("identify_sound", "resources/identify.wav", validator=isfile) +config += Option("output_unbufferer", "stdbuf -o 0") + +try: + config = config.load(CONFIG_PATH) +except (IOError, ExceptionInConfigError): + config.dump(CONFIG_PATH) + config = config.load(CONFIG_PATH) + + +class Handler(Handler_): + def on_close(self, code, reason): + logging.info("Unsubscribing '%s:%i' from sensor readouts." % (*self.remote_address, )) + sensor_readout.handler_disconnected(self) + + +server = Server(Handler, debug=config.highway_debug) +sensor_readout = SensorReadout() + + +loop = get_event_loop() +ensure_future(sensor_readout.run()) + +""" +{"analog" : {1, 2, 3}, "digital" : {1, 2, 3}} +""" +async def process_sensor_request(data, handler, route, action): + successful = True + for mode in REVERSE_NAMED_MODES: + if mode in data: + for port in data[mode]: + if type(port) is int: + if valid_sensor_port(port, + REVERSE_NAMED_MODES[mode]): + + action(port, REVERSE_NAMED_MODES[mode], + handler) + else: + successful = False + break + else: + successful = False + break + await handler.send(successful, route) + + +@server.route("identify") +async def identify(data, handler): + successful = False + try: + play_sound(config.identify_sound) + successful = True + except PlaybackFailure: + pass + await handler.send(successful, "identify") + + +@server.route("sensor_poll_rate") +async def sensor_poll_rate(data, handler): + successful = False + if type(data) in (int, float) and data >= 0.1: + sensor_readout.poll_rate = data + successful = True + await handler.send(successful, "sensor_poll_rate") + + +@server.route("sensor_unsubscribe_all") +async def sensor_unsubscribe_all(data, handler): + sensor_readout.unsubscribe_all(handler) + + + +@server.route("sensor_subscribe") +async def sensor_subscribe(data, handler): + await process_sensor_request(data, handler, "sensor_subscribe", + sensor_readout.subscribe) + + +@server.route("sensor_unsubscribe") +async def sensor_unsubscribe(data, handler): + await process_sensor_request(data, handler, "sensor_unsubscribe", + sensor_readout.unsubscribe) + + +@server.route("shutdown") +async def shutdown(data, handler): + successful = False + try: + check_output(["shutdown", "now"]) + successful = True + except CalledProcessError: + pass + await handler.send(successful, "shutdown") + + +@server.route("reboot") +async def reboot(data, handler): + successful = False + try: + check_output(["reboot"]) + successful = True + except CalledProcessError: + pass + await handler.send(successful, "reboot") + + +@server.route("upgrade") +async def upgrade(data, handler): + await stream_program_output("apt-get update && apt-get upgrade", + "upgrade", handler) + + +@server.route("kill_botball") +async def kill_botball(data, handler): + await stream_program_output("killall botball_user_program", + "kill_botball", handler) + + +@server.route("reset_coproc") +async def reset_coproc(data, handler): + await stream_program_output("wallaby_reset_coproc", + "reset_coproc", handler) + + +@server.route("reset_coproc") +async def restart_x11(data, handler): + await stream_program_output("systemctl restart x11", + "restart_x11", handler) + + +@server.route("restart_harrogate") +async def restart_harrogate(data, handler): + await stream_program_output("systemctl restart harrogate", + "restart_harrogate", handler) + + +@server.route("restart_networking") +async def restart_networking(data, handler): + await stream_program_output("systemctl restart networking", + "restart_networking", handler) + + +async def stream_program_output(command, route, handler): + await handler.send("> %s\n" % command, route + "_output") + command = "%s %s" % (config.output_unbufferer, command) + program = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) + + # Stream output async + loop = get_event_loop() + ensure_future(_stream_program_output(program, route, handler)) + + +async def _stream_program_output(program, route, handler): + has_disconnected = False + # Poll process for new output until finished + for line in iter(program.stdout.readline, b""): + line = line.decode() + try: + await handler.send(line, route + "_output") + except ConnectionClosed: + has_disconnected = True + logging.warning(line.rstrip("\n")) + program.wait() + if not has_disconnected: + exit_code = program.returncode if type(program.returncode) is int else -1 + await handler.send(exit_code, route + "_exit") + + + +@route("/") +def index(): + return static_file("index.html", root="dashb0ard") + +@route("/static/") +def static(filepath): + return static_file(filepath, root="dashb0ard/static") + + +if isdir(STATIC_PATH) and isdir(INDEX_PATH): + start_new_thread(run_bottle, (), {"host" : config.address, + "port" : config.http_port, "quiet" : not config.bottle_debug}) + + logging.header("Serving dashb0ard on 'http://%s:%s'" % (config.address, + config.http_port)) + +else: + logging.error("dashb0ard not found.") + + +try: + logging.header("Starting fl0w on 'ws://%s:%s'" % (config.address, + config.ws_port)) + server.start(config.address, config.ws_port) +except KeyboardInterrupt: + # server.stop() + pass \ No newline at end of file diff --git a/resources/identify.wav b/resources/identify.wav new file mode 100644 index 0000000..8a444b1 --- /dev/null +++ b/resources/identify.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81832bc5b34359bc87173dc234cbb30ea23aa541814809ad16eb1ba9bd7335af +size 22858 diff --git a/sensor_readout.py b/sensor_readout.py new file mode 100644 index 0000000..aebbb9d --- /dev/null +++ b/sensor_readout.py @@ -0,0 +1,158 @@ +from random import randint +from threading import Lock +from _thread import start_new_thread +from os.path import exists +from asyncio import sleep + + +from utils import is_wallaby + +from highway import log as logging + + + +ANALOG = 1 +DIGITAL = 2 +NAMED_MODES = {ANALOG : "analog", DIGITAL : "digital"} +REVERSE_NAMED_MODES = {v : k for k, v in NAMED_MODES.items()} +MODES = tuple(NAMED_MODES.keys()) + +CONFIG_PATH = "fl0w.cfg" +IS_WALLABY = is_wallaby() +LIB_WALLABY = "/usr/lib/libwallaby.so" + +READOUT_TEMPLATE = { + ANALOG : [], + DIGITAL : [] +} + +def valid_port(port, mode): + if mode == ANALOG: + return port >= 0 and port <= 5 + elif mode == DIGITAL: + return port >= 0 and port <= 9 + return False + +class SensorReadout: + def __init__(self, poll_rate=0.2): + self.poll_rate = poll_rate + + self.handler_lock = Lock() + self.handlers = {} + + self.running = True + + self.readout_required = READOUT_TEMPLATE.copy() + + # Wallaby library avaliable? + if not exists(LIB_WALLABY): + logging.warning("Wallaby library unavaliable.") + self.get_sensor_value = self.__get_random_value + else: + self.wallaby_library = cdll.LoadLibrary(LIB_WALLABY) + self.get_sensor_value = self.__get_sensor_value + + + async def run(self): + while self.running: + current_values = { + ANALOG : {}, + DIGITAL : {} + } + + for mode in MODES: + for port in self.readout_required[mode]: + current_values[mode][port] = self.get_sensor_value(port, mode) + + self.handler_lock.acquire() + for handler in self.handlers: + readouts = 0 + + response = {ANALOG : {}, DIGITAL : {}} + + for mode in MODES: + for port in self.handlers[handler][mode]: + response[mode][port] = current_values[mode][port] + readouts += 1 + if readouts != 0: + await handler.send(response, "sensor_stream") + self.handler_lock.release() + + await sleep(self.poll_rate) + + + def stop(self): + self.running = False + + + def subscribe(self, port, mode, handler): + if port not in self.readout_required[mode]: + self.readout_required[mode].append(port) + if not handler in self.handlers: + self.handler_lock.acquire() + self.handlers[handler] = READOUT_TEMPLATE.copy() + self.handler_lock.release() + + self.handler_lock.acquire() + self.handlers[handler][mode].append(port) + self.handler_lock.release() + + + def unsubscribe(self, port, mode, handler): + if handler in self.handlers: + if port in self.handlers[handler][mode]: + self.handler_lock.acquire() + + del self.handlers[handler][mode][ \ + self.handlers[handler][mode].index(port)] + + self.handler_lock.release() + self.determine_required_readouts() + + + def determine_required_readouts(self): + readout_required = READOUT_TEMPLATE.copy() + + self.handler_lock.acquire() + for handler in self.handlers: + for mode in MODES: + for port in self.handlers[handler][mode]: + if not port in readout_required[mode]: + readout_required[mode].append(port) + self.handler_lock.release() + self.readout_required = readout_required + + + async def unsubscribe_all(self, handler): + successful = False + if handler in self.handlers: + self.handler_lock.acquire() + del self.handlers[handler] + self.handler_lock.release() + successful = True + self.determine_required_readouts() + await handler.send(successful, "sensor_unsubscribe") + + + def handler_disconnected(self, handler): + if handler in self.handlers: + self.handler_lock.acquire() + del self.handlers[handler] + self.handler_lock.release() + self.determine_required_readouts() + + + def __get_sensor_value(self, port, mode): + if mode == SensorReadout.ANALOG: + return self.wallaby_library.analog(port) + elif mode == SensorReadout.DIGITAL: + return self.wallaby_library.digital(port) + + + def __get_random_value(self, port, mode): + if mode == ANALOG: + return randint(0, 4095) + elif mode == DIGITAL: + return randint(0, 1) + + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..daa90f3 --- /dev/null +++ b/utils.py @@ -0,0 +1,74 @@ +from traceback import print_exception +from sys import exc_info, byteorder +import platform +import os +import struct +import subprocess +import urllib + +HIGHEST_PORT = 65535 + +class HostnameNotChangedError(PermissionError): + def __init__(self): + super(PermissionError, self).__init__("hostname could not be changed") + +class NotSupportedOnPlatform(OSError): + def __init__(self): + super(OSError, self).__init__("feature not avaliable on OS") + +class PlaybackFailure(OSError): + def __init__(self): + super(OSError, self).__init__("audio playback failed") + + +def is_wallaby(): + return "3.18.21-custom" in platform.uname().release + + +def is_linux(): + return platform.uname().system == "Linux" + + +def is_darwin(): + return platform.uname().system == "Darwin" + + +def is_windows(): + return platform.uname().system == "Windows" + + +def set_hostname(hostname): + if is_linux(): + if os.geteuid() == 0: + open("/etc/hostname", "w").write(hostname) + else: + raise HostnameNotChangedError() + elif is_darwin(): + if os.geteuid() == 0: + subprocess.check_call(["scutil", "--set", "HostName", hostname]) + else: + raise HostnameNotChangedError() + else: + raise HostnameNotChangedError() + + +def get_hostname(): + return platform.uname().node + + +def get_ip_from_url(url): + return urllib.parse.urlsplit(url).netloc.split(':')[0] + + +def play_sound(path): + if is_linux() or is_darwin(): + try: + subprocess.check_call(["aplay" if is_linux() else "afplay", path]) + except subprocess.CalledProcessError as e: + raise PlaybackFailure() + else: + raise NotSupportedOnPlatform() + + +def valid_port(port): + return type(port) is int and port <= HIGHEST_PORT \ No newline at end of file