# -*- coding: utf-8 -*- # (c) 2019-2020 Andreas Motl <andreas@hiveeyes.org> # (c) 2019-2020 Richard Pobering <richard@hiveeyes.org> # (c) 2019-2020 Jan Hoffmann <jan.hoffmann@bergamsee.de> # License: GNU General Public License, Version 3 from binascii import hexlify from machine import Pin from terkin import logging from terkin.sensor.common import AbstractBus log = logging.getLogger(__name__) #log.setLevel(logging.DEBUG) class BusType: """ """ I2C = 'i2c' OneWire = 'onewire' class SensorManager: """Manages all buses and sensors.""" def __init__(self, settings): self.sensors = [] self.buses = {} self.settings = settings def register_sensor(self, sensor): """ :param sensor: """ self.sensors.append(sensor) def register_bus(self, bus): """ :param bus: """ bus_name = bus.name log.info('Registering bus "%s"', bus_name) self.buses[bus_name] = bus def get_bus_by_name(self, name): """ :param name: """ if name is None: log.error('Bus "{}" does not exist'.format(name)) return log.debug('Trying to find bus by name "%s"', name) bus = self.buses.get(name) log.debug('Found bus by name "%s": %s', name, bus) return bus def get_bus_by_sensortype(self, type): """ Return the bus for this sensor type. """ for sensor in self.sensors: if hasattr(sensor, 'type') and sensor.type == type: return sensor.bus def get_sensor_by_name(self, name): """ :param name: """ raise NotImplementedError('"get_sensor_by_name" not implemented yet') def get_sensor_by_type(self, type): """ Return all sensors filtered by type. """ for sensor in self.sensors: if hasattr(sensor, 'type') and sensor.type == type: yield sensor def get_sensors_by_family(self, family): """ Return all sensors filtered by family. """ for sensor in self.sensors: if hasattr(sensor, 'family') and sensor.family == family: yield sensor def setup_buses(self, buses_settings): """Register configured I2C, OneWire and SPI buses. :param buses_settings: """ effective_buses = [] effective_bus_ids = [] for bus_settings in buses_settings: if bus_settings.get("enabled"): effective_buses.append(bus_settings) effective_bus_ids.append(bus_settings['id']) log.info('Starting buses: %s', effective_bus_ids) for bus_settings in effective_buses: try: self.setup_bus(bus_settings) except Exception as ex: log.exc(ex, 'Registering bus failed. settings={}'.format(bus_settings)) def setup_bus(self, bus_settings): """ :param bus_settings: """ bus_family = bus_settings.get('family') if bus_family == BusType.OneWire: owb = OneWireBus(bus_settings) if 'pin_data' in bus_settings: owb.register_pin("data", bus_settings['pin_data']) owb.start() self.register_bus(owb) elif bus_family == BusType.I2C: i2c = I2CBus(bus_settings) if 'pin_sda' in bus_settings: i2c.register_pin("sda", bus_settings['pin_sda']) if 'pin_scl' in bus_settings: i2c.register_pin("scl", bus_settings['pin_scl']) i2c.start() self.register_bus(i2c) else: log.error("Invalid bus configuration: %s", bus_settings) def power_on(self): """Send power-on to all buses and sensors""" if self.settings.get('sensors.power_toggle_buses', True): self.power_toggle_buses('power_on') if self.settings.get('sensors.power_toggle_sensors', True): self.power_toggle_sensors('power_on') def power_off(self): """Send power-off to all buses and sensors""" if self.settings.get('sensors.power_toggle_sensors', True): self.power_toggle_sensors('power_off') if self.settings.get('sensors.power_toggle_buses', True): self.power_toggle_buses('power_off') def power_toggle_sensors(self, action): """ :param action: """ for sensor in self.sensors: sensorname = sensor.__class__.__name__ if hasattr(sensor, action): log.info('Sending {} to sensor {}'.format(action, sensorname)) try: getattr(sensor, action)() except Exception as ex: log.exc(ex, 'Sending {} to sensor {} failed'.format(action, sensorname)) def power_toggle_buses(self, action): """ :param action: """ for busname, bus in self.buses.items(): if hasattr(bus, action): log.info('Sending {} to bus {}'.format(action, busname)) try: getattr(bus, action)() except Exception as ex: log.exc(ex, 'Sending {} to sensor {} failed'.format(action, busname)) def start_sensors(self): log.info("Starting all sensors") for sensor in self.sensors: if hasattr(sensor, 'start'): try: sensor.start() except Exception as ex: log.exc(ex, 'Starting sensor "{}" failed. Reason: {}.'.format(sensor.type, ex)) class OneWireBus(AbstractBus): """Initialize the 1-Wire hardware driver and represent as bus object.""" type = BusType.OneWire def start(self): """ """ # Todo: Improve error handling. try: # Vanilla MicroPython 1.11 if self.platform_info.vendor == self.platform_info.MICROPYTHON.Vanilla: pin = Pin(int(self.pins['data'][1:])) import onewire_native self.adapter = onewire_native.OneWire(pin) # Pycom MicroPython 1.9.4 elif self.platform_info.vendor == self.platform_info.MICROPYTHON.Pycom: pin = Pin(self.pins['data']) if self.settings.get('driver') == 'native': log.info('Using native 1-Wire driver on Pycom MicroPython') import onewire_native self.adapter = onewire_native.OneWire(pin) else: log.info('Using pure-Python 1-Wire driver on Pycom MicroPython') import onewire_python self.adapter = onewire_python.OneWire(pin) elif self.platform_info.vendor == self.platform_info.MICROPYTHON.RaspberryPi: from terkin.sensor.linux import LinuxSysfsOneWireBus sysfs = self.settings['sysfs'] self.adapter = LinuxSysfsOneWireBus(sysfs) else: raise NotImplementedError('1-Wire bus support is not implemented on this platform') self.scan_devices() self.ready = True except Exception as ex: #log.exc(ex, '1-Wire hardware driver failed') #return False raise return True def scan_devices(self): """ """ # The 1-Wire bus sometimes needs a fix when coming back from deep sleep. #self.adapter.reset() # TODO: Tune this further? #time.sleep(1) # Scan for 1-Wire devices and remember them. # TODO: Refactor things specific to DS18x20 devices elsewhere. self.devices = [rom for rom in self.adapter.scan() if rom[0] == 0x10 or rom[0] == 0x28] log.info("Found {} 1-Wire (DS18x20) devices: {}".format(len(self.devices), self.get_devices_ascii())) def get_devices_ascii(self): """ """ return list(map(self.device_address_ascii, self.devices)) def serialize(self): """ """ info = super().serialize() if 'devices' in info: info['devices'] = self.get_devices_ascii() return info @staticmethod def device_address_ascii(address): """ :param address: """ # Compute ASCII representation of device address. if isinstance(address, (bytearray, bytes)): address = hexlify(address).decode() return address.lower() class I2CBus(AbstractBus): """Initialize the I2C hardware driver and represent as bus object.""" type = BusType.I2C frequency = 100000 def start(self): """ """ # Todo: Improve error handling. try: if self.platform_info.vendor == self.platform_info.MICROPYTHON.Vanilla: from machine import I2C self.adapter = I2C(self.number, sda=Pin(int(self.pins['sda'][1:])), scl=Pin(int(self.pins['scl'][1:])), freq=self.frequency) elif self.platform_info.vendor == self.platform_info.MICROPYTHON.Pycom: from machine import I2C self.adapter = I2C(self.number, mode=I2C.MASTER, pins=(self.pins['sda'], self.pins['scl']), baudrate=self.frequency) elif self.platform_info.vendor == self.platform_info.MICROPYTHON.Odroid: from smbus2 import SMBus self.adapter = SMBus(self.number) elif self.platform_info.vendor == self.platform_info.MICROPYTHON.RaspberryPi: import board import busio def i2c_add_bus(busnum, scl, sda): """ Register more I2C buses with Adafruit Blinka. Make Adafruit Blinka learn another I2C bus. Please make sure you define it within /boot/config.txt like:: dtoverlay=i2c-gpio,bus=3,i2c_gpio_delay_us=1,i2c_gpio_sda=26,i2c_gpio_scl=20 """ # Uncache this module, otherwise monkeypatching will fail on subsequent calls. import sys try: del sys.modules['microcontroller.pin'] except: pass # Monkeypatch "board.pin.i2cPorts". i2c_port = (busnum, scl, sda) if i2c_port not in board.pin.i2cPorts: board.pin.i2cPorts += (i2c_port,) pin_scl = self.pins['scl'] pin_sda = self.pins['sda'] # When I2C port pins are defined as Integers, register them first. if isinstance(pin_scl, int): i2c_add_bus(self.number, pin_scl, pin_sda) SCL = board.pin.Pin(pin_scl) SDA = board.pin.Pin(pin_sda) # When I2C port pins are defined as Strings and start with "board.", # they are probably already Pin aliases of Adafruit Blinka. elif isinstance(pin_scl, str) and pin_scl.startswith('board.'): SCL = eval(pin_scl) SDA = eval(pin_sda) self.adapter = busio.I2C(SCL, SDA) else: raise NotImplementedError('I2C bus is not implemented on this platform') self.just_started = True if not self.platform_info.vendor == self.platform_info.MICROPYTHON.Odroid: self.scan_devices() if self.platform_info.vendor == self.platform_info.MICROPYTHON.Odroid: self.devices = self.scan_devices_smbus2() log.info("Scan I2C bus via smbus2 for devices...") log.info("Found {} I2C devices: {}.".format(len(self.devices), self.devices)) self.ready = True except Exception as ex: #log.exc(ex, 'I2C hardware driver failed') raise def scan_devices(self): log.info('Scan I2C with id={} bus for devices...'.format(self.number)) self.devices = self.adapter.scan() # i2c.readfrom(0x76, 5) log.info("Found {} I2C devices: {}".format(len(self.devices), self.devices)) def scan_devices_smbus2(self, start=0x03, end=0x78): try: list = [] for i in range(start, end): val = 1 try: self.adapter.read_byte(i) except OSError as e: val = e.args[0] finally: if val != 5: # No device if val == 1: res = "Available" elif val == 16: res = "Busy" elif val == 110: res = "Timeout" else: res = "Error code: " + str(val) # print(hex(i) + " -> " + res) if res == 'Available': # print(i) list.append(i) return list except Exception as exp: log.exc(exp, 'scan smbus2 failed') def power_on(self): """ Turn on the I2C peripheral after power off. """ # Don't reinitialize device if power on just occurred through initial driver setup. if self.just_started: self.just_started = False return # uPy doesn't have deinit so it doesn't need init if self.platform_info.vendor == self.platform_info.MICROPYTHON.Pycom: from machine import I2C self.adapter.init(mode=I2C.MASTER, baudrate=self.frequency) def power_off(self): """ Turn off the I2C peripheral. https://docs.pycom.io/firmwareapi/pycom/machine/i2c.html """ log.info('Turning off I2C bus {}'.format(self.name)) if self.platform_info.vendor == self.platform_info.MICROPYTHON.Pycom: self.adapter.deinit()