4 Commits

Author SHA1 Message Date
6dee7fff7e feat: Allow configuring WiFi security
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m44s
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 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
Allow choosing between the three security modes exposed by the
micropython cyw43 wifi driver. Also allow setting up security in AP
mode.

Improve the WiFi section of the configuration UI.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2026-01-06 14:59:12 +01:00
6976aa6963 fix[player]: Don't latch tag if no playlist exists
When the device is in 'tagstartstop' tag mode, and the user presents a
new tag to get the serial number and create a playlist using the web UI,
the playerapp still remembered the tag as the currently playing tag even
though no playlist was found and no playback is running. After the user
saves the playlist in the UI and puts the tag on the device again, they
expect the playback to start with the new playlist. Instead, nothing
happens, because this is counted as the 'stop' event of the tagstartstop
mode. The user would have to remove the tag and present it again (after
waiting for the tagtimeout) to play the new playlist.

Fix this unexpected behaviour by not storing the current tag into the
playing_tag field if no playlist existed for the tag.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2026-01-06 14:59:12 +01:00
763305c659 fix[frontend]: Reset upload UI elements, don't expand tree view
- Make sure the upload progess bar and file choser are reset when
  loading the file browser screen.
- Don't expand the directories in the tree view by default.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2026-01-06 14:59:12 +01:00
6d18437863 fix: Remove directory based db creation
Previously, if no tonberry.db existed on the SD card, the database was
initialized with a playlist for each directory containing mp3 files,
with the tag serial number matching the directory name. This was used
during development before the web UI to edit the playlist db existed.
It is no longer necessary, and confusing to the user when unusable
playlists are created when albums are preloaded onto the SD card before
putting it in the tonberry device.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2026-01-06 14:59:12 +01:00
4 changed files with 45 additions and 42 deletions

View File

@@ -359,7 +359,10 @@
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain',
'root.VOLUME_MAX': 'Maximum volume (0-255)',
'root.VOLUME_BOOT': 'Volume at startup (0-255)',
'root.LED_MAX': 'Maximum LED brightness (0-255)'
'root.LED_MAX': 'Maximum LED brightness (0-255)',
'root.WIFI.SSID': 'Network name (SSID) (leave empty for AP mode)',
'root.WIFI.PASSPHRASE': 'Password',
'root.WIFI.SECURITY': 'Security mode'
};
const config_input_override = {
'root.TAGMODE': {
@@ -393,12 +396,21 @@
'root.LED_COUNT': {
'input-type': 'number'
},
'root.WLAN.SSID': {
'root.WIFI.SSID': {
'input-type': 'text'
},
'root.WLAN.PASSPHRASE': {
'root.WIFI.PASSPHRASE': {
'input-type': 'text'
},
'root.WIFI.SECURITY': {
'element': 'select',
'values': {
'open': 'Open',
'wpa_wpa2': 'WPA/WPA2 (PSK Mixed)',
'wpa3': 'WPA3 (SAE AES)',
'wpa2_wpa3': 'WPA2/WPA3 (PSK AES)'
}
},
'root.VOLUME_MAX': {
'input-type': 'number'
},
@@ -796,6 +808,10 @@
async function onShow(intent) {
document.getElementById('playlist-filebrowser-addtrack').disabled = true;
if (intent !== 'refresh') {
document.getElementById('playlist-filebrowser-upload-progress').value = 0;
document.getElementById("playlist-filebrowser-upload-files").value = "";
}
tree = document.getElementById("playlist-filebrowser-tree");
tree.innerHTML = "Loading...";
fetch('/api/v1/audiofiles')
@@ -830,7 +846,6 @@
if (type === 'directory') {
const nested = document.createElement('ul');
node.appendChild(nested);
node.classList.add('expanded');
parent.appendChild(node);
return nested;
}
@@ -917,7 +932,7 @@
}
if (donecount + 1 === totalcount) {
// Reload file list from device
onShow();
onShow('refresh');
} else {
uploadFileHelper(totalcount, donecount + 1, destdir, files.slice(1));
}
@@ -944,7 +959,7 @@
const saveRes = await fetch(`/api/v1/audiofiles?type=directory&location=${location}`,
{method: 'POST'});
// Reload file list from device
onShow();
onShow('refresh');
}
async function deleteItems() {
@@ -965,7 +980,7 @@
}
}
// Reload file list from device
onShow();
onShow('refresh');
}
let tree = (() => {

View File

@@ -82,7 +82,7 @@ class PlayerApp:
uid_str = b''.join('{:02x}'.format(x).encode() for x in new_tag)
if self.tag_mode == 'tagremains' or (self.tag_mode == 'tagstartstop' and new_tag != self.playing_tag):
self._set_playlist(uid_str)
self.playing_tag = new_tag
self.playing_tag = new_tag if self.playlist is not None else None
elif self.tag_mode == 'tagstartstop':
print('Tag presented again, stopping playback')
self._unset_playlist()

View File

@@ -3,11 +3,9 @@
import aiorepl # type: ignore
import asyncio
from errno import ENOENT
import machine
import micropython
import network
import os
import time
import ubinascii
import sys
@@ -38,19 +36,19 @@ hwconfig.board_init()
machine.mem32[0x40030000 + 0x00] = 0x10
def setup_wifi(ssid='', passphrase='', sec=network.WLAN.SEC_WPA2_WPA3):
def setup_wifi(ssid='', passphrase='', security=network.WLAN.SEC_WPA_WPA2):
network.hostname("TonberryPico")
if ssid is None or ssid == '':
apname = f"TonberryPicoAP_{machine.unique_id().hex()}"
print(f"Create AP {apname}")
wlan = network.WLAN(network.WLAN.IF_AP)
wlan.config(ssid=apname, security=wlan.SEC_OPEN)
wlan.config(ssid=apname, password=passphrase if passphrase is not None else '', security=security)
wlan.active(True)
else:
print(f"Connect to SSID {ssid} with passphrase {passphrase}...")
wlan = network.WLAN()
wlan.active(True)
wlan.connect(ssid, passphrase if passphrase is not None else '', security=sec)
wlan.connect(ssid, passphrase if passphrase is not None else '', security=security)
# configure power management
wlan.config(pm=network.WLAN.PM_PERFORMANCE)
@@ -88,22 +86,23 @@ def run():
np.fill((0, 0, led_max))
np.write()
# Force default access point
setup_wifi('', '')
setup_wifi('', '', network.WLAN.SEC_OPEN)
else:
setup_wifi(config.get_wifi_ssid(), config.get_wifi_passphrase())
secstring = config.get_wifi_security()
security = network.WLAN.SEC_WPA_WPA2
if secstring == 'open':
security = network.WLAN.SEC_OPEN
elif secstring == 'wpa_wpa2':
security = network.WLAN.SEC_WPA_WPA2
elif secstring == 'wpa3':
security = network.WLAN.SEC_WPA3
elif secstring == 'wpa2_wpa3':
security = network.WLAN.SEC_WPA2_WPA3
setup_wifi(config.get_wifi_ssid(), config.get_wifi_passphrase(), security)
# Setup MP3 player
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
baudrate=hwconfig.SD_CLOCKRATE):
# Temporary hack: build database from folders if no database exists
# Can be removed once playlists can be created via API
try:
_ = os.stat(DB_PATH)
except OSError as ex:
if ex.errno == ENOENT:
print("No playlist DB found, trying to build DB from tag dirs")
builddb()
with BTreeFileManager(DB_PATH) as playlistdb, \
AudioContext(hwconfig.I2S_DIN, hwconfig.I2S_DCLK, hwconfig.I2S_LRCLK) as audioctx:
@@ -130,24 +129,6 @@ def run():
asyncio.get_event_loop().run_forever()
def builddb():
"""
For testing, build a playlist db based on the previous tag directory format.
Can be removed once uploading files / playlist via the web api is possible.
"""
try:
os.unlink(DB_PATH)
except OSError:
pass
with BTreeFileManager(DB_PATH) as db:
for name, type_, _, _ in os.ilistdir(b'/sd'):
if type_ != 0x4000:
continue
fl = [b'/sd/' + name + b'/' + x for x in os.listdir(b'/sd/' + name) if x.endswith(b'.mp3')]
db.createPlaylistForTag(name, fl)
os.sync()
def error_blink():
while True:
if machine.Pin(hwconfig.BUTTONS[0], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0:

View File

@@ -26,6 +26,7 @@ class Configuration:
'WIFI': {
'SSID': '',
'PASSPHRASE': '',
'SECURITY': 'wpa_wpa2',
},
'VOLUME_MAX': 255,
'VOLUME_BOOT': 16,
@@ -100,6 +101,9 @@ class Configuration:
def get_wifi_passphrase(self) -> str:
return self._get('WIFI')['PASSPHRASE']
def get_wifi_security(self) -> str:
return self._get('WIFI')['SECURITY']
def get_volume_max(self) -> int:
return self._get('VOLUME_MAX')
@@ -126,6 +130,9 @@ class Configuration:
self._validate(self.DEFAULT_CONFIG, config)
if 'TAGMODE' in config and config['TAGMODE'] not in ['tagremains', 'tagstartstop']:
raise ValueError("Invalid TAGMODE: Must be 'tagremains' or 'tagstartstop'")
if 'WLAN' in config and 'SECURITY' in config['WLAN'] and \
config['WLAN']['SECURITY'] not in ['open', 'wpa_wpa2', 'wpa3', 'wpa2_wpa3']:
raise ValueError("Invalid WLAN SECURITY: Must be 'open', 'wpa_wpa2', 'wpa3' or 'wpa2_wpa3'")
self._merge_configs(self.config, config)
self.config = config
self._save()