Changeset - 25926382c27b
[Not reviewed]
refactor
0 5 4
matthewreed - 8 years ago 2017-07-11 20:12:50

Added modules for database connection, scheduler, and comms protocol definition
9 files changed with 677 insertions and 329 deletions:
0 comments (0 inline, 0 general)
database.py
Show inline comments
 
new file 100644
 
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))
hydrobot.py
Show inline comments
 
#!/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}
 
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!")
 
    sys.exit(0)
 
        
 
signal.signal(signal.SIGINT, close_program)
 

	
 
if __name__ == "__main__":
 
    sys.exit(main(sys.argv[1:]))
 
    main(sys.argv[1:])
hydrobot_def.json
Show inline comments
 
new file 100644
 
{
 
    "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"
 
        }
 
    ]
 
}
message.py
Show inline comments
 
#!/usr/bin/env python
 

	
 
class HydroBotMessage:
 
    
 
    def __init__(self, module_uuid, message_type, data_key, sensor_num, data):
 
        
 
        self.module_uuid = module_uuid
module.py
Show inline comments
 
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'
 
        if module in self._modules:
 
            raise ValueError('Module %s already in database' % module)
 
        else:
 
@@ -22,71 +48,136 @@ class ModuleList():
 
        
 
    def lookup_module(self, uuid):
 
        for module in self._modules:
 
            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)
 
        
network.py
Show inline comments
 
#!/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:
 
            if interface.interface_name == interface_name:
 
                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()
 
    
 
    def send_message(self, message):
 
        module = self.module_list.lookup_module(message.module_uuid)
 
        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)
 
    
network_interface.py
Show inline comments
 
#!/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
 
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
 
        os.system("sudo ip link set " + interface_name + " up type can bitrate 500000")
 
        
 
        self.interface_name = interface_name
 
        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, ())
 
        
 
    def send_message(self, message):
 
        address = self.uuid_lookup[message.module_uuid]
 
        can_frame = can.Frame(address, 8, [message.message_type, (message.data_key>>8) & 0xff, (message.data_key>>0) & 0xff, message.sensor_num] + list(struct.pack("f", message.data)))
 
@@ -60,37 +61,40 @@ class CanBusNetworkInterface(NetworkInte
 
        self.queue.send(can_frame)
 
        
 
    def process_message(self):
 
        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
 
        
 
    def send_message(self, message):
 
        pass
 
        
 
    def process_message(self, message):
 
        pass
 
        
 

	
 

	
 
#module knows how to send and recieve messages
 

	
protocol.py
Show inline comments
 
new file 100644
 
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')
 
            
scheduler.py
Show inline comments
 
new file 100644
 
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)
0 comments (0 inline, 0 general)