# HG changeset patch # User matthewreed # Date 2017-07-11 20:12:50 # Node ID 25926382c27b6f5d8f8771cf7d4d8c3168e38bfd # Parent 72dd61ca5365148ce7b0a507e8e8c1682162abf1 Added modules for database connection, scheduler, and comms protocol definition diff --git a/database.py b/database.py new file mode 100644 --- /dev/null +++ b/database.py @@ -0,0 +1,59 @@ +from influxdb import InfluxDBClient +from influxdb import SeriesHelper +import configparser +import logging +import protocol + +class MySeriesHelper(SeriesHelper): + + # Meta class stores time series helper configuration. + class Meta: + # The client should be an instance of InfluxDBClient. + #client = myclient + # The series name must be a string. Add dependent fields/tags in curly brackets. + series_name = '{measurement}' + # Defines all the fields in this time series. + fields = ['value'] + # Defines all the tags for the series. + tags = ['measurement'] + # Defines the number of data points to store prior to writing on the wire. + bulk_size = 5 + # autocommit must be set to True when using bulk_size + autocommit = True + +class Database: + + def __init__(self, config): + self.logger = logging.getLogger('hydrobot') + host = config.get("database", "host") + port = config.get("database", "port") + username = config.get("database", "username") + password = config.get("database", "password") + database = config.get("database", "database") + self.name = config.get("system", "name") + + try: + self.client = InfluxDBClient(host, port, username, password, database) + MySeriesHelper.Meta.client = self.client + MySeriesHelper.Meta.series_name = self.name + '.{measurement}' + self.logger.info("Connected to database") + except: + self.logger.error("Could not connect to database") + + # To manually submit data points which are not yet written, call commit: + #MySeriesHelper.commit() + + def log_message(self, name, message): + data_name = protocol.lookup_data_name_by_key(message.data_key) + if not data_name == None: + if message.sensor_num > 0: + num = "_" + str(message.sensor_num) + else: + num = "" + try: + self.logger.debug("Database log: " + name + "." + data_name + num) + MySeriesHelper(measurement=(name + "." + data_name + num), value=(float)(message.data)) + except: + self.logger.error("Database issue!") + else: + self.logger.warning("Unknown data key: " + str(message)) diff --git a/hydrobot.py b/hydrobot.py --- a/hydrobot.py +++ b/hydrobot.py @@ -1,39 +1,30 @@ #!/usr/bin/env python import sys -import os import time -import _thread -import ast +import signal import configparser -import datetime -from canard import can, messaging -from canard.hw import socketcan -from canard.file import jsondb -from canard.utils import queue -from influxdb import InfluxDBClient -from influxdb import SeriesHelper -from apscheduler.schedulers.background import BackgroundScheduler -import PID -from pytz import timezone import logging -import signal import network import network_interface from message import HydroBotMessage import module +import protocol +from database import Database +from scheduler import Scheduler + #TODO -#fix temperature offsets #add periodic output refresh #add output cleanup on shutdown +#validate config settings on load -# load config file +#load config file config = configparser.ConfigParser(allow_no_value = True) config.read("hydrobot.conf") -# set up logger +#set up logger logger = logging.getLogger('hydrobot') log_level = config.get("system", "log_level") levels = {"CRITICAL" : 50, "ERROR" : 40, "WARNING" : 30, "INFO" : 20, "DEBUG" : 10, "NOTSET" : 0} @@ -41,268 +32,42 @@ logger.setLevel(levels[log_level]) fh = logging.FileHandler('hydrobot.log') fh.setLevel(logging.INFO) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(module)s][%(funcName)s] %(message)s') #('%(asctime)s - %(name)s - %(levelname)s - %(message)s') fh.setFormatter(formatter) logger.addHandler(fh) - - -logger.info("Starting HydroBot!") - -class MySeriesHelper(SeriesHelper): - - # Meta class stores time series helper configuration. - class Meta: - # The client should be an instance of InfluxDBClient. - #client = myclient - # The series name must be a string. Add dependent fields/tags in curly brackets. - series_name = '{measurement}' - # Defines all the fields in this time series. - fields = ['value'] - # Defines all the tags for the series. - tags = ['measurement'] - # Defines the number of data points to store prior to writing on the wire. - bulk_size = 5 - # autocommit must be set to True when using bulk_size - autocommit = True - - -class Database: - - def __init__(self): - host = config.get("database", "host") - port = config.get("database", "port") - username = config.get("database", "username") - password = config.get("database", "password") - database = config.get("database", "database") - self.name = config.get("system", "name") - try: - self.client = InfluxDBClient(host, port, username, password, database) - MySeriesHelper.Meta.client = self.client - MySeriesHelper.Meta.series_name = self.name + '.{measurement}' - except: - logger.error("Could not connect to database") - - # To manually submit data points which are not yet written, call commit: - #MySeriesHelper.commit() - - def log_data(self, msgdb, message): - try: - if message == msgdb.AirSense: - MySeriesHelper(measurement='air_temp', value=(float)(message.Temperature.value)) - MySeriesHelper(measurement='air_humidity', value=(float)(message.Humidity.value)) - MySeriesHelper(measurement='air_pressure', value=(float)(message.Pressure.value)) - if message == msgdb.RelayDriveIn: - MySeriesHelper(measurement='water_flow_rate', value=(float)(message.FlowRate.value)) - MySeriesHelper(measurement='input_1', value=(float)(message.Input1.value)) - MySeriesHelper(measurement='input_2', value=(float)(message.Input2.value)) - MySeriesHelper(measurement='input_3', value=(float)(message.Input3.value)) - MySeriesHelper(measurement='input_4', value=(float)(message.Input4.value)) - if message == msgdb.WaterSense: - MySeriesHelper(measurement='water_level', value=(float)(message.PercentFull.value)) - MySeriesHelper(measurement='water_temp', value=(float)(message.Temperature.value)) - except: - logger.error("Could not connect to database") - -class CanBus: - - def __init__(self, database, interface): - - # Bring up CAN interface (maybe do this in a systemd service file) - # Passing random arguments to sudo is super dangerous - os.system("sudo ip link set " + interface + " up type can bitrate 500000") - - self.database = database - self.dev = socketcan.SocketCanDev(interface) - self.queue = queue.CanQueue(self.dev) - - parser = jsondb.JsonDbParser() - self.msgdb = parser.parse('hydrobot_can.json') - - self.temp_msg = self.msgdb.AirSense - self.relay_msg = self.msgdb.RelayDriveIn - self.relay_send_msg = self.msgdb.RelayDriveOut - - def start(self): - self.queue.start() - _thread.start_new_thread(self.process_can, ()) +ch = logging.StreamHandler(sys.stdout) +ch.setLevel(logging.DEBUG) +formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(module)s][%(funcName)s] %(message)s') #('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) - def process_can(self): - while True: - frame = self.queue.recv() - if frame != None: - message = self.msgdb.decode(frame) - if message: - logger.debug("Received CAN message! ID: " + hex(message.id)) - self.database.log_data(self.msgdb, message) - - def send_can(self): - self.relay_send_msg.Nothing.value = 0 - if self.relay_send_msg.Output1.value == 0: - self.relay_send_msg.Output1.value = 1 - else: - self.relay_send_msg.Output1.value = 0 - self.relay_send_msg.Output2.value = 1 - self.relay_send_msg.Output3.value = 1 - self.relay_send_msg.Output4.value = 1 - logger.debug("Send CAN message! ID: " + hex(self.relay_send_msg.id) + " Data: " + str(self.relay_send_msg.data)) - self.queue.send(self.relay_send_msg.encode()) - - def set_output(self, module, output, state): - logger.info("Output! " + module + " " + output + " " + str(state)) - msg = self.msgdb.lookup_message(module) - msg.lookup_signal(output).value = state - self.queue.send(msg.encode()) - - if msg.lookup_signal(output) == msg.Output1: - try: - MySeriesHelper(measurement='output_1', value=state) - except: - logger.error("Could not connect to database") - if msg.lookup_signal(output) == msg.Output2: - try: - MySeriesHelper(measurement='output_2', value=state) - except: - logger.error("Could not connect to database") - if msg.lookup_signal(output) == msg.Output3: - try: - MySeriesHelper(measurement='output_3', value=state) - except: - logger.error("Could not connect to database") - if msg.lookup_signal(output) == msg.Output4: - try: - MySeriesHelper(measurement='output_4', value=state) - except: - logger.error("Could not connect to database") - - -class Scheduler: - - def __init__(self, canbus): - self.canbus = canbus - self.apscheduler = BackgroundScheduler() - self.apscheduler.configure(timezone=timezone('US/Eastern')) - - def start(self): - self.apscheduler.start() - - for section in config.sections(): - if "timer" in section: - items = config.items(section) - for item in items: - if item[0] == 'trigger': - trigger = item[1] - if item[0] == 'module': - module = item[1] - if item[0] == 'output': - output = item[1] - if item[0] == 'on_time': - on_time = ast.literal_eval(item[1]) - if item[0] == 'off_time': - off_time = ast.literal_eval(item[1]) - if item[0] == 'on_duration': - on_duration = ast.literal_eval(item[1]) - if item[0] == 'off_duration': - off_duration = ast.literal_eval(item[1]) - if trigger == 'cron': - on_job = self.apscheduler.add_job(self.canbus.set_output, trigger, [module, output, 1], day = on_time[0], day_of_week = on_time[1], hour = on_time[2], minute = on_time[3], second = on_time[4]) - off_job = self.apscheduler.add_job(self.canbus.set_output, trigger, [module, output, 0], day = off_time[0], day_of_week = off_time[1], hour = off_time[2], minute = off_time[3], second = off_time[4]) - - #evalute the current state of the cron trigger and set output on if needed - if on_job.next_run_time > off_job.next_run_time: - self.canbus.set_output(module, output, 1) - else: - self.canbus.set_output(module, output, 0) - - if trigger == 'interval': - self.apscheduler.add_job(self.interval_off, trigger, [module, output, section + "_off", section + "_on", on_duration], id = section + "_off", weeks = off_duration[0], days = off_duration[1], hours = off_duration[2], minutes = off_duration[3], seconds = off_duration[4]) - timer = self.apscheduler.add_job(self.interval_on, trigger, [module, output, section + "_off", section + "_on", off_duration], id = section + "_on", weeks = on_duration[0], days = on_duration[1], hours = on_duration[2], minutes = on_duration[3], seconds = on_duration[4]) - timer.pause() - self.interval_off(module, output, section + "_off", section + "_on", on_duration) - - if "pid" in section: - items = config.items(section) - for item in items: - if item[0] == 'module_out': - module_out = item[1] - if item[0] == 'output': - signal_out = item[1] - if item[0] == 'module_in': - module_in = item[1] - if item[0] == 'input': - signal_in = item[1] - if item[0] == 'kp': - kp = item[1] - if item[0] == 'ki': - ki = item[1] - if item[0] == 'kd': - kd = item[1] - if item[0] == 'rate': - rate = item[1] - if item[0] == 'setpoint': - setpoint = item[1] - - pid = PID.PID(float(setpoint)) - pid.SetKp(kp) - pid.SetKi(ki) - pid.SetKd(kd) - pid.Reset() - self.apscheduler.add_job(self.process_PID, "interval", [pid, module_out, signal_out, module_in, signal_in], seconds = int(rate)) - - - def interval_off(self, module, output, timer_off, timer_on, on_duration): - self.canbus.set_output(module, output, 1) - self.apscheduler.get_job(timer_on).reschedule("interval", weeks = on_duration[0], days = on_duration[1], hours = on_duration[2], minutes = on_duration[3], seconds = on_duration[4]) - self.apscheduler.get_job(timer_on).resume() - self.apscheduler.get_job(timer_off).pause() - logger.info("Turning " + timer_on + "!") - - def interval_on(self, module, output, timer_off, timer_on, off_duration): - self.canbus.set_output(module, output, 0) - self.apscheduler.get_job(timer_off).reschedule("interval", weeks = off_duration[0], days = off_duration[1], hours = off_duration[2], minutes = off_duration[3], seconds = off_duration[4]) - self.apscheduler.get_job(timer_off).resume() - self.apscheduler.get_job(timer_on).pause() - logger.info("Turning " + timer_off + "!") - - def process_PID(self, pid, module_out, signal_out, module_in, signal_in): - msg = self.canbus.msgdb.lookup_message(module_in) - input_reading = msg.lookup_signal(signal_in).value - input_reading = 19 - pid_out = pid.Update(input_reading) - if pid_out > 0: - self.canbus.set_output(module_out, signal_out, 1) - run_time = datetime.datetime.now() + datetime.timedelta(milliseconds=pid_out) - self.apscheduler.add_job(self.canbus.set_output, "date", run_date=run_time, args=[module_out, signal_out, 0]) - -def func(): - print(1) - +logger.info('Logger initialized') def main(argv): + + logger.info("Starting HydroBot!") if len(argv) < 1: - print("Error: please specify a can interface") + logger.error("Please specify a can interface") return 1 - database = Database() - + database = Database(config) - module_network = network.Network(logger) - module_network.add_interface(network_interface.CanBusNetworkInterface(module_network, argv[0], logger)) + module_network = network.Network(database, config) module_network.start_all_interfaces() - message = HydroBotMessage(0x0001, 0x81, 0x0007, 0x0001, 0x0111) - module_network.send_message(message) - + scheduler = Scheduler(module_network, config) + scheduler.start() - #canbus = CanBus(database, argv[0]) - #canbus.start() - - #scheduler = Scheduler(canbus) - #scheduler.start() + #module = module_network.module_list.lookup_module_by_name("RelayDrive1") + #message = HydroBotMessage(module.uuid, (0x80 | protocol.lookup_command_key_by_name("config")), protocol.lookup_data_key_by_name("can_id"), 0, 0x300) + #module.send_message(message) + #message = HydroBotMessage(module.uuid, (0x80 | protocol.lookup_command_key_by_name("config")), protocol.lookup_data_key_by_name("data_rate"), 0, 10) + #module.send_message(message) while True: - time.sleep(0.001) + time.sleep(0.1) def close_program(signum, frame): logger.info("Closing now!") @@ -311,4 +76,4 @@ def close_program(signum, frame): signal.signal(signal.SIGINT, close_program) if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) + main(sys.argv[1:]) diff --git a/hydrobot_def.json b/hydrobot_def.json new file mode 100644 --- /dev/null +++ b/hydrobot_def.json @@ -0,0 +1,263 @@ +{ + "devices" : [ + { + "id" : "0x00", + "name" : "master", + "display" : "Master", + "class" : "None" + }, + { + "id" : "0x01", + "name" : "airsense", + "display" : "AirSense", + "class" : "AirSenseModule" + }, + { + "id" : "0x02", + "name" : "relaydrive", + "display" : "RelayDrive", + "class" : "RelayDriveModule", + "io" : [ + { + "name" : "input_1", + "type" : "input", + "sensor_num" : "1", + "functions" : [ + "digital_in", + "freq_in" + ] + }, + { + "name" : "input_2", + "type" : "input", + "sensor_num" : "2", + "functions" : [ + "digital_in", + "freq_in" + ] + }, + { + "name" : "input_3", + "type" : "input", + "sensor_num" : "3", + "functions" : [ + "digital_in", + "freq_in" + ] + }, + { + "name" : "input_4", + "type" : "input", + "sensor_num" : "4", + "functions" : [ + "digital_in", + "freq_in" + ] + }, + { + "name" : "input_5", + "type" : "input", + "sensor_num" : "5", + "functions" : [ + "digital_in", + "freq_in" + ] + }, + { + "name" : "input_6", + "type" : "input", + "sensor_num" : "6", + "functions" : [ + "digital_in", + "freq_in" + ] + }, + { + "name" : "input_7", + "type" : "input", + "sensor_num" : "7", + "functions" : [ + "digital_in", + "freq_in" + ] + }, + { + "name" : "input_8", + "type" : "input", + "sensor_num" : "8", + "functions" : [ + "digital_in", + "freq_in" + ] + } + ] + }, + { + "id" : "0x03", + "name" : "watersense", + "display" : "WaterSense", + "class" : "WaterSenseModule" + }, + { + "id" : "0x04", + "name" : "protomodule", + "display" : "ProtoModule", + "class" : "ProtoModule" + } + ], + "data_keys" : [ + { + "key" : "0x0001", + "name" : "digital_in", + "display" : "Digital Input", + "units" : "bool" + }, + { + "key" : "0x0002", + "name" : "freq_in", + "display" : "Frequency Input", + "units" : "Hz" + }, + { + "key" : "0x0003", + "name" : "analog_in", + "display" : "Analog Input", + "units" : "V" + }, + { + "key" : "0x0004", + "name" : "digital_out", + "display" : "Digital Output", + "units" : "bool" + }, + { + "key" : "0x0005", + "name" : "pwm_out", + "display" : "PWM Output", + "units" : "%Duty" + }, + { + "key" : "0x0006", + "name" : "analog_out", + "display" : "Analog Output", + "units" : "V" + }, + { + "key" : "0x0007", + "name" : "air_temp", + "display" : "Air Temp", + "units" : "degC" + }, + { + "key" : "0x0008", + "name" : "air_humidity", + "display" : "Air Humidity", + "units" : "%RH" + }, + { + "key" : "0x0009", + "name" : "air_pressure", + "display" : "Air Pressure", + "units" : "hPa" + }, + { + "key" : "0x000A", + "name" : "ambient_light", + "display" : "Ambient Light", + "units" : "lux" + }, + { + "key" : "0x000B", + "name" : "water_temp", + "display" : "Water Temp", + "units" : "degC" + }, + { + "key" : "0x000C", + "name" : "water_level", + "display" : "Water Level", + "units" : "%Full" + }, + { + "key" : "0x000D", + "name" : "ec", + "display" : "Conductivity", + "units" : "uS" + }, + { + "key" : "0x000E", + "name" : "ph", + "display" : "pH", + "units" : "pH" + }, + { + "key" : "0x0100", + "name" : "can_id", + "display" : "CAN ID", + "units" : "hex" + }, + { + "key" : "0x0101", + "name" : "data_rate", + "display" : "Data Rate", + "units" : "seconds" + }, + { + "key" : "0x0102", + "name" : "led_brightness", + "display" : "LED Brightness", + "units" : "%" + }, + { + "key" : "0x0103", + "name" : "input", + "display" : "Input", + "units" : "" + }, + { + "key" : "0x0104", + "name" : "output", + "display" : "Output", + "units" : "" + } + ], + + "command_keys" : [ + { + "key" : "0x00", + "name" : "e_stop", + "display" : "E-Stop", + "units" : "bool" + }, + { + "key" : "0x01", + "name" : "silence_bus", + "display" : "Silence Bus", + "units" : "bool" + }, + { + "key" : "0x10", + "name" : "set_output", + "display" : "Set Output", + "units" : "Data Key" + }, + { + "key" : "0x11", + "name" : "get_data", + "display" : "Get Data", + "units" : "Data Key" + }, + { + "key" : "0x12", + "name" : "config", + "display" : "Config", + "units" : "Data Key" + }, + { + "key" : "0x13", + "name" : "calibrate", + "display" : "Calibrate", + "units" : "Data Key" + } + ] +} diff --git a/message.py b/message.py --- a/message.py +++ b/message.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python class HydroBotMessage: diff --git a/module.py b/module.py --- a/module.py +++ b/module.py @@ -1,10 +1,36 @@ from abc import ABCMeta, abstractmethod import uuid +import logging +import protocol +from message import HydroBotMessage class ModuleList(): - def __init__(self): + def __init__(self, network, database, config): + self.network = network + self.database = database + self.config = config + self._modules = [] + self.load_modules() + + def load_modules(self): + + for section in self.config.sections(): + if "module" in section: + module_name = self.config.get(section, "name") + module_type = self.config.get(section, "type") + module_address = int(self.config.get(section, "address"), 16) + module_interface = self.config.get(section, "interface") + network_interface = self.network.get_interface(module_interface) + + module_id = protocol.lookup_device_id_by_name(module_type) + new_module = self.new_module(module_id, module_address, network_interface, module_name) + network_interface.address_lookup[module_address] = new_module.uuid + network_interface.uuid_lookup[new_module.uuid] = module_address + + new_module.config("data_rate", int(self.config.get(section, "data_rate"))) + new_module.config("led_brightness", int(self.config.get(section, "led_brightness"))) def add_module(self, module): assert isinstance(module, Module), 'invalid module' @@ -25,68 +51,133 @@ class ModuleList(): if module.uuid == uuid: return module - def new_module(self, device_type, address, interface): + def lookup_module_by_name(self, name): + for module in self._modules: + if module.name == name: + return module + + def new_module(self, device_id, address, interface, name = None): + + if name == None or self.lookup_module_by_name(name) != None: + name = self.auto_assign_name(device_id) - if device_type == 0x01: - module = AirSenseModule(device_type, address, interface) - elif device_type == 0x02: - module = RelayDriveModule(device_type, address, interface) - else: - module = UnknownModule(device_type, address, interface) + device_class = eval(protocol.lookup_device_class_by_id(device_id)) + module = device_class(address, interface, name, self.database) + + #if device_type == "airsense": + #module = AirSenseModule(address, interface, name, self.database) + #elif device_type == "relaydrive": + #module = RelayDriveModule(address, interface, name, self.database) + #else: + #module = UnknownModule(address, interface, name, self.database) self.add_module(module) return module + + def auto_assign_name(self, device_id): + keep_going = True + num = 1 + device_name = protocol.lookup_device_display_by_id(device_id) + while keep_going: + name_taken = False + for module in self._modules: + if module.name == device_name + str(num): + name_taken = True + break + if not name_taken: + return device_name + str(num) + else: + num = num + 1 + class Module(metaclass=ABCMeta): - def __init__(self, device_type, address, interface): - self.device_type = device_type + def __init__(self, address, interface, name, database): + self.logger = logging.getLogger('hydrobot') self.address = address self.interface = interface + self.name = name + self.database = database self.uuid = uuid.uuid1() - print("Created new module! " + str(self.uuid)) + self.logger.info("Created new module! " + self.name + " " + str(self.uuid)) @abstractmethod - def send_message(self): + def send_message(self, message): pass @abstractmethod - def receive_message(self): + def receive_message(self, message): + pass + + @abstractmethod + def update(self): pass + + @abstractmethod + def config(self): + pass + class AirSenseModule(Module): - def __init__(self, device_type, address, interface): - super(AirSenseModule, self).__init__(device_type, address, interface) + def __init__(self, address, interface, name, database): + super(AirSenseModule, self).__init__(address, interface, name, database) - def send_message(self): - message = None + def send_message(self, message): self.interface.network.send_message(message) - def receive_message(self): - print("AirSenseModule receive message!") + def receive_message(self, message): + self.logger.debug("Receive message! From: " + self.name) + self.database.log_message(self.name, message) + + def update(self): + pass + + def config(self, data_key, value): + message = HydroBotMessage(self.uuid, (0x80 | protocol.lookup_command_key_by_name("config")), protocol.lookup_data_key_by_name(data_key), 0, value) + self.interface.send_message(message) class RelayDriveModule(Module): - def __init__(self, device_type, address, interface): - super(RelayDriveModule, self).__init__(device_type, address, interface) + def __init__(self, address, interface, name, database): + super(RelayDriveModule, self).__init__(address, interface, name, database) + + + def send_message(self, message): + self.interface.network.send_message(message) - def send_message(self): + def receive_message(self, message): + self.logger.debug("Receive message! From: " + self.name) + self.database.log_message(self.name, message) + + def update(self): pass + + def config(self, data_key, value): + message = HydroBotMessage(self.uuid, (0x80 | protocol.lookup_command_key_by_name("config")), protocol.lookup_data_key_by_name(data_key), 0, value) + self.interface.send_message(message) - def receive_message(self): - print("RelayDriveModule receive message!") + def set_output(self, output, value): + message = HydroBotMessage(self.uuid, (0x80 | protocol.lookup_command_key_by_name("set_output")), protocol.lookup_data_key_by_name("digital_out"), int(output), value) + self.send_message(message) class UnknownModule(Module): - def __init__(self, device_type, address, interface): - super(UnknownModule, self).__init__(device_type, address, interface) + def __init__(self, address, interface, name, database): + super(UnknownModule, self).__init__(address, interface, name, database) - def send_message(self): + def send_message(self, message): pass - def receive_message(self): - print("UnknownModule receive message!") + def receive_message(self, message): + self.logger.debug("Receive message! From: " + self.name) + def update(self): + pass + + def config(self, data_key, value): + message = HydroBotMessage(self.uuid, (0x80 | protocol.lookup_command_key_by_name("config")), protocol.lookup_data_key_by_name(data_key), 0, value) + self.interface.send_message(message) + diff --git a/network.py b/network.py --- a/network.py +++ b/network.py @@ -1,21 +1,35 @@ -#!/usr/bin/env python - import network_interface import module +import logging class Network(): - def __init__(self, logger): + def __init__(self, database, config): + self.logger = logging.getLogger('hydrobot') + self.database = database + self.config = config + self.interfaces = [] - self.module_list = module.ModuleList() - self.logger = logger + self.load_interfaces() + self.module_list = module.ModuleList(self, database, config) def add_interface(self, interface): self.interfaces.append(interface) def load_interfaces(self): - #TODO: Load interfaces from config file - pass + #load interfaces from config file + for section in self.config.sections(): + if "interface" in section: + interface = None + interface_type = self.config.get(section, "type") + interface_name = self.config.get(section, "name") + if interface_type == "CAN": + interface = network_interface.CanBusNetworkInterface(self, interface_name) + self.add_interface(interface) + if interface_type == "WIFI": + interface = network_interface.CanBusNetworkInterface(self, interface_name) + self.add_interface(interface) + def get_interface(self, interface_name): for interface in self.interfaces: @@ -23,14 +37,13 @@ class Network(): return interface def start_interface(self, interface_name): - print("Network: start interface " + interface_name) - #TODO: Start interface + self.logger.info("Network: start interface " + interface_name) for interface in self.interfaces: if interface.interface_name == interface_name: interface.start() def start_all_interfaces(self): - print("Network: start all interfaces") + self.logger.info("Network: start all interfaces") for interface in self.interfaces: interface.start() @@ -39,9 +52,9 @@ class Network(): if not module == None: module.interface.send_message(message) else: - print("Could not find module " + str(message.module_uuid)) + self.logger.warning("Could not find module " + str(message.module_uuid)) def process_message(self, message): - print("Network: process message") - self.module_list.lookup_module(message.module_uuid).receive_message() + self.logger.debug("Network: process message") + self.module_list.lookup_module(message.module_uuid).receive_message(message) diff --git a/network_interface.py b/network_interface.py --- a/network_interface.py +++ b/network_interface.py @@ -1,12 +1,12 @@ -#!/usr/bin/env python - import os import struct import _thread from abc import ABCMeta, abstractmethod +import logging from message import HydroBotMessage import module +import protocol from canard import can from canard.hw import socketcan @@ -14,27 +14,27 @@ from canard.utils import queue class NetworkInterface(metaclass=ABCMeta): - def __init__(self, network, logger): + def __init__(self, network): + self.logger = logging.getLogger('hydrobot') self.network = network - self.logger = logger @abstractmethod def start(self): - print("Network: start") + self.logger.info("Network: start") @abstractmethod def send_message(self, message): pass def process_message(self, message): - print("Network Interface: process message") + self.logger.debug("Network Interface: process message") class CanBusNetworkInterface(NetworkInterface): - def __init__(self, network, interface_name, logger): - super(CanBusNetworkInterface, self).__init__(network, logger) + def __init__(self, network, interface_name): + super(CanBusNetworkInterface, self).__init__(network) # Bring up CAN interface (maybe do this in a systemd service file) # Passing random arguments to sudo is super dangerous @@ -44,12 +44,13 @@ class CanBusNetworkInterface(NetworkInte self.dev = socketcan.SocketCanDev(interface_name) self.queue = queue.CanQueue(self.dev) + #bidirectional address <==> uuid lookup self.address_lookup = {} self.uuid_lookup = {} def start(self): super(CanBusNetworkInterface, self).start() - print("CanBusNetworkInterface: start") + self.logger.info("CanBusNetworkInterface: start") self.queue.start() _thread.start_new_thread(self.process_message, ()) @@ -63,23 +64,30 @@ class CanBusNetworkInterface(NetworkInte while True: frame = self.queue.recv() if frame != None: - print("CanBusNetworkInterface: process message") + + base_id = frame.id & 0x7fe - if not frame.id in self.address_lookup: - new_module = self.network.module_list.new_module(frame.data[0] & 0x7f, frame.id, self) - self.address_lookup[frame.id] = new_module.uuid - self.uuid_lookup[new_module.uuid] = frame.id + if not base_id in self.address_lookup: + self.logger.debug("Uknown module address: " + hex(base_id)) + device_id = frame.data[0] & 0x7f + new_module = self.network.module_list.new_module(device_id, base_id, self) + self.address_lookup[base_id] = new_module.uuid + self.uuid_lookup[new_module.uuid] = base_id self.logger.debug("Received CAN message! ID: " + hex(frame.id)) - message = HydroBotMessage(self.address_lookup[frame.id], frame.data[0], ((frame.data[1] << 8) + frame.data[2]), frame.data[3], ((frame.data[4] << 24) + (frame.data[5] << 16) + (frame.data[6] << 8) + frame.data[7])) - print(message) + + data = [frame.data[7], frame.data[6], frame.data[5], frame.data[4]] + b = struct.pack('4B', *data) + f_data = struct.unpack('>f', b)[0] + message = HydroBotMessage(self.address_lookup[base_id], frame.data[0], ((frame.data[1] << 8) + frame.data[2]), frame.data[3], f_data) + self.logger.debug("Process message: " + str(message)) self.network.process_message(message) class WifiNetworkInterface(NetworkInterface): - def __init__(self, network, logger): - super(WifiNetworkInterface, self).__init__(network, logger) + def __init__(self, network): + super(WifiNetworkInterface, self).__init__(network) def start(self): pass @@ -90,7 +98,3 @@ class WifiNetworkInterface(NetworkInterf def process_message(self, message): pass - - -#module knows how to send and recieve messages - diff --git a/protocol.py b/protocol.py new file mode 100644 --- /dev/null +++ b/protocol.py @@ -0,0 +1,73 @@ +import json + +_protocol_def = None + +with open('hydrobot_def.json') as def_file: + _protocol_def = json.load(def_file) + +def lookup_device_name_by_id(id): + devices = _protocol_def['devices'] + for device in devices: + if device.get('id') == "{0:#0{1}x}".format(id, 4): + return device.get('name') + +def lookup_device_id_by_name(name): + devices = _protocol_def['devices'] + for device in devices: + if device.get('name') == name: + return int(device.get('id'), 16) + +def lookup_device_display_by_id(id): + devices = _protocol_def['devices'] + for device in devices: + if device.get('id') == "{0:#0{1}x}".format(id, 4): + return device.get('display') + +def lookup_device_class_by_id(id): + devices = _protocol_def['devices'] + for device in devices: + if device.get('id') == "{0:#0{1}x}".format(id, 4): + return device.get('class') + +def lookup_device_display_by_name(name): + devices = _protocol_def['devices'] + for device in devices: + if device.get('name') == name: + return device.get('display') + +def lookup_data_name_by_key(key): + data_keys = _protocol_def['data_keys'] + for data_key in data_keys: + if data_key.get('key') == "{0:#0{1}x}".format(key, 6): + return data_key.get('name') + +def lookup_data_key_by_name(name): + data_keys = _protocol_def['data_keys'] + for data_key in data_keys: + if data_key.get('name') == name: + return int(data_key.get('key'), 16) + +def lookup_data_display_by_key(key): + data_keys = _protocol_def['data_keys'] + for data_key in data_keys: + if data_key.get('key') == "{0:#0{1}x}".format(key, 6): + return data_key.get('display') + +def lookup_command_name_by_key(key): + commands = _protocol_def['command_keys'] + for command in commands: + if command.get('key') == "{0:#0{1}x}".format(key, 6): + return command.get('name') + +def lookup_command_key_by_name(name): + commands = _protocol_def['command_keys'] + for command in commands: + if command.get('name') == name: + return int(command.get('key'), 16) + +def lookup_command_display_by_key(key): + commands = _protocol_def['command_keys'] + for command in commands: + if command.get('key') == "{0:#0{1}x}".format(key, 6): + return command.get('display') + diff --git a/scheduler.py b/scheduler.py new file mode 100644 --- /dev/null +++ b/scheduler.py @@ -0,0 +1,81 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from pytz import timezone +import ast +import datetime +import logging + +class Scheduler: + + def __init__(self, network, config): + self.logger = logging.getLogger('hydrobot') + self.network = network + self.config = config + + self.apscheduler = BackgroundScheduler() + self.apscheduler.configure(timezone=timezone(config.get("system", "timezone"))) + + def start(self): + self.apscheduler.start() + self.load_schedules() + + def interval_on(self, module, output, timer_off, timer_on, on_duration): + module.set_output(output, 1) + self.apscheduler.get_job(timer_on).reschedule("interval", weeks = on_duration[0], days = on_duration[1], hours = on_duration[2], minutes = on_duration[3], seconds = on_duration[4]) + self.apscheduler.get_job(timer_on).resume() + self.apscheduler.get_job(timer_off).pause() + self.logger.info("Turning " + timer_on + "!") + + def interval_off(self, module, output, timer_off, timer_on, off_duration): + module.set_output(output, 0) + self.apscheduler.get_job(timer_off).reschedule("interval", weeks = off_duration[0], days = off_duration[1], hours = off_duration[2], minutes = off_duration[3], seconds = off_duration[4]) + self.apscheduler.get_job(timer_off).resume() + self.apscheduler.get_job(timer_on).pause() + self.logger.info("Turning " + timer_off + "!") + + def add_schedule(self, name, trigger, module_name, output, on_param, off_param): + + module = self.network.module_list.lookup_module_by_name(module_name) + if not module == None: + + if trigger == "cron": + on_job = self.apscheduler.add_job(module.set_output, trigger, [output, 1], day = on_param[0], day_of_week = on_param[1], hour = on_param[2], minute = on_param[3], second = on_param[4]) + off_job = self.apscheduler.add_job(module.set_output, trigger, [output, 0], day = off_param[0], day_of_week = off_param[1], hour = off_param[2], minute = off_param[3], second = off_param[4]) + + #evalute the current state of the cron trigger and set output on if needed + if on_job.next_run_time > off_job.next_run_time: + module.set_output(output, 1) + else: + module.set_output(output, 0) + + if trigger == "interval": + self.apscheduler.add_job(self.interval_on, trigger, [module, output, name + "_off", name + "_on", on_param], id = name + "_off", weeks = off_param[0], days = off_param[1], hours = off_param[2], minutes = off_param[3], seconds = off_param[4]) + timer = self.apscheduler.add_job(self.interval_off, trigger, [module, output, name + "_off", name + "_on", off_param], id = name + "_on", weeks = on_param[0], days = on_param[1], hours = on_param[2], minutes = on_param[3], seconds = on_param[4]) + timer.pause() + self.interval_on(module, output, name + "_off", name + "_on", on_param) + + else: + self.logger.warning("Module not found: " + module_name) + + + def load_schedules(self): + + for section in self.config.sections(): + if "timer" in section: + items = self.config.items(section) + for item in items: + if item[0] == "trigger": + trigger = item[1] + if item[0] == "module_name": + module_name = item[1] + if item[0] == "output": + output = item[1] + if item[0] == "on_time": + on_param = ast.literal_eval(item[1]) + if item[0] == "off_time": + off_param = ast.literal_eval(item[1]) + if item[0] == "on_duration": + on_param = ast.literal_eval(item[1]) + if item[0] == "off_duration": + off_param = ast.literal_eval(item[1]) + + self.add_schedule(section, trigger, module_name, output, on_param, off_param)