Example #1
0
class TerkinDatalogger:

    # Application metadata.
    name = 'Terkin MicroPython Datalogger'
    version = __version__

    def __init__(self, settings):

        # Obtain configuration settings.
        self.settings = TerkinConfiguration()
        self.settings.add(settings)

        # Configure logging.
        logging_enabled = self.settings.get('main.logging.enabled', False)
        if not logging_enabled:
            logging.disable_logging()

        # Initialize device.
        self.device = TerkinDevice(name=self.name, version=self.version, settings=self.settings)

        # Button manager instance (optional).
        self.button_manager = None

        # Initialize sensor domain.
        self.sensor_manager = SensorManager()

    @property
    def appname(self):
        return '{} {}'.format(self.name, self.version)

    def start(self):

        # Report about wakeup reason and run wakeup tasks.
        self.device.resume()

        # Turn off LTE modem and Bluetooth as we don't use them yet.
        # Todo: Revisit where this should actually go.
        self.device.power_off_lte_modem()
        self.device.power_off_bluetooth()

        log.info('Starting %s', self.appname)

        # Start the watchdog for sanity.
        self.device.start_watchdog()

        # Configure RGB-LED according to settings.
        self.device.configure_rgb_led()

        # Dump configuration settings.
        log_configuration = self.settings.get('main.logging.configuration', False)
        if log_configuration:
            self.settings.dump()

        # Initialize buttons / touch pads.
        buttons_enabled = self.settings.get('sensors.system.buttons.enabled', False)
        if buttons_enabled:
            self.button_manager = ButtonManager()
            self.start_buttons()

        # Disable this if you don't want serial access.
        #self.device.enable_serial()

        # Hello world.
        self.device.print_bootscreen()

        # Bootstrap infrastructure.
        self.device.start_networking()

        # Conditionally start telemetry if networking is available.
        if self.device.status.networking:
            self.device.start_telemetry()

        # Todo: Signal readyness by publishing information about the device (Microhomie).
        # e.g. ``self.device.publish_properties()``

        # Setup sensors.
        self.device.feed_watchdog()
        bus_settings = self.settings.get('sensors.busses')
        self.sensor_manager.register_busses(bus_settings)
        self.register_sensors()

        # Power up sensor peripherals.
        self.sensor_manager.power_on()

        # Ready.
        self.start_mainloop()

    def start_mainloop(self):

        # Todo: Refactor by using timers.

        # Enter the main loop.
        while True:

            # Feed the watchdog timer to keep the system alive.
            self.device.feed_watchdog()

            # Indicate activity.
            # Todo: Optionally disable this output.
            log.info('--- loop ---')

            # Run downstream mainloop handlers.
            self.loop()

            # Yup.
            machine.idle()

    def loop(self):
        """
        Main duty cycle loop.
        """

        #log.info('Terkin loop')

        # Read sensors.
        readings = self.read_sensors()

        # Transmit data.
        self.transmit_readings(readings)

        # Run the garbage collector.
        self.device.run_gc()

        # Sleep how ever.
        self.sleep()

    def sleep(self):
        """
        Sleep until the next measurement cycle.
        """
        interval = self.settings.get('main.interval')
        #print(dir(machine))

        # Use deep sleep if requested.
        try:
            deep = self.settings.get('main.deepsleep', False)
            if deep:

                # Shut down sensor peripherals.
                self.sensor_manager.power_off()

                # Shut down device peripherals.
                self.device.power_off()

            # Send device to deep sleep.
            self.device.hibernate(interval, deep=deep)

        # When hibernation fails, fall back to regular "time.sleep".
        except:
            log.exception('Failed to hibernate, falling back to regular sleep')
            # Todo: Emit error message here.
            log.info('Sleeping for {} seconds'.format(interval))
            time.sleep(interval)

    def register_sensors(self):
        """
        Add system sensors.
        """

        log.info('Registering Terkin sensors')

        system_sensors = [
            SystemMemoryFree,
            SystemTemperature,
            SystemBatteryLevel,
            SystemUptime,
        ]

        # Create environmental sensor adapters.
        for sensor_factory in system_sensors:
            sensor = sensor_factory()
            if hasattr(sensor, 'setup') and callable(sensor.setup):
                sensor.setup(self.settings)
            self.sensor_manager.register_sensor(sensor)

        # Add WiFi metrics.
        try:
            self.sensor_manager.register_sensor(SystemWiFiMetrics(self.device.networking.wifi_manager.station))
        except:
            log.exception('Enabling SystemWiFiMetrics sensor failed')

    def read_sensors(self):
        """Read sensors"""
        data = {}
        sensors = self.sensor_manager.sensors
        log.info('Reading %s sensor ports', len(sensors))
        for sensor in sensors:

            sensorname = sensor.__class__.__name__
            log.info('Reading sensor port "%s"', sensorname)

            try:
                reading = sensor.read()
                if reading is None or reading is AbstractSensor.SENSOR_NOT_INITIALIZED:
                    continue
                data.update(reading)

            except:
                log.exception('Reading sensor "%s" failed', sensorname)

            self.device.feed_watchdog()

        # Debugging: Print sensor data before running telemetry.
        log.info('Sensor data:  %s', data)

        return data

    def transmit_readings(self, data):
        """Transmit data"""

        # TODO: Optionally disable telemetry.
        if self.device.telemetry is None:
            log.warning('Telemetry disabled')
            return False

        telemetry_status = self.device.telemetry.transmit(data)
        count_total = len(telemetry_status)
        success = all(telemetry_status.values())

        # Evaluate telemetry status outcome.
        if success:
            log.info('Telemetry status: SUCCESS ({}/{})'.format(count_total, count_total))
        else:
            count_failed = len([item for item in telemetry_status.values() if item is not True])
            log.warning('Telemetry status: FAILURE. {} out of {} targets failed. '
                        'Status: {}'.format(count_failed, count_total, telemetry_status))

        return success

    def start_buttons(self):

        # RGB-LED: 2
        # POWER-ENABLE: 3
        # SD-Card: 4, 8
        # LTE 19, 20
        # Misc: 13, 14, 9, 23

        # Physical location when looking at the board with the RGB-LED oriented to the top.

        # Location: Left side, 6th pin from top.
        self.button_manager.setup_touchpad('P4', name='Touch3', location='Module-Left-Top-6th')

        # Location: Left side, 5th pin from bottom.
        self.button_manager.setup_touchpad('P8', name='Touch2', location='Module-Left-Bottom-5th')

        # Location: Right side.
        self.button_manager.setup_touchpad('P23', name='Touch6', location='Module-Right-Top-4th')
Example #2
0
class TerkinDatalogger:
    """ 
    Main class of project.
    Handles loop & sleep, registers sensors, reads their data and stores them.
    Shows up as 'datalogger' in the rest of the program.
    """

    # Application metadata.
    name = 'Terkin Datalogger'
    version = __version__

    # For the singleton factory.
    __instance__ = None

    def __init__(self, settings, platform_info: PlatformInfo = None):

        # Reference to the chronometer used for general timekeeping.
        self.duty_chrono = bootloader.duty_chrono

        # Signal startup with first available timestamp.
        log.info('Starting Terkin datalogger')

        # Obtain configuration settings.
        self.settings = TerkinConfiguration()
        self.settings.add(settings)
        self.settings.add_user_file()

        # Configure logging.
        logging_enabled = self.settings.get('main.logging.enabled', False)
        if not logging_enabled:
            log.info('Disabling logging to save bytes')
            logging.disable_logging()

        # Initialize ApplicationInfo object.
        self.application_info = ApplicationInfo(name=self.name,
                                                version=self.version,
                                                settings=self.settings,
                                                application=self,
                                                platform_info=platform_info)

        # Initialize transient storage.
        self.storage = TransientStorage()

        # Initialize device.
        self.device = TerkinDevice(self.application_info)

        # Button manager instance (optional).
        self.button_manager = None

        # Initialize sensor domain.
        self.sensor_manager = SensorManager(self.settings)

    def setup(self):

        # Report about wakeup reason and run wakeup tasks.
        self.device.resume()

        # Start the watchdog for sanity.
        self.device.watchdog.start()

        # Configure RGB-LED according to settings.
        self.device.configure_rgb_led()

        # Alternative startup signalling: 2 x green.
        self.device.blink_led(0x000b00, count=2)

        # Free up some memory.
        self.device.run_gc()

        # Turn off LTE modem and Bluetooth as we don't use them yet.
        # TODO: Make this configurable.
        has_lte_modem = self.application_info.platform_info.device_name in [
            'GPy', 'FiPy'
        ]
        lte_enabled = self.settings.get('networking.lte.enabled')
        if has_lte_modem and not lte_enabled:
            self.device.power_off_lte_modem()

        self.device.power_off_bluetooth()

        log.info('Starting %s', self.application_info.fullname)

        # Dump configuration settings.
        log_configuration = self.settings.get('main.logging.configuration',
                                              False)
        if log_configuration:
            self.settings.dump()

        # Disable this if you don't want serial access.
        #self.device.enable_serial()

        # Hello world.
        self.device.print_bootscreen()

        # Start networking and telemetry subsystems.

        # Conditionally start network services and telemetry if networking is available.
        try:
            self.device.start_networking()
        except Exception as ex:
            log.exc(ex, 'Networking subsystem failed')
            self.device.status.networking = False

        self.device.start_telemetry()

        # Todo: Signal readyness by publishing information about the device (Microhomie).
        # e.g. ``self.device.publish_properties()``

        self.setup_sensors()

    def setup_sensors(self):

        # Setup sensors.
        log.info('Setting up sensors')
        self.device.watchdog.feed()
        bus_settings = self.settings.get(
            'sensors.buses', self.settings.get('sensors.busses', []))
        self.sensor_manager.setup_buses(bus_settings)
        self.register_sensors()
        self.sensor_manager.start_sensors()

        log.info('Setup finished')

    def start(self):
        self.start_mainloop()

    def start_mainloop(self):
        """ """

        # Todo: Refactor by using timers.

        # Enter the main loop.
        while True:
            self.duty_task()

    def duty_task(self):
        """Main duty cycle task."""

        # Feed the watchdog timer to keep the system alive.
        self.device.watchdog.feed()

        # Indicate activity.
        # Todo: Optionally disable this output.
        log.info('--- cycle ---')

        # Run downstream mainloop handlers.
        self.duty_cycle()

        # Sleep how configured
        self.sleep()

    def duty_cycle(self):
        """Main duty cycle"""

        if not self.settings.get('main.deepsleep', False):
            self.duty_chrono.reset()

        #log.info('Terkin loop')

        # Alternative loop signalling: 1 x blue.
        # https://forum.pycom.io/topic/2067/brightness-of-on-board-led/7
        self.device.blink_led(0x00000b, count=2)

        # Read sensors.
        readings = self.read_sensors()

        # Remember current reading
        self.storage.last_reading = readings.data_in

        # Run the garbage collector.
        self.device.run_gc()

        # Transmit data.
        transmission_success = self.transmit_readings(readings)

        # Signal transmission outcome.
        if transmission_success:
            self.device.blink_led(0x00000b)
        else:
            self.device.blink_led(0x0b0000)

        # Run the garbage collector.
        self.device.run_gc()

        # Give the system some breath.
        machine.idle()

    def sleep(self):
        """Sleep or shutoff until the next measurement cycle."""

        lightsleep = self.settings.get('main.lightsleep', False)
        deepsleep = self.settings.get('main.deepsleep', False)
        shutoff = self.settings.get('main.shutoff', False)
        interval = self.get_sleep_time()

        # Amend deep sleep intent when masked through maintenance mode.
        if self.device.status.maintenance is True:
            lightsleep = False
            deepsleep = False
            shutoff = False
            log.info('Device is in maintenance mode. Skipping deep sleep and '
                     'adjusting sleep time to {} seconds.'.format(interval))

        # Prepare device shutdown.
        try:
            # Shut down sensor peripherals.
            self.sensor_manager.power_off()

            # Shut down networking.
            if deepsleep or shutoff:
                self.device.networking.stop()

        except Exception as ex:
            log.exc(ex, 'Power off failed')

        if shutoff:
            # shut off the MCU via DS3231
            self.shutoff()
        else:
            # Activate device sleep mode.
            try:
                self.device.hibernate(interval,
                                      lightsleep=lightsleep,
                                      deepsleep=deepsleep)

            # When hibernation fails, fall back to regular "time.sleep".
            except Exception as ex:
                log.exc(ex,
                        'Failed to hibernate, falling back to regular sleep')
                # Todo: Emit error message here.
                log.info('Sleeping for {} seconds'.format(interval))
                time.sleep(interval)

    def get_sleep_time(self):
        """ 
        Calculate the next sleep intervall.
        """

        interval = self.settings.get('main.interval', 60.0)

        # Configuration switchover backward compatibility / defaults.
        if isinstance(interval, (float, int)):
            self.settings.set('main.interval', {})
            self.settings.setdefault('main.interval.field', interval)
        self.settings.setdefault('main.interval.maintenance', 5.0)

        interval = self.settings.get('main.interval.field')

        # First, try to acquire deep sleep interval from NVRAM.
        # This gets used when set from a LoRaWAN downlink message.
        # pycom.nvs_get should return "None" in case of unset key. Instead it throws an error
        try:
            import pycom
            interval_minutes = pycom.nvs_get('deepsleep')
            if isinstance(interval_minutes, int):
                log.info(
                    'Deep sleep interval set to %s minute(s) by LoRaWAN downlink message',
                    interval_minutes)
                interval = interval_minutes * 60

        # Otherwise, use original configuration setting.
        except Exception as ex:
            pass

        # Amend deep sleep intent when masked through maintenance mode.
        if self.device.status.maintenance is True:
            interval = self.settings.get('main.interval.maintenance')

        # Compute sleeping duration from measurement interval and elapsed time.
        elapsed = self.duty_chrono.read()
        sleep_time = interval - elapsed

        if sleep_time <= 0:
            sleep_time = interval

        return sleep_time

    def register_sensors(self):
        """
        Configure and register sensor objects.
        There are three types of sensors: system, environment & buses.

        The sensors are registered by calling their respective classes
        from terkin/driver.

        Definitions are in 'settings.py'.
        """

        # Add sensors.
        log.info('Registering sensors')
        sensor_infos = []

        # Get list of system sensors from configuration settings.
        sensor_infos += self.settings.get('sensors.system', [])

        # Get list of environmental sensors from configuration settings.
        sensor_infos += self.settings.get('sensors.environment', [])

        # Backward compatibility for environmental sensors.
        if sensor_infos is None:
            sensor_infos += self.settings.get('sensors.registry',
                                              {}).values() or []

        # Scan sensor definitions, create and register sensor objects.
        for sensor_info in sensor_infos:

            sensor_type = sensor_info.get('type', 'unknown').lower()
            sensor_id = sensor_info.get('id',
                                        sensor_info.get('key', sensor_type))

            # Skip sensor if disabled in configuration.
            if not sensor_info.get('enabled', False):
                log.debug(
                    'Sensor with id={} and type={} is disabled, skipping registration'
                    .format(sensor_id, sensor_type))
                continue

            # Skip WiFi sensor registration when WiFi is disabled.
            if sensor_type == 'system.wifi':
                if not self.settings.get('networking.wifi.enabled'):
                    log.info('WiFi is disabled, skipping sensor registration')
                    continue

            self.register_sensor(sensor_info)

            # Clean up memory after creating each sensor object.
            #self.device.run_gc()

    def register_sensor(self, sensor_info):
        """
        Register one sensor.
        """
        sensor_type = sensor_info.get('type', 'unknown').lower()
        sensor_id = sensor_info.get('id', sensor_info.get('key', sensor_type))
        description = sensor_info.get('description')

        # Resolve associated bus object.
        sensor_bus = None
        sensor_bus_name = None
        if 'bus' in sensor_info:
            sensor_info_bus = sensor_info['bus']
            sensor_bus = self.sensor_manager.get_bus_by_name(sensor_info_bus)

            # Skip sensor if associated bus is disabled in configuration.
            if sensor_bus is None:
                log.info(
                    'Bus {} for sensor with id={} and type={} is disabled, '
                    'skipping registration'.format(sensor_info_bus, sensor_id,
                                                   sensor_type))
                return
            sensor_bus_name = sensor_bus.name

        # Human readable sensor address.
        if 'address' in sensor_info:
            sensor_address = hex(sensor_info.get('address'))
        else:
            sensor_address = None

        # Report sensor registration to user.
        message = 'Setting up sensor with id={} and type={} on bus={} with address={}'.format(
            sensor_id, sensor_type, sensor_bus_name, sensor_address)
        if description:
            message += ' described as "{}"'.format(description)
        log.info(message)

        # Backward compat.
        if sensor_type == 'ds18b20':
            sensor_type = 'ds18x20'

        # Registration NG
        # Run self-registration procedure by invoking
        # the "includeme()" function on each sensor module.
        try:

            # Load sensor module.
            import terkin.driver
            modulename = '{}_sensor'.format(sensor_type)
            fullname = 'terkin.driver.{}'.format(modulename)
            log.info('Importing module "{}"'.format(fullname))
            __import__(fullname)
            module = getattr(terkin.driver, modulename)

            # Acquire sensor object.
            includeme = getattr(module, 'includeme')
            sensor_object = includeme(self.sensor_manager, sensor_info)

            # Register sensor with sensor manager.
            self.sensor_manager.register_sensor(sensor_object)

            return

        except ImportError as ex:
            if not fullname.startswith('terkin.driver.system'):
                log.error('Driver module "{}" not found'.format(fullname))

        except AttributeError as ex:
            if "has no attribute 'includeme'" in str(ex):
                log.warning(
                    'Driver module "{}" is deprecated, "includeme" is missing'.
                    format(fullname))
            else:
                log.exc(ex, 'Driver module "{}" failed'.format(fullname))

        except Exception as ex:
            log.exc(ex,
                    'Registering driver module "{}" failed'.format(fullname))

        # Legacy registration
        try:
            self.register_sensor_legacy(sensor_info, sensor_bus)
        except Exception as ex:
            log.exc(
                ex, 'Setting up sensor with id={} and type={} failed'.format(
                    sensor_id, sensor_type))

    def register_sensor_legacy(self, sensor_info, sensor_bus):

        sensor_type = sensor_info.get('type', 'unknown').lower()

        # Sensor reporting about free system memory.
        if sensor_type == 'system.memfree':
            sensor_object = SystemMemoryFree(sensor_info)

        # Sensor which reports system temperature.
        elif sensor_type == 'system.temperature':
            sensor_object = SystemTemperature(sensor_info)

        # Sensor which reports battery voltage.
        elif sensor_type in [
                'system.voltage.battery', 'system.battery-voltage'
        ]:
            sensor_object = SystemVoltage(sensor_info)

        # Sensor which reports solar panel voltage.
        elif sensor_type == 'system.voltage.solar':
            sensor_object = SystemVoltage(sensor_info)

        # Sensor which reports system uptime metrics.
        elif sensor_type == 'system.uptime':
            sensor_object = SystemUptime(sensor_info)

        # Sensor which reports WiFi metrics.
        elif sensor_type == 'system.wifi':
            try:
                sensor_object = SystemWiFiMetrics(
                    sensor_info, self.device.networking.wifi_manager.station)
            except Exception as ex:
                log.exc(ex, 'Enabling SystemWiFiMetrics sensor failed')
                return

        # Initialize buttons / touch pads.
        elif sensor_type == 'system.touch-buttons':
            from terkin.sensor.button import ButtonManager
            self.button_manager = ButtonManager()
            self.start_buttons()
            return

        else:
            raise SensorUnknownError(
                'Unknown sensor type "{}"'.format(sensor_type))

        # Register sensor object with sensor manager.
        self.sensor_manager.register_sensor(sensor_object)

    def read_sensors(self) -> DataFrame:
        """
        Read measurements from all sensor objects that have been registered in the sensor_manager.
        Reading is done with the read() function of each respective sensor object.
        """

        # Power up sensor peripherals.
        self.sensor_manager.power_on()

        # Collect observations.
        data = {}
        richdata = {}
        readings = []

        # Iterate all registered sensors.
        sensors = self.sensor_manager.sensors
        log.info('Reading %s sensor ports', len(sensors))
        for sensor in sensors:

            # Signal sensor reading to user.
            sensorname = sensor.__class__.__name__
            log.info('Reading sensor port "%s"', sensorname)

            # Read sensor port.
            try:

                # Disable garbage collector to guarantee reasonable
                # realtime behavior before invoking sensor reading.
                with gc_disabled():
                    sensor_outcome = sensor.read()

                # Power off HX711 after reading
                if "HX711Sensor" in sensorname:
                    sensor.power_off()

                # Backward compat.
                if isinstance(sensor_outcome, SensorReading):
                    sensor_reading = sensor_outcome
                else:
                    sensor_reading = SensorReading()
                    sensor_reading.sensor = sensor
                    sensor_reading.data = sensor_outcome

                sensor_data = sensor_reading.data

                # Evaluate sensor outcome.
                if sensor_data is None or sensor_data is AbstractSensor.SENSOR_NOT_INITIALIZED:
                    continue

                # Round values according to sensor settings.
                if sensor.settings.get('decimals') is not None:
                    for key, value in sensor_data.items():
                        sensor_data[key] = round(
                            sensor_data[key], sensor.settings.get('decimals'))

                # Add sensor reading to observations.
                data.update(sensor_data)

                # Record reading for prettified output.
                self.record_reading(sensor, sensor_data, richdata)

                readings.append(sensor_reading)

            except Exception as ex:
                # Because of the ``gc_disabled`` context manager used above,
                # the propagation of exceptions has to be tweaked like that.
                log.exc(ex, 'Reading sensor "%s" failed', sensorname)

            # Feed the watchdog.
            self.device.watchdog.feed()

            # Clean up memory after reading each sensor object.
            #self.device.run_gc()

        # Debugging: Print sensor data before running telemetry.
        prettify_log = self.settings.get('sensors.prettify_log', False)
        if prettify_log:
            from terkin.util import ddformat
            log.info('Sensor data:\n\n%s', ddformat(richdata, indent=11))
        else:
            log.info('Sensor data:  %s', data)

        # Capture all sensor readings.
        result = DataFrame()
        result.readings = readings
        result.data_in = data

        return result

    def record_reading(self, sensor, reading, richdata):
        """

        :param sensor: 
        :param reading: 
        :param richdata: 

        """
        for key, value in reading.items():
            richdata[key] = {'value': value}
            if hasattr(sensor,
                       'settings') and 'description' in sensor.settings:
                richdata[key]['description'] = sensor.settings.get(
                    'description')
                # Hack to propagate the correct detail-description to prettified output.
                # TODO: Attach settings directly to its reading, while actually reading it.
                if 'devices' in sensor.settings:
                    for device_settings in sensor.settings['devices']:
                        device_address = device_settings['address'].lower()
                        if device_address in key:
                            if hasattr(sensor, 'get_device_description'):
                                device_description = sensor.get_device_description(
                                    device_address)
                                if device_description:
                                    richdata[key][
                                        'description'] = device_description

    def transmit_readings(self, dataframe: DataFrame):
        """
        Transmit data

        :param dataframe:

        """

        # TODO: Optionally disable telemetry.
        if self.device.telemetry is None:
            log.warning('Telemetry disabled')
            return False

        telemetry_status = self.device.telemetry.transmit(dataframe)
        count_total = len(telemetry_status)
        success = all(telemetry_status.values())

        # Evaluate telemetry status outcome.
        if success:
            log.info('Telemetry status: SUCCESS ({}/{})'.format(
                count_total, count_total))
        else:
            count_failed = len([
                item for item in telemetry_status.values() if item is not True
            ])
            log.warning(
                'Telemetry status: FAILURE. {} out of {} targets failed. '
                'Status: {}'.format(count_failed, count_total,
                                    telemetry_status))

        return success

    def start_buttons(self):
        """
        Configure ESP32 touchpads.
        """

        # RGB-LED: 2
        # POWER-ENABLE: 3
        # SD-Card: 4, 8
        # LTE 19, 20
        # Misc: 13, 14, 9, 23

        # Physical location when looking at the board with the RGB-LED oriented to the top.

        # Location: Left side, 6th pin from top.
        self.button_manager.setup_touchpad('P4',
                                           name='Touch3',
                                           location='Module-Left-Top-6th')

        # Location: Left side, 5th pin from bottom.
        self.button_manager.setup_touchpad('P8',
                                           name='Touch2',
                                           location='Module-Left-Bottom-5th')

        # Location: Right side.
        self.button_manager.setup_touchpad('P23',
                                           name='Touch6',
                                           location='Module-Right-Top-4th')

        # Location: Right side.
        # ValueError: invalid pin for touchpad
        """
        P18 and P17 are able to wake up on rising and falling edge. These two pins have internal
        pull-ups configurable by software (Pull-downs if needed must be added externally)

        -- https://docs.pycom.io/gitbook/assets/deepsleep-pinout.pdf


        ext0 External Wake-up Source
        RTC controller contains logic to trigger wake-up when one particular pin is set to
        a predefined logic level. That pin can be one of RTC GPIOs 0,2,4,12-15,25-27,32-39.

        -- https://lastminuteengineers.com/esp32-deep-sleep-wakeup-sources/#ext0-external-wakeup-source

        """
        #self.button_manager.setup_touchpad('P17', name='TouchX', location='Module-Right-Bottom-5th')
        #self.button_manager.setup_touchpad('P18', name='TouchY', location='Module-Right-Bottom-6th')

        # Will yield ``ValueError: Touch pad error``.
        #self.button_manager.setup_touchpad('P20', name='Touch8', location='Module-Right-Top-7th')
        #self.button_manager.setup_touchpad('P19', name='Touch9', location='Module-Right-Top-8th')

    def shutoff(self):
        """ shut off the MCU """

        import DS3231tokei
        import utime
        from machine import Pin, RTC

        bus = self.sensor_manager.get_bus_by_sensortype('DS3231')
        ds = DS3231tokei.DS3231(bus.adapter)
        interval = self.settings.get(
            'main.interval.shutoff',
            10) * 60  # convert from minutes to seconds
        (year, month, day, dotw, hour, minute,
         second) = ds.getDateTime()  # get the current time

        rtc = RTC()  # create RTC
        if year < 2001:
            year = 2001  # sanity check, as of mpy 1.12 year must be >= 2001
        rtc.init((year, month, day, dotw, hour, minute, second, 0))  # set time

        # check if its night or winter and adjust interval
        night_start = self.settings.get('main.interval.night_start', 0)
        night_end = self.settings.get('main.interval.night_end', 0)
        winter_start = self.settings.get('main.interval.winter_start', 0)
        winter_end = self.settings.get('main.interval.winter_end', 0)
        if night_start > 0 and (hour >= night_start or hour <= night_end
                                ):  # double interval for the night
            interval *= 2
        if winter_start > 0 and (month >= winter_start or month <= winter_end
                                 ):  # double interval for winter
            interval *= 2

        # Compute sleeping duration from measurement interval and elapsed time.
        elapsed = int(self.duty_chrono.read())
        now_secs = utime.mktime(utime.localtime())
        wake_at = now_secs - elapsed + interval
        if (wake_at - now_secs) < 180:  # don't shutoff for less than 3 minutes
            wake_at += interval

        (year, month, day, hour, minute, second, dotw,
         doty) = utime.localtime(wake_at)  # convert the wake up time

        # set alarm
        ds.setAlarm2(day, hour, minute, DS3231tokei.A2_ON_HOUR_MINUTE)

        # turn off MCU via MOSFET
        utime.sleep(1)
        ds.enableAlarm2()
        ds.resetAlarm2()

        # The End

    def scale_wizard(self):
        """
        Invoke scale adjustment wizard.

        Synopsis:

        - Invoke Terkin datalogger.
        - Interrupt by pressing CTRL+C.
        - Type ``datalogger.scale_wizard()``.
        """

        # Setup sensors.
        self.setup_sensors()

        # Invoke scale adjustment routine.
        from terkin.sensor.scale import ScaleAdjustment
        adj = ScaleAdjustment(sensor_manager=self.sensor_manager)
        adj.start_wizard()
Example #3
0
class TerkinDatalogger:

    # Application metadata.
    name = 'Terkin MicroPython Datalogger'
    version = __version__

    # For the singleton factory.
    __instance__ = None

    def __init__(self, settings, platform_info=None):

        # Fulfill singleton factory.
        TerkinDatalogger.__instance__ = self

        # Obtain configuration settings.
        self.settings = TerkinConfiguration()
        self.settings.add(settings)
        self.settings.add_user_file()

        self.application_info = ApplicationInfo(name=self.name,
                                                version=self.version,
                                                settings=self.settings,
                                                application=self,
                                                platform_info=platform_info)

        # Configure logging.
        logging_enabled = self.settings.get('main.logging.enabled', False)
        if not logging_enabled:
            log.info('Disabling logging to save bytes')
            logging.disable_logging()

        # Initialize transient storage.
        self.storage = TransientStorage()

        # Initialize device.
        self.device = TerkinDevice(self.application_info)

        # Button manager instance (optional).
        self.button_manager = None

        # Initialize sensor domain.
        self.sensor_manager = SensorManager()

        self.duty_chrono = GenericChronometer()

    @staticmethod
    def getInstance(settings=None):
        """
        Singleton factory.
        """
        if TerkinDatalogger.__instance__ is None:
            if settings is None:
                raise Exception(
                    "Settings are None but instance wasn't created before.")
            else:
                TerkinDatalogger(settings)

        return TerkinDatalogger.__instance__

    def setup(self):
        pass

    def start(self):

        self.duty_chrono.reset()

        # Report about wakeup reason and run wakeup tasks.
        self.device.resume()

        # Start the watchdog for sanity.
        self.device.watchdog.start()

        # Configure RGB-LED according to settings.
        self.device.configure_rgb_led()

        # Alternative startup signalling: 2 x green.
        self.device.blink_led(0x000b00, count=2)

        self.device.run_gc()

        # Turn off LTE modem and Bluetooth as we don't use them yet.
        # Todo: Revisit where this should actually go.
        # The modem driver takes about six seconds to initialize, so adjust the watchdog accordingly.
        self.device.watchdog.reconfigure_minimum_timeout(15000)
        if not self.settings.get('main.fastboot', False):
            self.device.power_off_lte_modem()
        self.device.power_off_bluetooth()
        self.device.watchdog.resume()

        log.info('Starting %s', self.application_info.fullname)

        # Dump configuration settings.
        log_configuration = self.settings.get('main.logging.configuration',
                                              False)
        if log_configuration:
            self.settings.dump()

        # Initialize buttons / touch pads.
        buttons_enabled = self.settings.get('sensors.system.buttons.enabled',
                                            False)
        if buttons_enabled:
            from terkin.sensor.button import ButtonManager
            self.button_manager = ButtonManager()
            self.start_buttons()

        # Disable this if you don't want serial access.
        #self.device.enable_serial()

        # Hello world.
        self.device.print_bootscreen()

        # Start networking and telemetry subsystems.

        # Conditionally start network services and telemetry if networking is available.
        try:
            self.device.start_networking()
        except Exception as ex:
            log.exc(ex, 'Networking subsystem failed')
            self.device.status.networking = False

        self.device.start_telemetry()

        # Todo: Signal readyness by publishing information about the device (Microhomie).
        # e.g. ``self.device.publish_properties()``

        # Setup sensors.
        self.device.watchdog.feed()
        bus_settings = self.settings.get('sensors.busses', [])
        self.sensor_manager.setup_busses(bus_settings)
        self.register_sensors()

        # Power up sensor peripherals.
        self.sensor_manager.power_on()

        # Ready.
        self.start_mainloop()

    def start_mainloop(self):

        # Todo: Refactor by using timers.

        # Enter the main loop.
        while True:

            # Feed the watchdog timer to keep the system alive.
            self.device.watchdog.feed()

            # Indicate activity.
            # Todo: Optionally disable this output.
            log.info('--- loop ---')

            # Run downstream mainloop handlers.
            self.loop()

            # Give the system some breath.
            machine.idle()

    def loop(self):
        """
        Main duty cycle loop.
        """

        if not self.settings.get('main.deepsleep', False):
            self.duty_chrono.reset()

        #log.info('Terkin loop')

        # Alternative loop signalling: 1 x blue.
        # https://forum.pycom.io/topic/2067/brightness-of-on-board-led/7
        self.device.blink_led(0x00000b, count=2)

        # Read sensors.
        readings = self.read_sensors()

        # Remember current reading
        self.storage.last_reading = readings

        # Run the garbage collector.
        self.device.run_gc()

        # Transmit data.
        transmission_success = self.transmit_readings(readings)

        # Signal transmission outcome.
        if transmission_success:
            self.device.blink_led(0x00000b)
        else:
            self.device.blink_led(0x0b0000)

        # Run the garbage collector.
        self.device.run_gc()

        # Sleep how ever.
        self.sleep()

    def sleep(self):
        """
        Sleep until the next measurement cycle.
        """

        lightsleep = self.settings.get('main.lightsleep', False)
        deepsleep = self.settings.get('main.deepsleep', False)
        interval = self.get_sleep_time()

        # Amend deep sleep intent when masked through maintenance mode.
        if self.device.status.maintenance is True:
            lightsleep = False
            deepsleep = False
            log.info('Device is in maintenance mode. Skipping deep sleep and '
                     'adjusting interval to {} seconds'.format(interval))

        # Use deep sleep if requested.
        try:
            if deepsleep:

                # Shut down sensor peripherals.
                self.sensor_manager.power_off()

                # Shut down device peripherals.
                self.device.power_off()

            # Send device to deep sleep.
            self.device.hibernate(interval,
                                  lightsleep=lightsleep,
                                  deepsleep=deepsleep)

        # When hibernation fails, fall back to regular "time.sleep".
        except Exception as ex:
            log.exc(ex, 'Failed to hibernate, falling back to regular sleep')
            # Todo: Emit error message here.
            log.info('Sleeping for {} seconds'.format(interval))
            time.sleep(interval)

    def get_sleep_time(self):
        interval = self.settings.get('main.interval', 60.0)

        # Configuration switchover backward compatibility / defaults.
        if isinstance(interval, (float, int)):
            self.settings.set('main.interval', {})
            self.settings.setdefault('main.interval.field', interval)
        self.settings.setdefault('main.interval.maintenance', 5.0)

        # Compute interval.
        interval = self.settings.get('main.interval.field')

        # Amend deep sleep intent when masked through maintenance mode.
        if self.device.status.maintenance is True:
            interval = self.settings.get('main.interval.maintenance')

        # Compute sleeping duration from measurement interval and elapsed time.
        elapsed = self.duty_chrono.read()
        sleep_time = interval - elapsed

        if sleep_time <= 0:
            sleep_time = interval

        return sleep_time

    def register_sensors(self):
        """
        Add system sensors.
        """

        log.info('Registering system sensors')

        system_sensors = [
            SystemMemoryFree,
            SystemTemperature,
            SystemBatteryLevel,
            SystemUptime,
        ]

        for sensor_factory in system_sensors:
            sensor_name = sensor_factory.__name__
            try:
                sensor = sensor_factory()
                if not sensor.enabled():
                    log.info('Sensor %s not enabled, skipping', sensor_name)
                    continue
                if hasattr(sensor, 'setup') and callable(sensor.setup):
                    sensor.setup(self.settings)
                self.sensor_manager.register_sensor(sensor)
            except Exception as ex:
                log.exc(ex, 'Registering system sensor "%s" failed',
                        sensor_name)

        # Add WiFi metrics.
        try:
            self.sensor_manager.register_sensor(
                SystemWiFiMetrics(self.device.networking.wifi_manager.station))
        except Exception as ex:
            log.exc(ex, 'Enabling SystemWiFiMetrics sensor failed')

    def read_sensors(self):
        """
        Read sensors
        """

        # Collect observations.
        data = {}
        richdata = {}

        # Iterate all registered sensors.
        sensors = self.sensor_manager.sensors
        log.info('Reading %s sensor ports', len(sensors))
        for sensor in sensors:

            # Signal sensor reading to user.
            sensorname = sensor.__class__.__name__
            log.info('Reading sensor port "%s"', sensorname)

            # Read sensor port.
            try:

                # Disable garbage collector to guarantee reasonable
                # realtime behavior before invoking sensor reading.
                with gc_disabled():
                    reading = sensor.read()

                # Evaluate sensor outcome.
                if reading is None or reading is AbstractSensor.SENSOR_NOT_INITIALIZED:
                    continue

                # Add sensor reading to observations.
                data.update(reading)

                # Record reading for prettified output.
                self.record_reading(sensor, reading, richdata)

            except Exception as ex:
                # Because of the ``gc_disabled`` context manager used above,
                # the propagation of exceptions has to be tweaked like that.
                log.exc(ex, 'Reading sensor "%s" failed', sensorname)

            # Feed the watchdog.
            self.device.watchdog.feed()

            self.device.run_gc()

        # Debugging: Print sensor data before running telemetry.
        prettify_log = self.settings.get('sensors.prettify_log', False)
        if prettify_log:
            log.info('Sensor data:\n\n%s', ddformat(richdata, indent=11))
        else:
            log.info('Sensor data:  %s', data)

        return data

    def record_reading(self, sensor, reading, richdata):
        for key, value in reading.items():
            richdata[key] = {'value': value}
            if hasattr(sensor,
                       'settings') and 'description' in sensor.settings:
                richdata[key]['description'] = sensor.settings.get(
                    'description')
                # Hack to propagate the correct detail-description to prettified output.
                # TODO: Attach settings directly to its reading, while actually reading it.
                if 'devices' in sensor.settings:
                    for device_settings in sensor.settings['devices']:
                        device_address = device_settings['address'].lower()
                        if device_address in key:
                            if hasattr(sensor, 'get_device_description'):
                                device_description = sensor.get_device_description(
                                    device_address)
                                if device_description:
                                    richdata[key][
                                        'description'] = device_description

    def transmit_readings(self, data):
        """Transmit data"""

        # TODO: Optionally disable telemetry.
        if self.device.telemetry is None:
            log.warning('Telemetry disabled')
            return False

        telemetry_status = self.device.telemetry.transmit(data)
        count_total = len(telemetry_status)
        success = all(telemetry_status.values())

        # Evaluate telemetry status outcome.
        if success:
            log.info('Telemetry status: SUCCESS ({}/{})'.format(
                count_total, count_total))
        else:
            count_failed = len([
                item for item in telemetry_status.values() if item is not True
            ])
            log.warning(
                'Telemetry status: FAILURE. {} out of {} targets failed. '
                'Status: {}'.format(count_failed, count_total,
                                    telemetry_status))

        return success

    def start_buttons(self):

        # RGB-LED: 2
        # POWER-ENABLE: 3
        # SD-Card: 4, 8
        # LTE 19, 20
        # Misc: 13, 14, 9, 23

        # Physical location when looking at the board with the RGB-LED oriented to the top.

        # Location: Left side, 6th pin from top.
        self.button_manager.setup_touchpad('P4',
                                           name='Touch3',
                                           location='Module-Left-Top-6th')

        # Location: Left side, 5th pin from bottom.
        self.button_manager.setup_touchpad('P8',
                                           name='Touch2',
                                           location='Module-Left-Bottom-5th')

        # Location: Right side.
        self.button_manager.setup_touchpad('P23',
                                           name='Touch6',
                                           location='Module-Right-Top-4th')

        # Location: Right side.
        # ValueError: invalid pin for touchpad
        """
Example #4
0
class TerkinDatalogger:
    """ 
    Main class of project.
    Handles loop & sleep, registers sensors, reads their data and stores them.
    Shows up as 'datalogger' in the rest of the program.
    """

    # Application metadata.
    name = 'Terkin MicroPython Datalogger'
    version = __version__

    # For the singleton factory.
    __instance__ = None

    def __init__(self, settings, platform_info: PlatformInfo = None):

        # Reference to the chronometer used for general timekeeping.
        self.duty_chrono = bootloader.duty_chrono

        # Fulfill singleton factory.
        TerkinDatalogger.__instance__ = self

        # Signal startup with first available timestamp.
        log.info('Starting Terkin datalogger')

        # Obtain configuration settings.
        self.settings = TerkinConfiguration()
        self.settings.add(settings)
        self.settings.add_user_file()

        # Configure logging.
        logging_enabled = self.settings.get('main.logging.enabled', False)
        if not logging_enabled:
            log.info('Disabling logging to save bytes')
            logging.disable_logging()

        # Initialize ApplicationInfo object.
        self.application_info = ApplicationInfo(name=self.name,
                                                version=self.version,
                                                settings=self.settings,
                                                application=self,
                                                platform_info=platform_info)

        # Initialize transient storage.
        self.storage = TransientStorage()

        # Initialize device.
        self.device = TerkinDevice(self.application_info)

        # Button manager instance (optional).
        self.button_manager = None

        # Initialize sensor domain.
        self.sensor_manager = SensorManager()

    @staticmethod
    def getInstance(settings=None):
        """Singleton factory.

        :param settings:  (Default value = None)

        """
        if TerkinDatalogger.__instance__ is None:
            if settings is None:
                raise Exception(
                    "Settings are None but instance wasn't created before.")
            else:
                TerkinDatalogger(settings)

        return TerkinDatalogger.__instance__

    def setup(self):
        """ """
        pass

    def start(self):
        """ """

        # Report about wakeup reason and run wakeup tasks.
        self.device.resume()

        # Start the watchdog for sanity.
        self.device.watchdog.start()

        # Configure RGB-LED according to settings.
        self.device.configure_rgb_led()

        # Alternative startup signalling: 2 x green.
        self.device.blink_led(0x000b00, count=2)

        # Free up some memory.
        self.device.run_gc()

        # Turn off LTE modem and Bluetooth as we don't use them yet.
        # TODO: Make this configurable.
        if self.application_info.platform_info.device_name in ['GPy', 'FiPy']:
            self.device.power_off_lte_modem()

        self.device.power_off_bluetooth()

        log.info('Starting %s', self.application_info.fullname)

        # Dump configuration settings.
        log_configuration = self.settings.get('main.logging.configuration',
                                              False)
        if log_configuration:
            self.settings.dump()

        # Disable this if you don't want serial access.
        #self.device.enable_serial()

        # Hello world.
        self.device.print_bootscreen()

        # Start networking and telemetry subsystems.

        # Conditionally start network services and telemetry if networking is available.
        try:
            self.device.start_networking()
        except Exception as ex:
            log.exc(ex, 'Networking subsystem failed')
            self.device.status.networking = False

        self.device.start_telemetry()

        # Todo: Signal readyness by publishing information about the device (Microhomie).
        # e.g. ``self.device.publish_properties()``

        # Setup sensors.
        self.device.watchdog.feed()
        bus_settings = self.settings.get('sensors.busses', [])
        self.sensor_manager.setup_busses(bus_settings)
        self.register_sensors()

        # Ready.
        self.start_mainloop()

    def start_mainloop(self):
        """ """

        # Todo: Refactor by using timers.

        # Enter the main loop.
        while True:

            # Feed the watchdog timer to keep the system alive.
            self.device.watchdog.feed()

            # Indicate activity.
            # Todo: Optionally disable this output.
            log.info('--- loop ---')

            # Run downstream mainloop handlers.
            self.loop()

            # Give the system some breath.
            machine.idle()

    def loop(self):
        """Main duty cycle loop."""

        if not self.settings.get('main.deepsleep', False):
            self.duty_chrono.reset()

        #log.info('Terkin loop')

        # Alternative loop signalling: 1 x blue.
        # https://forum.pycom.io/topic/2067/brightness-of-on-board-led/7
        self.device.blink_led(0x00000b, count=2)

        # Read sensors.
        readings = self.read_sensors()

        # Remember current reading
        self.storage.last_reading = readings.data_in

        # Run the garbage collector.
        self.device.run_gc()

        # Transmit data.
        transmission_success = self.transmit_readings(readings)

        # Signal transmission outcome.
        if transmission_success:
            self.device.blink_led(0x00000b)
        else:
            self.device.blink_led(0x0b0000)

        # Run the garbage collector.
        self.device.run_gc()

        # Sleep how ever.
        self.sleep()

    def sleep(self):
        """Sleep until the next measurement cycle."""

        lightsleep = self.settings.get('main.lightsleep', False)
        deepsleep = self.settings.get('main.deepsleep', False)
        interval = self.get_sleep_time()

        # Amend deep sleep intent when masked through maintenance mode.
        if self.device.status.maintenance is True:
            lightsleep = False
            deepsleep = False
            log.info('Device is in maintenance mode. Skipping deep sleep and '
                     'adjusting interval to {} seconds'.format(interval))

        # Prepare device shutdown.
        try:

            # Shut down sensor peripherals.
            self.sensor_manager.power_off()

            # Shut down networking.
            if deepsleep:
                self.device.networking.stop()

        except Exception as ex:
            log.exc(ex, 'Power off failed')

        # Activate device sleep mode.
        try:
            self.device.hibernate(interval,
                                  lightsleep=lightsleep,
                                  deepsleep=deepsleep)

        # When hibernation fails, fall back to regular "time.sleep".
        except Exception as ex:
            log.exc(ex, 'Failed to hibernate, falling back to regular sleep')
            # Todo: Emit error message here.
            log.info('Sleeping for {} seconds'.format(interval))
            time.sleep(interval)

    def get_sleep_time(self):
        """ """
        import pycom

        interval = self.settings.get('main.interval', 60.0)

        # Configuration switchover backward compatibility / defaults.
        if isinstance(interval, (float, int)):
            self.settings.set('main.interval', {})
            self.settings.setdefault('main.interval.field', interval)
        self.settings.setdefault('main.interval.maintenance', 5.0)

        # First, try to acquire deep sleep interval from NVRAM.
        # This gets used when set from a LoRaWAN downlink message.
        # pycom.nvs_get should return "None" in case of unset key. Instead it throws an error
        try:
            interval_minutes = pycom.nvs_get('deepsleep')
            log.info(
                'Deep sleep interval set to %s minute(s) by LoRaWAN downlink message',
                interval_minutes)
            interval = interval_minutes * 60
        # Otherwise, fall back to original configuration setting.
        except Exception as ex:
            interval = self.settings.get('main.interval.field')

        # Amend deep sleep intent when masked through maintenance mode.
        if self.device.status.maintenance is True:
            interval = self.settings.get('main.interval.maintenance')

        # Compute sleeping duration from measurement interval and elapsed time.
        elapsed = self.duty_chrono.read()
        sleep_time = interval - elapsed

        if sleep_time <= 0:
            sleep_time = interval

        return sleep_time

    def register_sensors(self):
        """
        Configure and register sensor objects.
        There are three types of sensors: system, environment & busses. Only the former two are assigned to the latter (if applicable).
        Definitions are in 'settings.py'.
        The sensor are registered by calling their respective classes from terkin/drivers/
        """

        # Add sensors.
        log.info('Registering sensors')
        sensor_infos = []

        # Get list of system sensors from configuration settings.
        sensor_infos += self.settings.get('sensors.system', [])

        # Get list of environmental sensors from configuration settings.
        sensor_infos += self.settings.get('sensors.environment', [])

        # Backward compatibility for environmental sensors.
        if sensor_infos is None:
            sensor_infos += self.settings.get('sensors.registry',
                                              {}).values() or []

        # Scan sensor definitions, create and register sensor objects.
        for sensor_info in sensor_infos:

            sensor_type = sensor_info.get('type', 'unknown').lower()
            sensor_id = sensor_info.get('id',
                                        sensor_info.get('key', sensor_type))
            description = sensor_info.get('description')

            # Skip sensor if disabled in configuration.
            if sensor_info.get('enabled') is False:
                log.info(
                    'Sensor with id={} and type={} is disabled, skipping registration'
                    .format(sensor_id, sensor_type))
                continue

            # skip WiFi sensor registration when WiFi is disabled
            if sensor_type == 'system.wifi':
                if not self.settings.get('networking.wifi.enabled'):
                    log.info('WiFi is disabled, skipping sensor registration')
                    continue

            # Resolve associated bus object.
            sensor_bus = None
            sensor_bus_name = None
            if 'bus' in sensor_info:
                sensor_info_bus = sensor_info['bus']
                sensor_bus = self.sensor_manager.get_bus_by_name(
                    sensor_info_bus)

                # Skip sensor if associated bus is disabled in configuration.
                if sensor_bus is None:
                    log.info(
                        'Bus {} for sensor with id={} and type={} is disabled, '
                        'skipping registration'.format(sensor_info_bus,
                                                       sensor_id, sensor_type))
                    continue
                sensor_bus_name = sensor_bus.name

            # Human readable sensor address.
            if 'address' in sensor_info:
                sensor_address = hex(sensor_info.get('address'))
            else:
                sensor_address = None

            # Report sensor registration to user.
            message = 'Setting up sensor with with id={} and type={} on bus={} with address={} ' \
                      'described as "{}"'.format(sensor_id, sensor_type, sensor_bus_name, sensor_address, description)
            log.info(message)

            try:

                # Sensor reporting about free system memory.
                if sensor_type == 'system.memfree':
                    sensor_object = SystemMemoryFree(sensor_info)

                # Sensor which reports system temperature.
                elif sensor_type == 'system.temperature':
                    sensor_object = SystemTemperature(sensor_info)

                # Sensor which reports battery voltage.
                elif sensor_type in [
                        'system.voltage.battery', 'system.battery-voltage'
                ]:
                    sensor_object = SystemVoltage(sensor_info)

                # Sensor which reports solar panel voltage.
                elif sensor_type == 'system.voltage.solar':
                    sensor_object = SystemVoltage(sensor_info)

                # Sensor which reports system uptime metrics.
                elif sensor_type == 'system.uptime':
                    sensor_object = SystemUptime(sensor_info)

                # Sensor which reports WiFi metrics.
                elif sensor_type == 'system.wifi':
                    try:
                        sensor_object = SystemWiFiMetrics(
                            sensor_info,
                            self.device.networking.wifi_manager.station)
                    except Exception as ex:
                        log.exc(ex, 'Enabling SystemWiFiMetrics sensor failed')
                        continue

                # Initialize buttons / touch pads.
                elif sensor_type == 'system.touch-buttons':
                    from terkin.sensor.button import ButtonManager
                    self.button_manager = ButtonManager()
                    self.start_buttons()

                # Setup and register HX711 sensors.
                elif sensor_type == 'hx711':
                    sensor_object = HX711Sensor(settings=sensor_info)
                    sensor_object.set_address(
                        sensor_info.get('number',
                                        sensor_info.get('address', 0)))
                    sensor_object.register_pin('dout', sensor_info['pin_dout'])
                    sensor_object.register_pin('pdsck',
                                               sensor_info['pin_pdsck'])
                    sensor_object.register_parameter('scale',
                                                     sensor_info['scale'])
                    sensor_object.register_parameter('offset',
                                                     sensor_info['offset'])
                    sensor_object.register_parameter(
                        'gain', sensor_info.get('gain', 128))

                    # Select driver module. Use "gerber" (vanilla) or "heisenberg" (extended).
                    # hx711_sensor.select_driver('gerber')
                    sensor_object.select_driver('heisenberg')

                    # Start sensor.
                    sensor_object.start()

                # Setup and register DS18X20 sensors.
                elif sensor_type == 'ds18b20':
                    sensor_object = DS18X20Sensor(settings=sensor_info)
                    sensor_object.acquire_bus(sensor_bus)

                    # Start sensor.
                    sensor_object.start()

                # Setup and register BME280 sensors.
                elif sensor_type == 'bme280':

                    sensor_object = BME280Sensor(settings=sensor_info)
                    if 'address' in sensor_info:
                        sensor_object.set_address(sensor_info['address'])
                    sensor_object.acquire_bus(sensor_bus)

                    # Start sensor.
                    sensor_object.start()

                elif sensor_type == 'max17043':

                    sensor_object = MAX17043Sensor(settings=sensor_info)
                    if 'address' in sensor_info:
                        sensor_object.set_address(sensor_info['address'])
                    sensor_object.acquire_bus(sensor_bus)

                    # Start sensor.
                    sensor_object.start()

                else:
                    log.warning(
                        'Sensor with id={} has unknown type, skipping registration. '
                        'Sensor settings:\n{}'.format(sensor_id, sensor_info))
                    continue

                # Register sensor object with sensor manager.
                self.sensor_manager.register_sensor(sensor_object)

            except Exception as ex:
                log.exc(
                    ex,
                    'Setting up sensor with id={} and type={} failed'.format(
                        sensor_id, sensor_type))

            # Clean up memory after creating each sensor object.
            #self.device.run_gc()

    def read_sensors(self) -> DataFrame:
        """
        Read measurements from all sensor objects that have been registered in the sensor_manager.
        Reading is done with the read() function of each respective sensor object.
        """

        # Power up sensor peripherals.
        self.sensor_manager.power_on()

        # Collect observations.
        data = {}
        richdata = {}
        readings = []

        # Iterate all registered sensors.
        sensors = self.sensor_manager.sensors
        log.info('Reading %s sensor ports', len(sensors))
        for sensor in sensors:

            # Signal sensor reading to user.
            sensorname = sensor.__class__.__name__
            log.info('Reading sensor port "%s"', sensorname)

            # Read sensor port.
            try:

                # Disable garbage collector to guarantee reasonable
                # realtime behavior before invoking sensor reading.
                with gc_disabled():
                    sensor_data = sensor.read()

                # Evaluate sensor outcome.
                if sensor_data is None or sensor_data is AbstractSensor.SENSOR_NOT_INITIALIZED:
                    continue

                # Add sensor reading to observations.
                data.update(sensor_data)

                # Record reading for prettified output.
                self.record_reading(sensor, sensor_data, richdata)

                # Capture single sensor reading.
                item = SensorReading()
                item.sensor = sensor
                item.data = sensor_data
                readings.append(item)

            except Exception as ex:
                # Because of the ``gc_disabled`` context manager used above,
                # the propagation of exceptions has to be tweaked like that.
                log.exc(ex, 'Reading sensor "%s" failed', sensorname)

            # Feed the watchdog.
            self.device.watchdog.feed()

            # Clean up memory after reading each sensor object.
            #self.device.run_gc()

        # Debugging: Print sensor data before running telemetry.
        prettify_log = self.settings.get('sensors.prettify_log', False)
        if prettify_log:
            log.info('Sensor data:\n\n%s', ddformat(richdata, indent=11))
        else:
            log.info('Sensor data:  %s', data)

        # Capture all sensor readings.
        result = DataFrame()
        result.readings = readings
        result.data_in = data

        return result

    def record_reading(self, sensor, reading, richdata):
        """

        :param sensor: 
        :param reading: 
        :param richdata: 

        """
        for key, value in reading.items():
            richdata[key] = {'value': value}
            if hasattr(sensor,
                       'settings') and 'description' in sensor.settings:
                richdata[key]['description'] = sensor.settings.get(
                    'description')
                # Hack to propagate the correct detail-description to prettified output.
                # TODO: Attach settings directly to its reading, while actually reading it.
                if 'devices' in sensor.settings:
                    for device_settings in sensor.settings['devices']:
                        device_address = device_settings['address'].lower()
                        if device_address in key:
                            if hasattr(sensor, 'get_device_description'):
                                device_description = sensor.get_device_description(
                                    device_address)
                                if device_description:
                                    richdata[key][
                                        'description'] = device_description

    def transmit_readings(self, dataframe: DataFrame):
        """
        Transmit data

        :param dataframe:

        """

        # TODO: Optionally disable telemetry.
        if self.device.telemetry is None:
            log.warning('Telemetry disabled')
            return False

        telemetry_status = self.device.telemetry.transmit(dataframe)
        count_total = len(telemetry_status)
        success = all(telemetry_status.values())

        # Evaluate telemetry status outcome.
        if success:
            log.info('Telemetry status: SUCCESS ({}/{})'.format(
                count_total, count_total))
        else:
            count_failed = len([
                item for item in telemetry_status.values() if item is not True
            ])
            log.warning(
                'Telemetry status: FAILURE. {} out of {} targets failed. '
                'Status: {}'.format(count_failed, count_total,
                                    telemetry_status))

        return success

    def start_buttons(self):
        """
        Configure ESP32 touchpads.
        """

        # RGB-LED: 2
        # POWER-ENABLE: 3
        # SD-Card: 4, 8
        # LTE 19, 20
        # Misc: 13, 14, 9, 23

        # Physical location when looking at the board with the RGB-LED oriented to the top.

        # Location: Left side, 6th pin from top.
        self.button_manager.setup_touchpad('P4',
                                           name='Touch3',
                                           location='Module-Left-Top-6th')

        # Location: Left side, 5th pin from bottom.
        self.button_manager.setup_touchpad('P8',
                                           name='Touch2',
                                           location='Module-Left-Bottom-5th')

        # Location: Right side.
        self.button_manager.setup_touchpad('P23',
                                           name='Touch6',
                                           location='Module-Right-Top-4th')

        # Location: Right side.
        # ValueError: invalid pin for touchpad
        """