7 Commits

Author SHA1 Message Date
2971df7b68 feat: Enable dualstack IPv4/IPv6 for microdot
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m40s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 10s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
18a58992f3 feat: Visual feedback on LEDs during startup
Set the LEDs to a fixed color during bootup to show the different modes:
- Orange when the device is booting
- Red when button 0 is held and the device goes to a shell
- Blue when button 1 is held and the device goes to AP mode instead of
  joining the configured WiFi network
- Red blinking when the run() method throws an exception for any reason

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
4e0af8e3fc feat: Join an existing WiFi network
Add ssid and passphrase to config to configure the network to join. If
empty SSID is configured, it will create the "TonberryPicoAP..." network
in AP mode as before.

Holding the button 1 during startup will revert to AP mode regardless of
the current configuration.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
d3aef1be32 fix: frontend: don't convert text that looks like an integer to integers
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
b9baa1c7d5 microdot: Update to v2.5.1
The important fix is an urldecode bug causing umlauts to be decoded
incorrectly from url arguments. This was fixed in v2.2.0, but let's just
update to the latest version.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
67d7650923 fix: webserver: Catch and report IO errors on upload
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
43fd68779c feat: store git version in fw and show in web ui
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
5 changed files with 80 additions and 15 deletions

View File

@@ -29,7 +29,7 @@ execute_process(COMMAND ${GIT} rev-parse HEAD
OUTPUT_STRIP_TRAILING_WHITESPACE
)
execute_process(COMMAND ${GIT} describe --match 'v*' --always
execute_process(COMMAND ${GIT} describe --match 'v*' --always --dirty
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
OUTPUT_VARIABLE TONBERRY_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE

View File

@@ -360,6 +360,21 @@
'tagstartstop': 'Present tag once to start, present again to stop playback'
}
},
'root.BUTTON_MAP.NEXT': {
'input-type': 'number'
},
'root.BUTTON_MAP.PREV': {
'input-type': 'number'
},
'root.BUTTON_MAP.VOLUP': {
'input-type': 'number'
},
'root.BUTTON_MAP.VOLDOWN': {
'input-type': 'number'
},
'root.BUTTON_MAP.PLAY_PAUSE': {
'input-type': 'number'
},
'root.IDLE_TIMEOUT_SECS': {
'input-type': 'number'
},
@@ -369,6 +384,12 @@
'root.LED_COUNT': {
'input-type': 'number'
},
'root.WLAN.SSID': {
'input-type': 'text'
},
'root.WLAN.PASSPHRASE': {
'input-type': 'text'
}
};
function renderObject(obj, path) {
@@ -439,7 +460,7 @@
let val = input.value.trim();
if (val === "") val = null;
else if (!isNaN(val)) val = Number(val);
else if (!isNaN(val) && input.type != 'text') val = Number(val);
current[path[path.length - 1]] = val;
});

View File

@@ -10,6 +10,7 @@ import network
import os
import time
import ubinascii
import sys
# Own modules
import app
@@ -37,14 +38,22 @@ hwconfig.board_init()
machine.mem32[0x40030000 + 0x00] = 0x10
def setup_wifi():
def setup_wifi(ssid='', passphrase='', sec=network.WLAN.SEC_WPA2_WPA3):
network.hostname("TonberryPico")
if ssid == '':
apname = f"TonberryPicoAP_{machine.unique_id().hex()}"
print(f"Create AP {apname}")
wlan = network.WLAN(network.WLAN.IF_AP)
wlan.config(ssid=f"TonberryPicoAP_{machine.unique_id().hex()}", security=wlan.SEC_OPEN)
wlan.config(ssid=apname, security=wlan.SEC_OPEN)
wlan.active(True)
else:
print(f"Connect to SSID {ssid} with passphrase {passphrase}...")
wlan = network.WLAN()
wlan.active(True)
wlan.connect(ssid, passphrase, security=sec)
# disable power management
wlan.config(pm=network.WLAN.PM_NONE)
# configure power management
wlan.config(pm=network.WLAN.PM_PERFORMANCE)
mac = ubinascii.hexlify(network.WLAN().config('mac'), ':').decode()
print(f" mac: {mac}")
@@ -65,14 +74,22 @@ DB_PATH = '/sd/tonberry.db'
config = Configuration()
# Setup LEDs
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
np.fill((32, 32, 0))
np.write()
def run():
asyncio.new_event_loop()
# Setup LEDs
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
# Wifi with default config
setup_wifi()
if machine.Pin(hwconfig.BUTTONS[1], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0:
np.fill((0, 0, 32))
np.write()
# Force default access point
setup_wifi('', '')
else:
setup_wifi(config.get_wifi_ssid(), config.get_wifi_passphrase())
# Setup MP3 player
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
@@ -130,7 +147,24 @@ def builddb():
os.sync()
def error_blink():
while True:
np.fill((32, 0, 0))
np.write()
time.sleep_ms(500)
np.fill((0, 0, 0))
np.write()
time.sleep_ms(500)
if __name__ == '__main__':
time.sleep(1)
if machine.Pin(hwconfig.BUTTONS[0], machine.Pin.IN, machine.Pin.PULL_UP).value() != 0:
try:
run()
except Exception as ex:
sys.print_exception(ex)
error_blink()
else:
np.fill((32, 0, 0))
np.write()

View File

@@ -22,7 +22,11 @@ class Configuration:
'PREV': None,
'NEXT': 1,
},
'TAGMODE': 'tagremains'
'TAGMODE': 'tagremains',
'WIFI': {
'SSID': '',
'PASSPHRASE': '',
}
}
def __init__(self, config_path='/config.json'):
@@ -87,6 +91,12 @@ class Configuration:
def get_tagmode(self) -> str:
return self._get('TAGMODE')
def get_wifi_ssid(self) -> str:
return self._get('WIFI')['SSID']
def get_wifi_passphrase(self) -> str:
return self._get('WIFI')['PASSPHRASE']
# For the web API
def get_config(self) -> Mapping[str, Any]:
return self.config

View File

@@ -31,7 +31,7 @@ Request.max_content_length = 128 * 1024 * 1024 # 128MB requests allowed
def start_webserver(config_, app_):
global server, config, app, nfc, playlist_db, leds, timer_manager
server = asyncio.create_task(webapp.start_server(port=80))
server = asyncio.create_task(webapp.start_server(host='::0', port=80))
config = config_
app = app_
nfc = app.get_nfc()