diff --git a/.gitignore b/.gitignore index 8d76d4b..e312f63 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # General /_tmp +/.upload_cache # Python __pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50c24e9 --- /dev/null +++ b/Makefile @@ -0,0 +1,67 @@ +# Configuration +LIGHTBAR_HOST ?= 192.168.17.22 +LIGHTBAR_URL := http://$(LIGHTBAR_HOST) +WEBREPL_CLI := webrepl_cli.py +WEBREPL_HOST := $(LIGHTBAR_HOST) +WEBREPL_PASSWORD ?= acab +REPL_TTY_PATH ?= /dev/ttyUSB0 + +# Directory for saving timestamps of when files were last uploaded +UPLOAD_CACHE_DIR := .upload_cache + +# Auto-detect all .py files that should be uploaded +SRC_UPLOAD_TARGETS := $(patsubst %.py,$(UPLOAD_CACHE_DIR)/%.py.timestamp,$(wildcard src/*.py src/**/*.py)) +LIB_UPLOAD_TARGETS := $(patsubst %.py,$(UPLOAD_CACHE_DIR)/%.py.timestamp,$(wildcard lib/*.py)) + +# Default target +.DEFAULT_GOAL := upload-reboot + +# -- Device control + +# Reboot the ESP32 via REST API +.PHONY: reboot +reboot: + curl -X POST $(LIGHTBAR_URL)/api/reboot + +# Shutdown the HTTP server via REST API, allowing access to the WebREPL +.PHONY: shutdown +shutdown: + curl -X POST $(LIGHTBAR_URL)/api/shutdown + +# -- Deployment + +# Upload all files (or only /src, or only /lib) to the ESP32 via WebREPL +.PHONY: upload upload-all upload-src upload-lib +upload: upload-all +upload-all: upload-src upload-lib +upload-src: $(SRC_UPLOAD_TARGETS) +upload-lib: $(LIB_UPLOAD_TARGETS) + +# Upload all files via WebREPL and reboot +.PHONY: upload-reboot +upload-reboot: upload-all reboot + +# Pattern rules for uploading files to the ESP32 via WebREPL if they were changed since the last upload +$(UPLOAD_CACHE_DIR)/%.py.timestamp :: %.py + $(WEBREPL_CLI) -p $(WEBREPL_PASSWORD) $*.py $(WEBREPL_HOST):/$(subst src/,,$*).py >/dev/null + @mkdir -p $(@D) && touch $@ + +# Create necessary directories on the ESP32 filesystem via REPL (not WebREPL!) +# TODO: This cannot be done via webrepl_cli.py, so you either need to use REPL over USB, or create the directories manually +.PHONY: repl-create-directories +repl-create-directories: + @echo "(Note: If the directory already exists, rshell will print \"Unable to create [DIR]\")" + @echo + rshell -p $(REPL_TTY_PATH) mkdir /pyboard/lib /pyboard/lightbar + +# Clear the .upload_cache directory that contains the last upload timestamps +.PHONY: clear-upload-cache clear-upload-cache-src +clear-upload-cache-all: + rm -rf $(UPLOAD_CACHE_DIR) +clear-upload-cache-src: + rm -rf $(UPLOAD_CACHE_DIR)/src/ + +# Clear the upload timestamp cache and re-upload all files +.PHONY: reupload-all reupload-src +reupload-all: clear-upload-cache-all upload-all +reupload-src: clear-upload-cache-src upload-src diff --git a/examples/webrepl_cfg.py b/examples/webrepl_cfg.py new file mode 100644 index 0000000..79c8566 --- /dev/null +++ b/examples/webrepl_cfg.py @@ -0,0 +1,4 @@ +# Example file for WebREPL configuration. Copy to src/webrepl_cfg.py, adjust and upload to the board. + +# Define the password for WebREPL here +PASS = 'ultra-secret-webrepl-password' diff --git a/src/wlan_cfg.example.py b/examples/wlan_cfg.py similarity index 73% rename from src/wlan_cfg.example.py rename to examples/wlan_cfg.py index 61b0e5e..0e238c8 100644 --- a/src/wlan_cfg.example.py +++ b/examples/wlan_cfg.py @@ -1,4 +1,4 @@ -# Example file for WLAN configuration. Copy to wlan_cfg.py and adjust! +# Example file for WLAN configuration. Copy to src/wlan_cfg.py, adjust and upload to the board. SSID = 'Example SSID' PASSWORD = 'ultra secret wlan password' diff --git a/src/lib/microdot.py b/lib/microdot.py similarity index 100% rename from src/lib/microdot.py rename to lib/microdot.py diff --git a/src/lib/microdot_asyncio.py b/lib/microdot_asyncio.py similarity index 100% rename from src/lib/microdot_asyncio.py rename to lib/microdot_asyncio.py diff --git a/src/lightbar/app.py b/src/lightbar/app.py new file mode 100644 index 0000000..8e71b58 --- /dev/null +++ b/src/lightbar/app.py @@ -0,0 +1,22 @@ +import machine +import uasyncio +from microdot_asyncio import Microdot + +from lightbar.frontend import frontend +from lightbar.rest_api import rest_api + + +class Lightbar(Microdot): + def __init__(self): + super().__init__() + self.mount(frontend) + self.mount(rest_api, url_prefix='/api') + + async def scheduled_shutdown(self): + await uasyncio.sleep(0.1) + self.shutdown() + + @staticmethod + async def scheduled_reboot(): + await uasyncio.sleep(0.1) + machine.reset() diff --git a/src/lightbar/frontend.py b/src/lightbar/frontend.py new file mode 100644 index 0000000..04a364a --- /dev/null +++ b/src/lightbar/frontend.py @@ -0,0 +1,8 @@ +from microdot_asyncio import Microdot + +frontend = Microdot() + + +@frontend.get('') +async def handle_index(_request): + return 'Meow, world :3\n' diff --git a/src/lightbar/rest_api.py b/src/lightbar/rest_api.py new file mode 100644 index 0000000..8178865 --- /dev/null +++ b/src/lightbar/rest_api.py @@ -0,0 +1,17 @@ +import uasyncio +from microdot_asyncio import Microdot + +rest_api = Microdot() + + +# TODO -- all of these responses aren't REST yet +@rest_api.post('/shutdown') +async def shutdown(request): + uasyncio.create_task(request.app.scheduled_shutdown()) + return 'Shutting down!\n' + + +@rest_api.post('/reboot') +async def reboot(request): + uasyncio.create_task(request.app.scheduled_reboot()) + return 'Rebooting now!\n' diff --git a/src/main.py b/src/main.py index bbab886..4ee5017 100644 --- a/src/main.py +++ b/src/main.py @@ -1,12 +1,5 @@ -from microdot_asyncio import Microdot +from lightbar.app import Lightbar -app = Microdot() - - -@app.route('/') -async def index(request): - return 'Meow, world\n' - - -print('[main] Running Microdot app...') -app.run(port=80) +print('[main] Starting lightbar HTTP server...') +api_server = Lightbar() +api_server.run(port=80, debug=True) diff --git a/src/webrepl_cfg.example.py b/src/webrepl_cfg.example.py deleted file mode 100644 index 6b1358a..0000000 --- a/src/webrepl_cfg.example.py +++ /dev/null @@ -1,2 +0,0 @@ -# Define the password for WebREPL here -PASS = 'ultra-secret-webrepl-password'