Initial commit
This commit is contained in:
commit
8cdffa444f
|
@ -0,0 +1,2 @@
|
|||
.vscode/
|
||||
*.swp
|
|
@ -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/
|
|
@ -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()
|
|
@ -0,0 +1,2 @@
|
|||
from .adafruit_io import AdafruitIO
|
||||
from .server import Server
|
|
@ -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 ''
|
|
@ -0,0 +1,11 @@
|
|||
config = {
|
||||
'adafruit_io_nickname': '',
|
||||
'adafruit_io_group': ''
|
||||
}
|
||||
|
||||
secrets = {
|
||||
'ssid': 'ssid',
|
||||
'password': 'password',
|
||||
'adafruit_io_username': '',
|
||||
'adafruit_io_key': ''
|
||||
}
|
|
@ -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')
|
|
@ -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)))
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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')
|
|
@ -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])
|
|
@ -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'
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,40 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>Pico W</title>
|
||||
<meta http-equiv="refresh" content="500">
|
||||
<style>
|
||||
body {
|
||||
margin: 20px;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
background-color: #333;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.led-on {
|
||||
color: greenyellow;
|
||||
}
|
||||
|
||||
.led-off {
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
.water-present {
|
||||
color: pink;
|
||||
}
|
||||
|
||||
.water-absent {
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Pico W</h2>
|
||||
<p>{{datetime}}</p>
|
||||
<p>Led is <span class="{{ledClass}}">{{ledStatus}}</span></p>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue