More AI slop
This commit is contained in:
52
firmware/esp32c3-python/README.md
Normal file
52
firmware/esp32c3-python/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# MicroPython variant for ESP32C3
|
||||
|
||||
This firmware is designed to run on ESP32-C3 boards using MicroPython. It drives a Nixie tube clock with high refresh rate, advanced dimming, and blending effects, leveraging the ESP32's RMT peripheral for precise timing
|
||||
|
||||
## Highlights
|
||||
|
||||
- **High Refresh Rate:** Smooth, flicker-free display using RMT hardware
|
||||
- **Dimming & Blending:** Configurable night mode brightness and digit blending transitions
|
||||
- **Configurable via `config.py`:** All major settings can be overridden by uploading a `config.py` file
|
||||
- **NTP Synchronization:** Periodically syncs time with NTP servers
|
||||
- **Timezone & DST Support:** Adjustable timezone and daylight saving settings
|
||||
|
||||
## Configuration Options
|
||||
|
||||
You can override any of these defaults by uploading a `config.py` file:
|
||||
|
||||
- `CONFIG_TIMEZONE`: Timezone offset from UTC (default: 2)
|
||||
- `CONFIG_DAYLIGHT_SAVING_ENABLED`: Enable/disable DST (default: True)
|
||||
- `CONFIG_NTP_SYNC_INTERVAL_HOURS`: NTP sync interval (default: 24)
|
||||
- `CONFIG_DIMMING_ENABLED`: Enable night dimming (default: True)
|
||||
- `CONFIG_DIMMING_BRIGHTNESS`: Night brightness (default: 0.4)
|
||||
- `CONFIG_DIMMING_START`: Dimming start hour (default: 15)
|
||||
- `CONFIG_DIMMING_END`: Dimming end hour (default: 6)
|
||||
- `CONFIG_DIMMING_GAMMA`: Dimming gamma correction (default: 2.2)
|
||||
- `CONFIG_BLENDING_ENABLED`: Enable digit blending (default: True)
|
||||
- `CONFIG_BLENDING_DURATION`: Blending duration in ms (default: 150)
|
||||
- `CONFIG_NETWORKS`: Dictionary of SSIDs and passwords for auto-connect
|
||||
|
||||
## Features
|
||||
|
||||
- **WiFi Auto-Connect:** Scans and connects to known networks
|
||||
- **NTP Time Sync:** Sets system time via NTP after WiFi connection
|
||||
- **Display Modes:** Time, date, and temperature rendering functions
|
||||
- **RMT-Based Display:** Uses ESP32 RMT for precise display timing
|
||||
- **Night Mode:** Automatic dimming based on configured hours
|
||||
- **Blending:** Smooth digit transitions for improved aesthetics
|
||||
|
||||
## Usage
|
||||
|
||||
1. Flash MicroPython to your ESP32-C3 board
|
||||
2. Upload `main.py` and (optionally) `config.py` to the device
|
||||
3. Reboot the board. The clock will auto-connect to WiFi, sync time, and start displaying
|
||||
|
||||
## Notes
|
||||
|
||||
- The firmware is intended for advanced users familiar with MicroPython and ESP32 hardware
|
||||
- For custom settings, create and upload a `config.py` file with your overrides
|
||||
- RMT and SPI pin assignments are set for typical Nixie clock hardware; adjust as needed for your build
|
||||
|
||||
---
|
||||
|
||||
For more details, see the source code and
|
||||
239
firmware/esp32c3-python/main.py
Normal file
239
firmware/esp32c3-python/main.py
Normal file
@@ -0,0 +1,239 @@
|
||||
import network
|
||||
import json
|
||||
import os
|
||||
import ntptime
|
||||
import machine
|
||||
from time import ticks_ms, localtime, sleep_ms
|
||||
from esp32 import RMT
|
||||
from time import sleep
|
||||
from machine import SPI, SoftSPI, Timer, Pin
|
||||
from time import sleep_ms
|
||||
|
||||
# Timezone settings
|
||||
CONFIG_TIMEZONE = 2
|
||||
CONFIG_DAYLIGHT_SAVING_ENABLED = True
|
||||
CONFIG_NTP_SYNC_INTERVAL_HOURS = 24
|
||||
|
||||
# General animation settings
|
||||
CONFIG_CPU_FREQUENCY = 80000000
|
||||
CONFIG_RMT_DIVISOR = 255
|
||||
CONFIG_REFRESH_RATE = 100
|
||||
|
||||
# Night time dimming configuration
|
||||
CONFIG_DIMMING_ENABLED = True
|
||||
CONFIG_DIMMING_BRIGHTNESS = 0.4
|
||||
CONFIG_DIMMING_START = 15
|
||||
CONFIG_DIMMING_END = 6
|
||||
CONFIG_DIMMING_GAMMA = 2.2
|
||||
|
||||
# Daytime frame blending configuration
|
||||
CONFIG_BLENDING_ENABLED = True
|
||||
CONFIG_BLENDING_DURATION = 150
|
||||
|
||||
# Wireless networks
|
||||
CONFIG_NETWORKS = {
|
||||
"k-space.ee legacy": "",
|
||||
}
|
||||
|
||||
try:
|
||||
from config import *
|
||||
except ImportError:
|
||||
print("Upload config.py to override configuration")
|
||||
except:
|
||||
print("Failed to load config.py")
|
||||
|
||||
assert CONFIG_TIMEZONE >= -12
|
||||
assert CONFIG_TIMEZONE < 14
|
||||
assert CONFIG_DAYLIGHT_SAVING_ENABLED in (True, False)
|
||||
assert CONFIG_CPU_FREQUENCY in (80000000, 160000000, 240000000)
|
||||
assert CONFIG_DIMMING_BRIGHTNESS >= 0
|
||||
assert CONFIG_DIMMING_BRIGHTNESS <= 1
|
||||
assert CONFIG_DIMMING_START >= 12
|
||||
assert CONFIG_DIMMING_START <= 23
|
||||
assert CONFIG_DIMMING_END >= 1
|
||||
assert CONFIG_DIMMING_END < 12
|
||||
assert CONFIG_DIMMING_GAMMA >= 1
|
||||
assert CONFIG_BLENDING_DURATION < 500
|
||||
assert CONFIG_BLENDING_DURATION >= 0
|
||||
assert CONFIG_RMT_DIVISOR >= 1
|
||||
assert CONFIG_RMT_DIVISOR <= 255
|
||||
|
||||
print("Setting CPU frequency to", CONFIG_CPU_FREQUENCY // 1000000, "MHz")
|
||||
machine.freq(CONFIG_CPU_FREQUENCY)
|
||||
|
||||
sta_if = network.WLAN(network.STA_IF)
|
||||
sta_if.active(True)
|
||||
for jssid, _, _, _, _, _ in sta_if.scan():
|
||||
ssid = jssid.decode("utf-8")
|
||||
if ssid in CONFIG_NETWORKS:
|
||||
sta_if.connect(ssid, CONFIG_NETWORKS[ssid])
|
||||
print("Connecting to", ssid, "...")
|
||||
while not sta_if.isconnected():
|
||||
pass
|
||||
print("Obtained DHCP lease for", sta_if.ifconfig()[0])
|
||||
ntptime.settime()
|
||||
sta_if.active(False)
|
||||
break
|
||||
else:
|
||||
print("No configured wireless network found")
|
||||
|
||||
print("Press Ctrl-C now to abort main.py execution and retain keyboard input")
|
||||
sleep_ms(2000)
|
||||
|
||||
clock = Pin(20, mode=Pin.OUT)
|
||||
latch = Pin(9, mode=Pin.OUT)
|
||||
data = Pin(2, mode=Pin.OUT)
|
||||
unused = Pin(11)
|
||||
spi = SoftSPI(baudrate=3000000, sck=clock, mosi=data, miso=unused)
|
||||
|
||||
COLON_LEFT_BOTTOM = 1 << 17 << 16
|
||||
COLON_LEFT_TOP = 1 << 18 << 16
|
||||
COLON_RIGHT_BOTTOM = 1 << 17
|
||||
COLON_RIGHT_TOP = 1 << 18
|
||||
|
||||
COLON_LEFT_BOTH = COLON_LEFT_TOP | COLON_LEFT_BOTTOM
|
||||
COLON_RIGHT_BOTH = COLON_RIGHT_TOP | COLON_RIGHT_BOTTOM
|
||||
COLON_BOTTOM_BOTH = COLON_LEFT_BOTTOM | COLON_RIGHT_BOTTOM
|
||||
COLON_TOP_BOTH = COLON_LEFT_TOP | COLON_RIGHT_TOP
|
||||
COLON_ALL = COLON_LEFT_BOTH | COLON_RIGHT_BOTH
|
||||
|
||||
IN15A_MICRO = 0
|
||||
IN15A_PERCENT = 2
|
||||
IN15A_PETA = 3
|
||||
IN15A_KILO = 4
|
||||
IN15A_MEGA = 5
|
||||
IN15A_MILLI = 6
|
||||
IN15A_PLUS = 7
|
||||
IN15A_MINUS = 8
|
||||
IN15A_PICO = 9
|
||||
|
||||
def clamp(v, lower=-99, upper=99):
|
||||
if v > upper:
|
||||
return upper
|
||||
elif v < lower:
|
||||
return lower
|
||||
|
||||
def render_digit(j, position=0):
|
||||
assert j >= -1 and j <= 9
|
||||
if j == -1: j = 10
|
||||
return [11, 9, 12, 8, 0, 4, 1, 3, 2, 10, 15][j] << 3 << (position << 3)
|
||||
|
||||
def render_digits(*args):
|
||||
z = 0
|
||||
for position, value in enumerate(reversed(args)):
|
||||
z |= render_digit(value, position)
|
||||
return z
|
||||
|
||||
def render_time(colons=True):
|
||||
_, _, _, h, m, s, _, _ = localtime()
|
||||
return render_digits(h // 10, h % 10, m // 10, m % 10, s // 10, s % 10) | (colons and COLON_ALL)
|
||||
|
||||
def render_date(colons=True):
|
||||
y, m, d, _, _, _, _, _ = localtime()
|
||||
return render_digits(y // 10, y % 10, m // 10, m % 10, d // 10, d % 10) | (colons and COLON_BOTTOM_BOTH)
|
||||
|
||||
def render_temperature(t):
|
||||
val = abs(clamp(t))
|
||||
return render_digits(-1, IN15A_MINUS if t < 0 else -1, val // 10, val % 10, -1, -1) | COLON_RIGHT_TOP
|
||||
|
||||
rmt = RMT(0, pin=latch, clock_div=CONFIG_RMT_DIVISOR, idle_level=False)
|
||||
tim = Timer(0, mode=Timer.PERIODIC)
|
||||
|
||||
RMT_DURATION = CONFIG_CPU_FREQUENCY // CONFIG_RMT_DIVISOR // CONFIG_REFRESH_RATE
|
||||
assert RMT_DURATION <= 32767, "RMT duration %d overflows 32767" % RMT_DURATION
|
||||
|
||||
print("Refresh rate: %d Hz" % CONFIG_REFRESH_RATE)
|
||||
print("PWM period: %d RMT pulses" % RMT_DURATION)
|
||||
|
||||
i = 0
|
||||
d = 0
|
||||
|
||||
from time import time, ticks_ms, localtime
|
||||
z = time()
|
||||
calibration = 0
|
||||
while z == time():
|
||||
calibration = ticks_ms() % 1000
|
||||
|
||||
def is_dst(y, mo, d, h, m):
|
||||
if mo < 3 or mo > 11:
|
||||
return False
|
||||
if 3 < mo < 11:
|
||||
return True
|
||||
if mo == 3:
|
||||
return d >= 8 # Approximation
|
||||
if mo == 11:
|
||||
return d < 7 # Approximation
|
||||
return False
|
||||
|
||||
def get_time():
|
||||
subsec = (ticks_ms() - calibration) % 1000
|
||||
now = time() + CONFIG_TIMEZONE * 3600
|
||||
y, mo, d, h, m, s, _, _ = localtime(now)
|
||||
if CONFIG_DAYLIGHT_SAVING_ENABLED and is_dst(y, mo, d, h, m):
|
||||
now += 3600 # Add 1 hour
|
||||
y, mo, d, h, m, s, _, _ = localtime(now)
|
||||
return y, mo, d, h, m, s, subsec
|
||||
|
||||
|
||||
def display_static(j):
|
||||
spi.write(j.to_bytes(6))
|
||||
rmt.write_pulses((1,1), 0)
|
||||
|
||||
def display_dimmed(j, brightness=0.5):
|
||||
duty = brightness ** CONFIG_DIMMING_GAMMA
|
||||
assert brightness >= 0
|
||||
assert brightness <= 1
|
||||
assert duty >= 0
|
||||
assert duty <= 1
|
||||
pulses = (1, 100, 1 + int(RMT_DURATION * duty), 1)
|
||||
if brightness == 1.0:
|
||||
display_static(j)
|
||||
else:
|
||||
spi.write(j.to_bytes(6))
|
||||
rmt.write_pulses(pulses, 0)
|
||||
spi.write('\x78\x78\x78\x78\x78\x78')
|
||||
|
||||
def display_blended(j, i, progression=0.5):
|
||||
assert progression >= 0
|
||||
assert progression <= 1
|
||||
duty = progression
|
||||
pulses = (1, 100, 1 + int(RMT_DURATION * duty), 1)
|
||||
if progression == 1.0:
|
||||
display_static(j)
|
||||
else:
|
||||
spi.write(j.to_bytes(6))
|
||||
rmt.write_pulses(pulses, 0)
|
||||
spi.write(i.to_bytes(6))
|
||||
|
||||
STATE_PREVIOUS = 0x787878787878
|
||||
STATE_REFRESH_MODE = 0
|
||||
|
||||
def run_clock(*args):
|
||||
global STATE_PREVIOUS
|
||||
global STATE_REFRESH_MODE
|
||||
y, mo, d, h, m, s, subsec = get_time()
|
||||
current = render_digits(h // 10, h % 10, m // 10, m % 10, s // 10, s % 10)
|
||||
prev = STATE_PREVIOUS
|
||||
if subsec <= 500:
|
||||
current |= COLON_ALL
|
||||
prev |= COLON_ALL
|
||||
if CONFIG_DIMMING_ENABLED and (h >= CONFIG_DIMMING_START or h < CONFIG_DIMMING_END):
|
||||
if STATE_REFRESH_MODE != 1:
|
||||
print("Switching to dimming mode, because", CONFIG_DIMMING_START, "<=", h, "<=", CONFIG_DIMMING_END)
|
||||
STATE_REFRESH_MODE = 1
|
||||
display_dimmed(current, CONFIG_DIMMING_BRIGHTNESS)
|
||||
elif CONFIG_BLENDING_ENABLED:
|
||||
if STATE_REFRESH_MODE != 2:
|
||||
print("Switching to blending mode")
|
||||
STATE_REFRESH_MODE = 2
|
||||
if subsec <= CONFIG_BLENDING_DURATION:
|
||||
if current != prev:
|
||||
display_blended(current, prev, progression=subsec / CONFIG_BLENDING_DURATION)
|
||||
else:
|
||||
display_static(current)
|
||||
STATE_PREVIOUS = current
|
||||
else:
|
||||
display_static(current)
|
||||
|
||||
print("Setting up periodic timer at %d Hz (%d ms)" % (CONFIG_REFRESH_RATE, 1000 // CONFIG_REFRESH_RATE))
|
||||
tim.init(mode=Timer.PERIODIC, period=1000 // CONFIG_REFRESH_RATE, callback=run_clock)
|
||||
Reference in New Issue
Block a user