commit 8cdffa444f0d577bd127e3568f5d5def3470cd30 Author: Mike Cifelli <1836280-mike-cifelli@users.noreply.gitlab.com> Date: Tue Feb 7 08:49:20 2023 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..084ac1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +*.swp diff --git a/install-on-device-fs b/install-on-device-fs new file mode 100755 index 0000000..dcc16f4 --- /dev/null +++ b/install-on-device-fs @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +DEVICES=$(mpremote connect list | grep MicroPython | cut -d " " -f 1) + +if [ -z $DEVICES ] ; then + echo "No MicroPython devices found in FS mode" + exit 1 +fi + +DEVICE=${DEVICES[0]} + +echo "Copying firmware files to ${DEVICE}" + +function create_directory { + echo -n "> creating directory $1" + + RESULT=$(mpremote connect ${DEVICE} mkdir $1) + ERROR=$? + + + if [ $ERROR -eq 0 ] ; then + echo " .. done!" + else + if [[ "$RESULT" == *"EEXIST"* ]] ; then + echo " .. already exists, skipping." + else + echo " .. failed!" + echo "! it looks like this device is already in use - is Thonny running?" + exit 1 + fi + fi +} + +function copy { + for file in $1 + do + echo -n "> copying file $file" + mpremote connect ${DEVICE} cp $file $2 > /dev/null + if [ $? -eq 0 ] ; then + echo " .. done!" + else + echo " .. failed!" + fi + done +} + + +create_directory networking +create_directory sensors +create_directory www + +copy "main.py" : +copy "networking/*.py" :networking/ +copy "sensors/*.py" :sensors/ +copy "www/*" :www/ diff --git a/main.py b/main.py new file mode 100644 index 0000000..4421002 --- /dev/null +++ b/main.py @@ -0,0 +1,88 @@ +import json +import time + +from machine import Pin +from networking import AdafruitIO +from networking import logging +from networking import ntp +from networking import Server +from networking import templates +from networking import util + + +class LedServer(Server): + + def __init__(self): + super().__init__() + self.led = Pin('LED', Pin.OUT) + self.aio = AdafruitIO() + self.ntp_interval_in_seconds = 60 * 60 + self.aio_interval_in_seconds = 60 * 5 + self.ntp_ticks = time.ticks_ms() + self.aio_ticks = time.ticks_ms() + + ntp.sync() + + def cleanup(self): + super().cleanup() + self.led.off() + + def work(self): + super().work() + ticks = time.ticks_ms() + + if util.secondsElapsed(ticks, self.ntp_ticks) > self.ntp_interval_in_seconds: + self.ntp_ticks = ticks + ntp.sync() + + if util.secondsElapsed(ticks, self.aio_ticks) > self.aio_interval_in_seconds: + self.aio_ticks = ticks + self.aio.upload(self.getReading()) + + def handlePath(self, path): + if path == 'index.json': + return self.getJsonData() + elif path == 'light/on': + self.led.on() + return self.getJsonData() + elif path == 'light/off': + self.led.off() + return self.getJsonData() + + return self.getPathData(path) + + def getJsonData(self): + return json.dumps(self.getState()) + + def getPathData(self, path): + if path.endswith('.txt') or path.endswith('.ico'): + with open(f'www/{path}', 'rb') as f: + return f.read() + + return templates.render( + f'www/{path or self.default_path}', + ledStatus=self.ledTextValue(), + ledClass=self.ledCssClass(), + datetime=util.datetime() + ) + + def getReading(self): + return {'timestamp': util.datetimeISO8601(), 'readings': self.getState()} + + def getState(self): + return {'is-led-on': bool(self.led.value())} + + def ledTextValue(self): + return 'ON' if self.led.value() else 'OFF' + + def ledCssClass(self): + return 'led-on' if self.led.value() else 'led-off' + + +def main(): + logging.log_file = 'www/log.txt' + LedServer().run() + + +if __name__ == '__main__': + main() diff --git a/networking/__init__.py b/networking/__init__.py new file mode 100644 index 0000000..126c83a --- /dev/null +++ b/networking/__init__.py @@ -0,0 +1,2 @@ +from .adafruit_io import AdafruitIO +from .server import Server diff --git a/networking/adafruit_io.py b/networking/adafruit_io.py new file mode 100644 index 0000000..46460e7 --- /dev/null +++ b/networking/adafruit_io.py @@ -0,0 +1,68 @@ +import urequests + +from . import logging +from .config import config +from .config import secrets + + +class AdafruitIO: + def __init__(self): + self.nickname = config['adafruit_io_nickname'] + + self.headers = { + 'x-aio-key': secrets['adafruit_io_key'], + 'content-type': 'application/json' + } + + group = config['adafruit_io_group'] + username = secrets['adafruit_io_username'] + self.url = f'https://io.adafruit.com/api/v2/{username}/groups/{group}/data' + + def upload(self, reading): + result = urequests.post( + self.url, + json=self.createPayload(reading), + headers=self.headers + ) + + try: + if result.status_code != 200: + self.logError(result) + return False + + return True + finally: + result.close() + + def createPayload(self, reading): + payload = { + 'created_at': reading['timestamp'], + 'feeds': [] + } + + for key, value in reading['readings'].items(): + payload['feeds'].append({ + 'key': f'{self.nickname}-{key}', + 'value': str(value) + }) + + return payload + + def logError(self, result): + reason = 'unknown' + error_message = self.getErrorMessage(result) + + if result.status_code == 429: + reason = 'rate limited' + elif result.status_code == 422 and error_message.find('data created_at may not be in the future') == 0: + reason = 'future reading' + else: + reason = f'{result.status_code} - {result.reason.decode("utf-8")}' + + logging.debug(f'upload issue: {reason} - "{error_message}"') + + def getErrorMessage(self, result): + try: + return result.json()['error'] + except (TypeError, KeyError): + return '' diff --git a/networking/config.sample.py b/networking/config.sample.py new file mode 100644 index 0000000..430bae6 --- /dev/null +++ b/networking/config.sample.py @@ -0,0 +1,11 @@ +config = { + 'adafruit_io_nickname': '', + 'adafruit_io_group': '' +} + +secrets = { + 'ssid': 'ssid', + 'password': 'password', + 'adafruit_io_username': '', + 'adafruit_io_key': '' +} diff --git a/networking/http.py b/networking/http.py new file mode 100644 index 0000000..15edf03 --- /dev/null +++ b/networking/http.py @@ -0,0 +1,6 @@ +okResponse = 'HTTP/1.1 200 OK\r\ncontent-type: text/html\r\n\r\n'.encode('ascii') +okTextResponse = 'HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\n\r\n'.encode('ascii') +okJsonResponse = 'HTTP/1.1 200 OK\r\ncontent-type: application/json\r\n\r\n'.encode('ascii') +okIconResponse = 'HTTP/1.1 200 OK\r\ncontent-type: image/x-icon\r\n\r\n'.encode('ascii') +notFoundResponse = 'HTTP/1.1 404 Not Found\r\n\r\n'.encode('ascii') +serverErrorResponse = 'HTTP/1.1 500 Internal Server Error\r\n\r\n'.encode('ascii') diff --git a/networking/logging.py b/networking/logging.py new file mode 100644 index 0000000..1415ff0 --- /dev/null +++ b/networking/logging.py @@ -0,0 +1,127 @@ +import machine +import os +import gc + +from . import util + +log_file = 'log.txt' + +LOG_INFO = 0b00001 +LOG_WARNING = 0b00010 +LOG_ERROR = 0b00100 +LOG_DEBUG = 0b01000 +LOG_EXCEPTION = 0b10000 +LOG_ALL = LOG_INFO | LOG_WARNING | LOG_ERROR | LOG_DEBUG | LOG_EXCEPTION + +_logging_types = LOG_ALL + +# the log file will be truncated if it exceeds _log_truncate_at bytes in +# size. the defaults values are designed to limit the log to at most +# three blocks on the Pico +_log_truncate_at = 11 * 1024 +_log_truncate_to = 8 * 1024 + + +def file_size(file): + try: + return os.stat(file)[6] + except OSError: + return None + + +def set_truncate_thresholds(truncate_at, truncate_to): + global _log_truncate_at + global _log_truncate_to + _log_truncate_at = truncate_at + _log_truncate_to = truncate_to + + +def enable_logging_types(types): + global _logging_types + _logging_types = _logging_types | types + + +def disable_logging_types(types): + global _logging_types + _logging_types = _logging_types & ~types + + +# truncates the log file down to a target size while maintaining +# clean line breaks +def truncate(file, target_size): + # get the current size of the log file + size = file_size(file) + + # calculate how many bytes we're aiming to discard + discard = size - target_size + if discard <= 0: + return + + with open(file, 'rb') as infile: + with open(file + '.tmp', 'wb') as outfile: + # skip a bunch of the input file until we've discarded + # at least enough + while discard > 0: + chunk = infile.read(1024) + discard -= len(chunk) + + # try to find a line break nearby to split first chunk on + break_position = max( + chunk.find(b'\n', -discard), # search forward + chunk.rfind(b'\n', -discard) # search backwards + ) + if break_position != -1: # if we found a line break.. + outfile.write(chunk[break_position + 1:]) + + # now copy the rest of the file + while True: + chunk = infile.read(1024) + if not chunk: + break + outfile.write(chunk) + + # delete the old file and replace with the new + os.remove(file) + os.rename(file + '.tmp', file) + + +def log(level, text): + log_entry = '{0} [{1:8} /{2:>4}kB] {3}'.format( + util.datetime(), + level, + round(gc.mem_free() / 1024), + text + ) + + print(log_entry) + + with open(log_file, 'a') as logfile: + logfile.write(log_entry + '\n') + + if _log_truncate_at and file_size(log_file) > _log_truncate_at: + truncate(log_file, _log_truncate_to) + + +def info(*items): + if _logging_types & LOG_INFO: + log('info', ' '.join(map(str, items))) + + +def warn(*items): + if _logging_types & LOG_WARNING: + log('warning', ' '.join(map(str, items))) + + +def error(*items): + if _logging_types & LOG_ERROR: + log('error', ' '.join(map(str, items))) + + +def debug(*items): + if _logging_types & LOG_DEBUG: + log('debug', ' '.join(map(str, items))) + + +def exception(*items): + if _logging_types & LOG_EXCEPTION: + log('exception', ' '.join(map(str, items))) diff --git a/networking/ntp.py b/networking/ntp.py new file mode 100644 index 0000000..c154309 --- /dev/null +++ b/networking/ntp.py @@ -0,0 +1,41 @@ +import machine +import struct +import time +import usocket + +from . import logging +from . import util + + +def sync(): + if fetch(timeout=3): + logging.info(f'time updated to {util.datetime()}') + else: + logging.error(f'failed to update time') + + +def fetch(synch_with_rtc=True, timeout=10): + ntp_host = 'time.cifelli.xyz' + + timestamp = None + try: + query = bytearray(48) + query[0] = 0x1b + address = usocket.getaddrinfo(ntp_host, 123)[0][-1] + socket = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM) + socket.settimeout(timeout) + socket.sendto(query, address) + data = socket.recv(48) + socket.close() + local_epoch = 2208988800 + timestamp = struct.unpack("!I", data[40:44])[0] - local_epoch + timestamp = time.gmtime(timestamp) + except Exception as e: + return None + + if synch_with_rtc: + machine.RTC().datetime(( + timestamp[0], timestamp[1], timestamp[2], timestamp[6], + timestamp[3], timestamp[4], timestamp[5], 0)) + + return timestamp diff --git a/networking/server.py b/networking/server.py new file mode 100644 index 0000000..100c151 --- /dev/null +++ b/networking/server.py @@ -0,0 +1,99 @@ +import io +import select +import socket +import sys + +from . import logging +from . import wifi +from . import http + + +class Server: + + def __init__(self): + self.wlan = wifi.connect() + self.socket = socket.socket() + self.poller = select.poll() + self.default_path = 'index.html' + + def cleanup(self): + self.socket.close() + self.wlan.disconnect() + + def run(self): + try: + addr = self.listen() + self.poller.register(self.socket, select.POLLIN) + + logging.info(f'listening on {addr}') + + while True: + try: + self.serve() + self.work() + except Exception as e: + self.logException(e) + finally: + self.cleanup() + + def logException(self, e): + buf = io.StringIO() + sys.print_exception(e, buf) + logging.debug(f'exception:', buf.getvalue()) + + def listen(self): + addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1] + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind(addr) + self.socket.listen(1) + + return addr + + def serve(self): + evts = self.poller.poll(5000) + + for sock, _evt in evts: + try: + conn, addr = sock.accept() + logging.info(f'client connected from {addr}') + request = conn.recv(1024).decode('utf-8').strip() + + self.handleRequest(conn, request) + except: + conn.write(http.serverErrorResponse) + raise + finally: + conn.close() + + def handleRequest(self, conn, request): + [method, path, _protocol] = request.partition('\n')[0].split() + + logging.info(f'{method} {path}') + + try: + if method == 'GET': + response = self.handlePath(path.strip('/')) + + conn.write(self.getPathContentType(path)) + conn.write(response) + else: + conn.write(http.notFoundResponse) + except OSError: + conn.write(http.notFoundResponse) + + def getPathContentType(self, path): + if path.endswith('.txt'): + return http.okTextResponse + elif path.endswith('.json'): + return http.okJsonResponse + elif path.endswith('.ico'): + return http.okIconResponse + + return http.okResponse + + def handlePath(self, _path): + return '' + + def work(self): + if not self.wlan.isconnected(): + self.wlan = wifi.connect() diff --git a/networking/templates.py b/networking/templates.py new file mode 100644 index 0000000..f2be3d5 --- /dev/null +++ b/networking/templates.py @@ -0,0 +1,41 @@ +def render(template, **kwargs): + [startString, endString] = ['{{', '}}'] + [startLength, endLength] = [len(startString), len(endString)] + + with open(template) as f: + data = f.read() + tokenCaret = 0 + result = '' + isRendering = True + + while isRendering: + start = data.find(startString, tokenCaret) + end = data.find(endString, start) + + isRendering = start != -1 and end != -1 + + if isRendering: + token = data[start + startLength:end].strip() + + result = ( + result + + data[tokenCaret:start] + + replaceToken(token, kwargs) + ) + + tokenCaret = end + endLength + else: + result = result + data[tokenCaret:] + + return result + + +def replaceToken(token, values): + result = str(values[token]) if token in values else '' + result = result.replace('&', '&') + result = result.replace('"', '"') + result = result.replace("'", ''') + result = result.replace('>', '>') + result = result.replace('<', '<') + + return result diff --git a/networking/util.py b/networking/util.py new file mode 100644 index 0000000..a09d88b --- /dev/null +++ b/networking/util.py @@ -0,0 +1,18 @@ +import machine +import time + + +def datetime(): + dt = machine.RTC().datetime() + + return '{0:04d}-{1:02d}-{2:02d} {4:02d}:{5:02d}:{6:02d} UTC'.format(*dt) + + +def datetimeISO8601(): + dt = machine.RTC().datetime() + + return '{0:04d}-{1:02d}-{2:02d}T{4:02d}:{5:02d}:{6:02d}Z'.format(*dt) + + +def secondsElapsed(ticks1, ticks2): + return time.ticks_diff(ticks1, ticks2) / 1000 diff --git a/networking/wifi.py b/networking/wifi.py new file mode 100644 index 0000000..3eccfca --- /dev/null +++ b/networking/wifi.py @@ -0,0 +1,49 @@ +import network +import rp2 +import time + +from . import logging +from .config import secrets + + +class WifiConnectionError(RuntimeError): + pass + + +def connect(): + while True: + try: + return connectToWifi() + except WifiConnectionError as e: + logging.error(e.value) + time.sleep(180) + + +def connectToWifi(): + rp2.country('US') + + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + wlan.config(pm=0xa11140) + wlan.connect(secrets['ssid'], secrets['password']) + + wait_for_connection(wlan) + + logging.info('connected') + logging.info(f'ip = {wlan.ifconfig()[0]}') + + return wlan + + +def wait_for_connection(wlan): + maxWait = 10 + + while maxWait > 0: + if wlan.status() < 0 or wlan.status() >= 3: + break + maxWait -= 1 + logging.info('waiting for connection...') + time.sleep(1) + + if wlan.status() != 3: + raise WifiConnectionError('network connection failed') diff --git a/sensors/__init__.py b/sensors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sensors/mcp9808.py b/sensors/mcp9808.py new file mode 100644 index 0000000..6b3c38c --- /dev/null +++ b/sensors/mcp9808.py @@ -0,0 +1,253 @@ +# Imports +from machine import I2C + + +# Register pointers +REG_CONFIG = const(1) +REG_TEMP_BOUNDARY_UPPER = const(2) +REG_TEMP_BOUNDARY_LOWER = const(3) +REG_TEMP_BOUNDARY_CRITICAL = const(4) +REG_TEMP = const(5) +REG_MANUFACTURER_ID = const(6) +REG_DEVIDE_ID = const(7) +REG_RESOLUTION = const(8) + + +# Sensor resolution values +TEMP_RESOLUTION_MIN = const(0) # +0.5 C, refresh rate 30 ms +TEMP_RESOLUTION_LOW = const(1) # +0.25 C, refresh rate 65 ms +TEMP_RESOLUTION_AVG = const(2) # +0.125 C, refresh rate 130 ms +TEMP_RESOLUTION_MAX = const(3) # +0.0625 C, refresh rate 250 ms [Default] + + +# Alert selectors +ALERT_SELECT_ALL = const(0) # ambient > upper || ambient > critical || ambient < lower [Default] +ALERT_SELECT_CRIT = const(1) # Ambient temp > critical + + +# Alert polarity +ALERT_POLARITY_ALOW = const(0) # Active-low, requires pull-up [Default] +ALERT_POLARITY_AHIGH = const(1) # Active-high + + +# Alert output mode +ALERT_OUTPUT_COMPARATOR = const(0) +ALERT_OUTPUT_INTERRUPT = const(1) + +class MCP9808(object): + """ + This class implements an interface to the MCP9808 temprature sensor from + Microchip. + """ + + def __init__(self, i2c=None, addr=0x18): + """ + Initialize a sensor object on the given I2C bus and accessed by the + given address. + """ + if i2c == None or i2c.__class__ != I2C: + raise ValueError('I2C object needed as argument!') + self._i2c = i2c + self._addr = addr + self._check_device() + + def _send(self, buf): + """ + Sends the given bufer object over I2C to the sensor. + """ + if not isinstance(buf, bytearray): + buf = bytearray([buf]) + + if hasattr(self._i2c, "writeto"): + # Micropython + self._i2c.writeto(self._addr, buf) + elif hasattr(self._i2c, "send"): + # PyBoard Micropython + self._i2c.send(self._addr, buf) + else: + raise Exception("Invalid I2C object. Unknown Micropython/platform?") + + def _recv(self, n): + """ + Read bytes from the sensor using I2C. The byte count must be specified + as an argument. + Returns a bytearray containing the result. + """ + if hasattr(self._i2c, "writeto"): + # Micropython (PyCom) + return self._i2c.readfrom(self._addr, n) + elif hasattr(self._i2c, "send"): + # PyBoard Micropython + return self._i2c.recv(n, self._addr) + else: + raise Exception("Invalid I2C object. Unknown Micropython/platform?") + + def _check_device(self): + """ + Tries to identify the manufacturer and device identifiers. + """ + self._send(REG_MANUFACTURER_ID) + self._m_id = self._recv(2) + if not self._m_id == b'\x00T': + raise Exception("Invalid manufacturer ID: '%s'!" % self._m_id) + self._send(REG_DEVIDE_ID) + self._d_id = self._recv(2) + if not self._d_id == b'\x04\x00': + raise Exception("Invalid device or revision ID: '%s'!" % self._d_id) + + def set_shutdown_mode(self, shdn=True): + """ + Set sensor into shutdown mode to draw less than 1 uA and disable + continous temperature conversion. + """ + if shdn.__class__ != bool: + raise ValueError('Boolean argument needed to set shutdown mode!') + self._send(REG_CONFIG) + cfg = self._recv(2) + b = bytearray() + b.append(REG_CONFIG) + if shdn: + b.append(cfg[0] | 1) + else: + b.append(cfg[0] & ~1) + b.append(cfg[1]) + self._send(b) + + def set_alert_mode(self, enable_alert=True, output_mode=ALERT_OUTPUT_INTERRUPT, polarity=ALERT_POLARITY_ALOW, selector=ALERT_SELECT_ALL): + """ + Set sensor into alert mode with the provided output, + polarity and selector parameters + + If output mode is set to interrupt, a call to acknowledge_alert_irq() + is required to deassert the MCP9808 + """ + if enable_alert.__class__ != bool: + raise ValueError('Boolean argument needed to set alert mode!') + if output_mode not in [ALERT_OUTPUT_COMPARATOR, ALERT_OUTPUT_INTERRUPT]: + raise ValueError("Invalid output mode set.") + if selector not in [ALERT_SELECT_ALL, ALERT_SELECT_CRIT]: + raise ValueError("Invalid alert selector set.") + if polarity not in [ALERT_POLARITY_ALOW, ALERT_POLARITY_AHIGH]: + raise ValueError("Invalid alert polarity set.") + + enable_alert = 1 if enable_alert else 0 + self._send(REG_CONFIG) + cfg = self._recv(2) + + alert_bits = (output_mode | (polarity << 1) | (selector << 2) | (enable_alert << 3)) & 0xF + lsb_data = (cfg[1] & 0xF0) | alert_bits + + b = bytearray() + b.append(REG_CONFIG) + b.append(cfg[0]) + b.append(lsb_data) + self._send(b) + + def acknowledge_alert_irq(self): + """ + Must be called if MCP9808 is operating in interrupt output mode + """ + self._send(REG_CONFIG) + cfg = self._recv(2) + b = bytearray() + b.append(REG_CONFIG) + b.append(cfg[0]) # MSB data + b.append(cfg[1] | 0x20) # LSB data with interrupt clear bit set + self._send(b) + + def set_alert_boundary_temp(self, boundary_register, value): + """ + Sets the alert boundary for the requested boundary register + """ + if boundary_register not in [REG_TEMP_BOUNDARY_LOWER, REG_TEMP_BOUNDARY_UPPER, REG_TEMP_BOUNDARY_CRITICAL]: + raise ValueError("Given alert boundary register is not valid!") + if value < -128 or value > 127: # 8 bit two's complement + raise ValueError("Temperature out of range [-128, 127]") + + integral = int(value) + frac = abs(value - integral) + if integral < 0: + integral = (1 << 9) + integral + integral = ((integral & 0x1FF) << 4) + frac = (((1 if frac * 2 >= 1 else 0) << 1) + (1 if (frac * 2 - int(frac * 2)) * 2 >= 1 else 0)) << 2 + twos_value = (integral + frac if value >= 0 else integral - frac) & 0x1ffc + b = bytearray() + b.append(boundary_register) + b.append((twos_value & 0xFF00) >> 8) + b.append(twos_value & 0xFF) + self._send(b) + + + def set_resolution(self, r): + """ + Sets the temperature resolution. + """ + if r not in [TEMP_RESOLUTION_MIN, TEMP_RESOLUTION_LOW, TEMP_RESOLUTION_AVG, TEMP_RESOLUTION_MAX]: + raise ValueError('Invalid temperature resolution requested!') + b = bytearray() + b.append(REG_RESOLUTION) + b.append(r) + self._send(b) + + def get_temp(self): + """ + Read temperature in degree celsius and return float value. + """ + self._send(REG_TEMP) + raw = self._recv(2) + u = (raw[0] & 0x0f) << 4 + l = raw[1] / 16 + if raw[0] & 0x10 == 0x10: + temp = (u + l) - 256 + else: + temp = u + l + return temp + + def get_temp_int(self): + """ + Read a temperature in degree celsius and return a tuple of two parts. + The first part is the decimal part and the second the fractional part + of the value. + This method does avoid floating point arithmetic completely to support + platforms missing float support. + """ + self._send(REG_TEMP) + raw = self._recv(2) + u = (raw[0] & 0xf) << 4 + l = raw[1] >> 4 + if raw[0] & 0x10 == 0x10: + temp = (u + l) - 256 + frac = -((raw[1] & 0x0f) * 100 >> 4) + else: + temp = u + l + frac = (raw[1] & 0x0f) * 100 >> 4 + return temp, frac + + def _debug_config(self, cfg=None): + """ + Prints the first 9 bits of the config register mapped to human + readable descriptions + """ + if not cfg: + self._send(REG_CONFIG) + cfg = self._recv(2) + + # meanings[a][b] with a the bit index (LSB order), + # b=0 the config description and b={bit value}+1 the value description + meanings = [ + ["Alert output mode", "Comparator", "Interrupt"], + ["Alert polarity", "Active-low", "Active-high"], + ["Alert Selector", "All", "Only Critical"], + ["Alert enabled", "False", "True"], + ["Alert status", "Not asserted", "Asserted as set by mode"], + ["Interrupt clear bit", "0", "1"], + ["Window [low, high] locked", "Unlocked", "Locked"], + ["Critical locked", "Unlocked", "Locked"], + ["Shutdown", "False", "True"] + ] + + print("Raw config: {}".format(str(cfg))) + for i in range(0, min(len(meanings), len(cfg)*8)): + part = 0 if i > 7 else 1 + value = 1 if (cfg[part] & (2**(i % 8))) > 0 else 0 + print(meanings[i][0] + ": " + meanings[i][1 + value]) diff --git a/sensors/watersensor.py b/sensors/watersensor.py new file mode 100644 index 0000000..2485bfa --- /dev/null +++ b/sensors/watersensor.py @@ -0,0 +1,18 @@ +import machine + + +class WaterSensor: + + def __init__(self, pin): + self.waterSensor = machine.ADC(pin) + self.reading = self.waterSensor.read_u16() + self.threshold = 2000 + + def takeReading(self): + self.reading = self.waterSensor.read_u16() + + def isWaterPresent(self): + return self.reading > self.threshold + + def waterStatus(self): + return 'Present' if self.isWaterPresent() else 'Absent' diff --git a/www/favicon.ico b/www/favicon.ico new file mode 100644 index 0000000..a99fefe Binary files /dev/null and b/www/favicon.ico differ diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..a2a1c46 --- /dev/null +++ b/www/index.html @@ -0,0 +1,40 @@ + + + + Pico W + + + + + +

Pico W

+

{{datetime}}

+

Led is {{ledStatus}}

+ + +