Initial commit
This commit is contained in:
commit
9d032a29d2
|
@ -0,0 +1,2 @@
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Mike Cifelli
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,3 @@
|
||||||
|
# aquifer
|
||||||
|
|
||||||
|
A Raspberry Pi Pico W project for marshalling a car into a garage.
|
|
@ -0,0 +1,54 @@
|
||||||
|
#!/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 net
|
||||||
|
create_directory sensors
|
||||||
|
create_directory www
|
||||||
|
|
||||||
|
copy "main.py" :
|
||||||
|
copy "net/*.py" :net/
|
||||||
|
copy "www/*" :www/
|
|
@ -0,0 +1,139 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
from machine import Pin
|
||||||
|
from machine import ADC
|
||||||
|
from net import logging
|
||||||
|
from net import ntp
|
||||||
|
from net import Server
|
||||||
|
from net import templates
|
||||||
|
from net import util
|
||||||
|
from net.config import config
|
||||||
|
|
||||||
|
|
||||||
|
class Marshaller(Server):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.switch = Pin(8, Pin.IN, Pin.PULL_UP)
|
||||||
|
self.red_led = Pin(11, Pin.OUT)
|
||||||
|
self.blue_led = Pin(13, Pin.OUT)
|
||||||
|
self.green_led = Pin(20, Pin.OUT)
|
||||||
|
self.yellow_led = Pin(18, Pin.OUT)
|
||||||
|
self.v = Pin(27, Pin.OUT)
|
||||||
|
self.a = ADC(2)
|
||||||
|
self.last_value = -1
|
||||||
|
|
||||||
|
self.v.on()
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.last_http_activation_ticks = time.ticks_ms()
|
||||||
|
self.http_activation_interval_in_seconds = 2 * 60
|
||||||
|
self.is_http_activation = False
|
||||||
|
self.ntp_interval_in_seconds = 3 * 60 * 60
|
||||||
|
self.ntp_ticks = time.ticks_ms()
|
||||||
|
self.distance_interval_in_milliseconds = 50
|
||||||
|
self.distance_ticks = time.ticks_ms()
|
||||||
|
self.isWaterPresent = False
|
||||||
|
|
||||||
|
ntp.sync()
|
||||||
|
|
||||||
|
def work(self):
|
||||||
|
ticks = time.ticks_ms()
|
||||||
|
|
||||||
|
if self.is_scanning():
|
||||||
|
self.v.on()
|
||||||
|
if util.millisecondsElapsed(ticks, self.distance_ticks) > self.distance_interval_in_milliseconds:
|
||||||
|
self.distance_ticks = ticks
|
||||||
|
self.show_color()
|
||||||
|
|
||||||
|
if self.is_http_activation and util.secondsElapsed(ticks, self.last_http_activation_ticks) > self.http_activation_interval_in_seconds:
|
||||||
|
self.is_http_activation = False
|
||||||
|
else:
|
||||||
|
self.v.off()
|
||||||
|
self.leds_off()
|
||||||
|
super().work()
|
||||||
|
|
||||||
|
if util.secondsElapsed(ticks, self.ntp_ticks) > self.ntp_interval_in_seconds:
|
||||||
|
self.ntp_ticks = ticks
|
||||||
|
ntp.sync()
|
||||||
|
|
||||||
|
def is_scanning(self):
|
||||||
|
return self.switch.value() == 0 or self.is_http_activation
|
||||||
|
|
||||||
|
def get_buffered_distance_in_inches(self):
|
||||||
|
distance = self.get_distance_in_inches()
|
||||||
|
|
||||||
|
if abs(distance - self.last_value) > 0.25:
|
||||||
|
self.last_value = distance
|
||||||
|
return distance
|
||||||
|
|
||||||
|
return self.last_value
|
||||||
|
|
||||||
|
# TODO - don't convert distances to inches, convert thresholds in the opposite direction
|
||||||
|
def get_distance_in_inches(self):
|
||||||
|
return self.a.read_u16() / 65535 * 1024 * 5 * 0.03937008
|
||||||
|
|
||||||
|
def handlePath(self, path):
|
||||||
|
if (path == 'on'):
|
||||||
|
return self.http_activation()
|
||||||
|
|
||||||
|
return self.getPathData(path)
|
||||||
|
|
||||||
|
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}',
|
||||||
|
hostname=config['hostname'],
|
||||||
|
datetime=util.datetime(),
|
||||||
|
is_active=self.is_scanning()
|
||||||
|
)
|
||||||
|
|
||||||
|
def http_activation(self):
|
||||||
|
self.is_http_activation = True
|
||||||
|
self.last_http_activation_ticks = time.ticks_ms()
|
||||||
|
|
||||||
|
# todo - no content status
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def show_color(self):
|
||||||
|
distance_in_inches = self.get_buffered_distance_in_inches()
|
||||||
|
|
||||||
|
if distance_in_inches < 15:
|
||||||
|
self.leds_off()
|
||||||
|
self.red_led.on()
|
||||||
|
elif distance_in_inches < 20:
|
||||||
|
self.leds_off()
|
||||||
|
self.green_led.on()
|
||||||
|
self.yellow_led.on()
|
||||||
|
elif distance_in_inches < 25:
|
||||||
|
self.leds_off()
|
||||||
|
self.green_led.on()
|
||||||
|
elif distance_in_inches < 40:
|
||||||
|
self.leds_off()
|
||||||
|
self.yellow_led.on()
|
||||||
|
else:
|
||||||
|
self.leds_off()
|
||||||
|
self.blue_led.on()
|
||||||
|
|
||||||
|
def leds_off(self):
|
||||||
|
self.red_led.off()
|
||||||
|
self.blue_led.off()
|
||||||
|
self.green_led.off()
|
||||||
|
self.yellow_led.off()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.v.off()
|
||||||
|
self.leds_off()
|
||||||
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.log_file = 'www/log.txt'
|
||||||
|
Marshaller().run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1 @@
|
||||||
|
from .server import Server
|
|
@ -0,0 +1,8 @@
|
||||||
|
config = {
|
||||||
|
'hostname': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = {
|
||||||
|
'ssid': 'ssid',
|
||||||
|
'password': 'password'
|
||||||
|
}
|
|
@ -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', 0, -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(500)
|
||||||
|
|
||||||
|
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,21 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
def millisecondsElapsed(ticks1, ticks2):
|
||||||
|
return time.ticks_diff(ticks1, ticks2)
|
|
@ -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')
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,22 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{{hostname}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 20px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #333;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>{{hostname}}</h2>
|
||||||
|
<p>{{datetime}}</p>
|
||||||
|
<p>Active: {{is_active}}</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in New Issue