diff --git a/.gitignore b/.gitignore index c1aa232..29b9988 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ # General /_tmp /.upload_cache +*_bak +*_bak[0-9] # Python __pycache__ @@ -18,8 +20,12 @@ __pycache__ *.mpy # Configuration files +/Makefile.env +/src/lightbar_cfg.py /src/webrepl_cfg.py /src/wlan_cfg.py -# FreeCAD backups +# 3D modeling temporary files (FreeCAD backups, Cura projects, G-code files) *.FCStd1 +*.3mf +*.gcode diff --git a/Makefile b/Makefile index 50c24e9..e8cbceb 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,19 @@ -# 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 +# Configuration defaults (please use Makefile.env to override these, see Makefile.env.example) +LIGHTBAR_HOST = 192.168.13.12 +LIGHTBAR_URL = http://$(LIGHTBAR_HOST) +WEBREPL_CLI = webrepl_cli.py +WEBREPL_HOST = $(LIGHTBAR_HOST) +WEBREPL_PASSWORD = ultra-secret-webrepl-password +REPL_TTY_PATH = /dev/ttyUSB0 + +# Include local config +-include Makefile.env # 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)) +SRC_UPLOAD_TARGETS := $(patsubst %.py,$(UPLOAD_CACHE_DIR)/%.py.timestamp,$(wildcard src/*.py src/**/*.py src/**/**/*.py)) LIB_UPLOAD_TARGETS := $(patsubst %.py,$(UPLOAD_CACHE_DIR)/%.py.timestamp,$(wildcard lib/*.py)) # Default target @@ -52,7 +55,7 @@ $(UPLOAD_CACHE_DIR)/%.py.timestamp :: %.py 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 + rshell -p $(REPL_TTY_PATH) mkdir /pyboard/lib /pyboard/lightbar /pyboard/lightbar/endpoints # Clear the .upload_cache directory that contains the last upload timestamps .PHONY: clear-upload-cache clear-upload-cache-src diff --git a/Makefile.env.example b/Makefile.env.example new file mode 100644 index 0000000..7486047 --- /dev/null +++ b/Makefile.env.example @@ -0,0 +1,11 @@ +# This is an example file. +# Copy this to Makefile.env and adjust it to the IP and password of your lightbar. + +# IP address or hostname of your lightbar +LIGHTBAR_HOST := 192.168.13.12 + +# Password for WebREPL as set in webrepl_cfg.py (used to transfer files to the ESP32) +WEBREPL_PASSWORD := ultra-secret-webrepl-password + +# Device path to the serial port of the ESP32 when connected via USB +REPL_TTY_PATH := /dev/ttyUSB0 diff --git a/examples/lightbar_cfg.py b/examples/lightbar_cfg.py new file mode 100644 index 0000000..fbb987a --- /dev/null +++ b/examples/lightbar_cfg.py @@ -0,0 +1,10 @@ +# Example file for Lightbar configuration. Copy to src/lightbar_cfg.py, adjust and upload to the board. + +# Port to start the HTTP server on +SERVER_PORT = 80 + +# Whether the HTTP server should be started in debug mode +SERVER_DEBUG = False + +# Amount of LEDs in the lightbar +NEOPIXEL_COUNT = 28 diff --git a/misc/case/lightbar_case-CaseBottom.stl b/misc/case/lightbar_case-CaseBottom.stl new file mode 100644 index 0000000..d15d0e9 Binary files /dev/null and b/misc/case/lightbar_case-CaseBottom.stl differ diff --git a/misc/case/lightbar_case-CaseTop.stl b/misc/case/lightbar_case-CaseTop.stl new file mode 100644 index 0000000..e0bcbcc Binary files /dev/null and b/misc/case/lightbar_case-CaseTop.stl differ diff --git a/misc/chip_pinout.txt b/misc/chip_pinout.txt new file mode 100644 index 0000000..3b58a34 --- /dev/null +++ b/misc/chip_pinout.txt @@ -0,0 +1,6 @@ +Board: ESP32 NodeMCU + +Pins: +- Neopixels: GPIO26 +- Status LED: GPIO4 +- Button: GPIO21 (against GND) diff --git a/src/boot.py b/src/boot.py index ef06500..8c9ce8f 100644 --- a/src/boot.py +++ b/src/boot.py @@ -1,10 +1,12 @@ # boot.py -- This file is executed on every boot (including wake-boot from deepsleep) -from lightbar import wlan_manager +import uasyncio import webrepl -# Connect to WLAN as defined in wlan_cfg.py -wlan_manager.connect() +from lightbar import wlan_manager + +# Asynchronously connect to WLAN as defined in wlan_cfg.py +uasyncio.create_task(wlan_manager.connect_async()) # Start WebREPL on default port 8266 with password defined in webrepl_cfg.PASS webrepl.start() diff --git a/src/lightbar/frontend.py b/src/lightbar/endpoints/frontend.py similarity index 100% rename from src/lightbar/frontend.py rename to src/lightbar/endpoints/frontend.py diff --git a/src/lightbar/rest_api.py b/src/lightbar/endpoints/rest_api.py similarity index 100% rename from src/lightbar/rest_api.py rename to src/lightbar/endpoints/rest_api.py diff --git a/src/lightbar/lightbar_controller.py b/src/lightbar/lightbar_controller.py new file mode 100644 index 0000000..8f34e1c --- /dev/null +++ b/src/lightbar/lightbar_controller.py @@ -0,0 +1,43 @@ +import uasyncio +from machine import Pin +from neopixel import NeoPixel + +# Constants +NEOPIXEL_PIN = 26 + + +class LightbarController: + """ + Controls the LEDs using the NeoPixel library. + """ + neopixel_count: int + neopixel: NeoPixel + + def __init__(self, *, neopixel_count: int): + print('[LightbarController] Initializing Neopixels') + self.neopixel_count = neopixel_count + self.neopixel = NeoPixel(Pin(NEOPIXEL_PIN), neopixel_count) + + def start(self): + uasyncio.create_task(self.run_loop()) + + async def run_loop(self): + print('[LightbarController] Turning on Neopixels') + np = self.neopixel + np.fill((0, 0, 0)) + + color_set = [ + (255, 0, 0), + (255, 255, 0), + (0, 255, 0), + (0, 255, 255), + (0, 0, 255), + (255, 0, 255), + ] + + while True: + for i in range(self.neopixel_count): + np[i] = color_set[i % len(color_set)] + np.write() + + await uasyncio.sleep(0.5) diff --git a/src/lightbar/app.py b/src/lightbar/lightbar_server.py similarity index 67% rename from src/lightbar/app.py rename to src/lightbar/lightbar_server.py index 8e71b58..eafa0b7 100644 --- a/src/lightbar/app.py +++ b/src/lightbar/lightbar_server.py @@ -2,11 +2,14 @@ import machine import uasyncio from microdot_asyncio import Microdot -from lightbar.frontend import frontend -from lightbar.rest_api import rest_api +from lightbar.endpoints.frontend import frontend +from lightbar.endpoints.rest_api import rest_api -class Lightbar(Microdot): +class LightbarServer(Microdot): + """ + Runs the HTTP server to control the lightbar. + """ def __init__(self): super().__init__() self.mount(frontend) diff --git a/src/lightbar/wlan_manager.py b/src/lightbar/wlan_manager.py index 9b2c0bd..0c8af3c 100644 --- a/src/lightbar/wlan_manager.py +++ b/src/lightbar/wlan_manager.py @@ -1,6 +1,8 @@ -import network import time +import network +from machine import Pin + def connect() -> network.WLAN: """ @@ -19,13 +21,21 @@ def connect() -> network.WLAN: wlan = network.WLAN(network.STA_IF) wlan.active(True) + status_led = Pin(4, Pin.OUT) + status_led.off() + if not wlan.isconnected(): wlan.connect(wlan_cfg.SSID, wlan_cfg.PASSWORD) print('[wlan_manager] Connecting to WLAN "{}" '.format(wlan_cfg.SSID), end='') while not wlan.isconnected(): print('.', end='') - time.sleep(1) + + # Let the LED blink to indicate the connection attempt + status_led.on() + time.sleep(0.5) + status_led.off() + time.sleep(0.5) print('\n[wlan_manager] Connected!') else: print('[wlan_manager] Already connected to WLAN "{}"'.format(wlan.config('essid'))) @@ -35,4 +45,16 @@ def connect() -> network.WLAN: wlan.ifconfig(wlan_cfg.IFCONFIG) print('[wlan_manager] IP config: {}'.format(wlan.ifconfig())) + + # Let the LED blink 2 times quickly to indicate that the WLAN is connected + for _ in range(2): + time.sleep(0.125) + status_led.on() + time.sleep(0.125) + status_led.off() + return wlan + + +async def connect_async(): + connect() diff --git a/src/main.py b/src/main.py index 4ee5017..9375060 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,18 @@ -from lightbar.app import Lightbar +from lightbar.lightbar_controller import LightbarController +from lightbar.lightbar_server import LightbarServer -print('[main] Starting lightbar HTTP server...') -api_server = Lightbar() -api_server.run(port=80, debug=True) +try: + import lightbar_cfg +except Exception as e: + print('[main] Could not import lightbar_cfg.py: {}'.format(str(e))) + raise e + +print('[main] Starting lightbar controller') +lightbar_controller = LightbarController(neopixel_count=lightbar_cfg.NEOPIXEL_COUNT) +lightbar_controller.start() + +print('[main] Starting lightbar HTTP server') +api_server = LightbarServer() +api_server.run(port=lightbar_cfg.SERVER_PORT, debug=lightbar_cfg.SERVER_DEBUG) + +print('[main] HTTP server shut down! End of main')