Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MSB State Time Button

An ESP32-based device for displaying and setting the Makerspace Bonn open/closed status. Features an OLED display showing current time and status, with a rotary encoder for setting closing times.

Features

  • Status Display: Shows current time and makerspace open/closed status
  • Time Setting: Use rotary encoder to select closing time in 15-minute increments
  • MQTT Integration: Receives real-time status updates via MQTT
  • NTP Time Sync: Automatic time synchronization with DST support (CET/CEST)
  • Screensaver: Bouncing logo animation with configurable timeout to prevent OLED burn-in
  • Auto-Reconnect: WiFi and MQTT reconnection with keepalive pings and a receive watchdog, so a silently dead broker connection is detected instead of freezing the status
  • State Freshness: Status older than STATE_MAX_AGE is reported as unknown (?) rather than shown as current
  • Configurable Brightness: Separate brightness levels for init, normal, and screensaver modes
  • Logging System: Centralized logging with configurable levels (DEBUG/INFO/WARN/ERROR)

Hardware

Component Specification
Microcontroller ESP32
Display SH1106 128x64 OLED (I2C)
Input Rotary encoder with push button

Pin Configuration

Function GPIO Pin
I2C SCL 7
I2C SDA 6
Button 8
Rotary A 2
Rotary B 1

Installation

Everything below runs from the command line. Two tools are involved and they do different jobs:

Tool Job
esptool Writes the MicroPython firmware to flash. Needed once per board.
mpremote Copies application files onto a board that already runs MicroPython, and opens the REPL. This is the day-to-day tool.
# mpremote - into the project venv, so it matches the repo
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

# esptool - only needed to flash firmware onto a fresh board
brew install esptool          # or: .venv/bin/pip install esptool

1. Find the port

.venv/bin/mpremote devs

The board appears as an Espressif USB JTAG/serial device:

/dev/cu.usbmodem2101 aa:bb:cc:dd:ee:ff 303a:1001 Espressif USB JTAG/serial debug unit

Use that path as <PORT> below. On Linux it is usually /dev/ttyACM0 or /dev/ttyUSB0. mpremote also accepts the shorthand a0, a1, ... for /dev/ttyACM* and u0, u1, ... for /dev/ttyUSB*.

2. Flash MicroPython (first time per board only)

Skip this if the board already runs MicroPython - mpremote devs listing it is a good sign that it does.

Download a firmware build for your board from micropython.org/download - this project runs on a LOLIN C3 MINI (ESP32-C3FH4), so LOLIN_C3_MINI is the right build. Then:

esptool --chip esp32c3 --port <PORT> erase-flash
esptool --chip esp32c3 --port <PORT> --baud 460800 \
        write-flash -z 0x0 LOLIN_C3_MINI-<version>.bin

Notes:

  • The offset is 0x0 on ESP32-C3, not the 0x1000 used by the classic ESP32. Getting this wrong produces a board that boots to nothing.
  • Subcommands are spelled with dashes on esptool v5 (write-flash). Older v4 releases use underscores (write_flash); v5 still accepts those with a deprecation warning.
  • erase-flash wipes secrets.py along with everything else, so re-upload the whole application afterwards.
  • If esptool cannot reset the board into its bootloader, hold BOOT, tap RESET, release BOOT, then re-run the command.

3. Create secrets.py

Create src/secrets.py with your credentials. It is gitignored and never committed:

wifi_access = {
    "YourSSID": "YourPassword",
    # Add multiple networks for fallback
}

mqtt_server = "your.mqtt.server"
mqtt_user = "username"
mqtt_pass = "password"

API_key = "your-api-key"

4. Upload the application

The device has a flat filesystem - files live at the root, not under src/, so copy the contents of src/ to ::

.venv/bin/mpremote connect <PORT> cp src/*.py src/*.pbm src/*.pf :

mpremote compares hashes and prints Up to date for files it skips, so re-running this is cheap and it is safe to use for incremental updates.

The glob deliberately lists the three extensions rather than using cp -r src/., which would drag __pycache__ onto the device.

To push only what you changed:

.venv/bin/mpremote connect <PORT> cp src/mqtt_service.py src/main.py :

5. Run it

Uploading interrupts the running program, so reset when you are done:

.venv/bin/mpremote connect <PORT> reset

Configuration

Settings are defined at the top of main.py:

# Logging
LOG_LEVEL = 1  # 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR

# Screensaver
SCREENSAVER_TIMEOUT = 300  # Seconds of inactivity (300 = 5 min)

# How long to wait for the broker to confirm a closing time we just set
REQUEST_CONFIRM_TIMEOUT = 10

# Display brightness (0-255)
BRIGHTNESS_INIT = 50        # During startup
BRIGHTNESS_NORMAL = 200     # Normal operation
BRIGHTNESS_SCREENSAVER = 5  # Screensaver mode

MQTT connection health is tuned in mqtt_service.py:

KEEPALIVE = 60       # Broker keepalive announced at CONNECT
PING_INTERVAL = 20   # Send PINGREQ this often - the device only subscribes,
                     # so without pings the broker drops it after ~90s
RX_TIMEOUT = 90      # No inbound bytes at all for this long = dead socket
STATE_MAX_AGE = 23 * 60 * 60  # Received state is trusted for this long (23h)
READ_TIMEOUT = 5     # Ceiling on any blocking socket read

Why state expiry is measured in hours, not minutes

msb/state is published on change and retained by the broker, so the topic is quiet for long stretches - a device that has heard nothing for an hour is almost certainly just looking at a space whose status has not changed. Liveness is the job of PING_INTERVAL and RX_TIMEOUT, which watch the connection; STATE_MAX_AGE only exists so that a status we can no longer refresh at all eventually stops being presented as current. Setting it to minutes would show ? nearly all the time.

Why the pings matter

The device subscribes to msb/state and never publishes. Without a periodic PINGREQ it sends the broker nothing, so the broker closes the connection once the keepalive lapses. Worse, umqtt.simple.check_msg() cannot tell a quiet connection from a dead one - a half-open socket just returns None forever and never raises. The result was a display stuck on hours-old status. RxWatchSocket wraps the live socket to make inbound bytes (including PINGRESP) visible, which is what lets the watchdog fire.

Usage

Normal Mode

  • Display shows current time and makerspace status (open/closed)
  • If open, shows closing time
  • Shows ? when no recent status has been received, rather than a stale value

Setting Closing Time

  1. Turn rotary encoder to select desired closing time
  2. Press button to confirm
  3. Device sends new closing time to the API
  4. Display returns to normal once MQTT reports that closing time, or after REQUEST_CONFIRM_TIMEOUT seconds if no matching confirmation arrives

Screensaver

  • Activates after configurable timeout (default 5 minutes)
  • Shows bouncing MSB logo with lock status icon
  • Reduced brightness to save power and reduce burn-in
  • Any input (rotation or button press) wakes the display

Development

# Interactive REPL - Ctrl-] to exit, Ctrl-C to interrupt the running program
.venv/bin/mpremote connect <PORT> repl

# List files on the device
.venv/bin/mpremote connect <PORT> fs ls

# Pull a file back off the device
.venv/bin/mpremote connect <PORT> cp :main.py ./main-from-device.py

# Run a local file on the device without installing it
.venv/bin/mpremote connect <PORT> run src/main.py

# One-off expression against the device
.venv/bin/mpremote connect <PORT> exec "import os; print(os.listdir())"

# Host-side unit tests (no hardware needed)
./test.sh

Watching the log

mpremote repl shows the log output live. For a non-interactive capture - useful for watching a boot sequence end to end - read the serial port directly:

.venv/bin/mpremote connect <PORT> reset
.venv/bin/python - <<'EOF'
import serial, time
ser = serial.Serial('<PORT>', 115200, timeout=0.5)
start = time.time()
while time.time() - start < 120:
    line = ser.readline()
    if line:
        print("[%6.1fs] %s" % (time.time() - start,
                               line.decode('utf-8', 'replace').rstrip()))
EOF

A hard reset makes the native-USB C3 drop off the bus and re-enumerate, so give the port a moment (or retry the open) before reading.

The serial module used above is pyserial, which mpremote already pulls in, so it is present in the venv without a separate install.

Gotchas

  • exec "import main" does not inspect the running program - it starts a second one. MicroPython executes main.py as a script at boot rather than importing it, so importing it from the REPL re-runs the whole thing, infinite loop included, and hangs your session. To inspect service internals, build a throwaway instance in the REPL instead:
    .venv/bin/mpremote connect <PORT> exec "
    import secrets
    from mqtt_service import MQTTService
    s = MQTTService(secrets.mqtt_server, secrets.mqtt_user, secrets.mqtt_pass, 'probe')
    print(s.connect_and_subscribe(), s.rx_watch_active)
    "
  • Only one program can hold the port. If mpremote reports the device is busy, close any REPL or serial monitor still attached.
  • [STATUS] lines only print in normal mode, so they stop once the screensaver kicks in. Turn the encoder to wake it if you are watching for them.

Project Structure

src/
├── main.py              # Main application loop and configuration
├── logger.py            # Centralized logging module
├── MSBDisplay.py        # Display rendering (status, screensaver)
├── mqtt_service.py      # MQTT client with auto-reconnect
├── wifi_manager.py      # WiFi connection management
├── state_manager.py     # API communication
├── button_handler.py    # Button input with debouncing
├── rotary_irq_esp.py    # Rotary encoder driver
├── enhanced_display.py  # Extended display functions
├── sh1106.py            # SH1106 OLED driver
├── packed_font.py       # Custom font rendering
├── *.pbm                # Bitmap images (logo, icons)
├── *.pf                 # Packed font files
└── secrets.py           # Credentials (not in repo)

Dependencies

On the device:

  • MicroPython (ESP32 port) - developed against 1.25
  • umqtt.simple (included in MicroPython)
  • urequests (included in MicroPython)

On the host, for flashing and uploading:

License

MIT License

About

ESP32 MicroPython device for displaying and setting makerspace open/closed status on OLED display

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages