9 Commits

Author SHA1 Message Date
bbe0b3ea7f microdot: Update to v2.5.1
All checks were successful
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m42s
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-22 12:24:16 +01:00
6b96269f53 fix: webserver: Catch and report IO errors on upload
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-22 12:24:07 +01:00
f99dec19c9 feat: store git version in fw and show in web ui
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m41s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 11s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-21 21:20:13 +01:00
704951074b feat: Long press VOL_DOWN button to shutdown/reset device
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-21 18:31:13 +01:00
cac61f924f Merge branch 'file-upload' into mbl-next
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m43s
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 9s
Run pytests / Check-Pytest (push) Successful in 11s
2025-12-21 18:31:04 +01:00
9320a3cff2 fix: Show reboot request response in UI
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 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
Also make response from api more understandable for non-technical users.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-21 18:30:45 +01:00
65efebc5c2 feat: allow reboot commands only if usb cable is inserted 2025-12-21 18:30:45 +01:00
040ae4a731 fix: fix flake8 complaint
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-21 18:30:45 +01:00
9cf044bc80 feat: frontend: Add reboot to bootloader button (for updates)
Some checks failed
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m47s
Check code formatting / Check-C-Format (push) Successful in 6s
Check code formatting / Check-Python-Flake8 (push) Failing after 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-21 17:07:09 +01:00
8 changed files with 121 additions and 11 deletions

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
#include "py/obj.h"
#include "py/runtime.h"
#include "py/objstr.h"
#ifndef TONBERRY_GIT_REVISION
#define TONBERRY_GIT_REVISION "unknown"
#endif
#ifndef TONBERRY_VERSION
#define TONBERRY_VERSION "unknown"
#endif
static const MP_DEFINE_STR_OBJ(tonberry_git_revision_obj, TONBERRY_GIT_REVISION);
static const MP_DEFINE_STR_OBJ(tonberry_version_obj, TONBERRY_VERSION);
static const mp_rom_map_elem_t board_module_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_board)},
{MP_ROM_QSTR(MP_QSTR_revision), MP_ROM_PTR(&tonberry_git_revision_obj)},
{MP_ROM_QSTR(MP_QSTR_version), MP_ROM_PTR(&tonberry_version_obj)},
};
static MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table);
const mp_obj_module_t board_cmodule = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t *)&board_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_board, board_cmodule);

View File

@@ -20,3 +20,22 @@ set(GEN_PINS_CSV_ARG --board-csv "${GEN_PINS_BOARD_CSV}")
add_link_options("-Wl,--print-memory-usage")
set(PICO_USE_FASTEST_SUPPORTED_CLOCK 1)
find_program(GIT git)
execute_process(COMMAND ${GIT} rev-parse HEAD
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
OUTPUT_VARIABLE TONBERRY_GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
execute_process(COMMAND ${GIT} describe --match 'v*' --always
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
OUTPUT_VARIABLE TONBERRY_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(MICROPY_SOURCE_BOARD "${CMAKE_CURRENT_LIST_DIR}/board.c")
set(MICROPY_DEF_BOARD
TONBERRY_GIT_REVISION="${TONBERRY_GIT_REVISION}"
TONBERRY_VERSION="${TONBERRY_VERSION}")

View File

@@ -131,6 +131,12 @@
list-style: none;
}
.footer {
font-size: x-small;
color: gray;
margin-top: 20px;
}
</style>
</head>
<body>
@@ -153,6 +159,8 @@
<li><button onclick="showScreen('playlist')">Open Playlist Editor</button></li>
<!-- More screens can be added later -->
</ul>
<hr>
<button onclick="requestReboot()">Reboot to bootloader</button>
</div>
<!-- CONFIG EDITOR SCREEN -->
@@ -255,7 +263,17 @@
</div>
</div>
</div>
<div class="footer">
<hr>
<div class="flex-horizontal">
<div>
<a href="https://git.ka.blankertz.org/TonBERRY/tonberry-pico">TonBERRY pico</a>
</div>
<div>
Version: <span id="footer-version">unknown</span>
</div>
</div>
</div>
<script>
const Screens = {};
let activeScreen = null;
@@ -964,6 +982,14 @@
return { init, onShow };
})();
// Misc
async function requestReboot() {
const resp = await fetch('/api/v1/reboot/bootloader', {'method': 'POST'});
if (!resp.ok) {
alert('Reboot to bootloader failed: ' + await resp.text());
}
}
// Initialization
Object.values(Screens).forEach(screen => {
@@ -971,6 +997,13 @@
});
showScreen("menu");
fetch('/api/v1/info')
.then((resp) => resp.json())
.then((info) => {
const version = document.getElementById('footer-version');
version.innerText = info.version;
});
</script>
</body>

View File

@@ -196,8 +196,5 @@ class PlayerApp:
def get_playlist_db(self):
return self.playlist_db
def get_timer_manager(self):
return self.timer_manager
def get_leds(self):
return self.leds

View File

@@ -2,6 +2,7 @@
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
from utils.helpers import safe_callback
from utils.timer import TimerManager
from utils.buttons import Buttons
from utils.config import Configuration
from utils.leds import LedManager
@@ -9,7 +10,6 @@ from utils.mbrpartition import MBRPartition
from utils.pinindex import get_pin_index
from utils.playlistdb import BTreeDB, BTreeFileManager
from utils.sdcontext import SDContext
from utils.timer import TimerManager
__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "Configuration", "get_pin_index", "LedManager", "MBRPartition",
"safe_callback", "SDContext", "TimerManager"]

View File

@@ -5,7 +5,7 @@ import asyncio
import machine
import micropython
import time
from utils import safe_callback
from utils import safe_callback, TimerManager
try:
from typing import TYPE_CHECKING # type: ignore
except ImportError:
@@ -32,6 +32,7 @@ class Buttons:
def __init__(self, cb: "ButtonCallback", config, hwconfig):
self.button_map = config.get_button_map()
self.hw_buttons = hwconfig.BUTTONS
self.hwconfig = hwconfig
self.cb = cb
self.buttons = dict()
for key_id, key_name in self.KEYMAP.items():
@@ -42,6 +43,7 @@ class Buttons:
self.int_flag = asyncio.ThreadSafeFlag()
self.pressed: list[int] = []
self.last: dict[int, int] = {}
self.timer_manager = TimerManager()
for button in self.buttons.keys():
button.irq(handler=self._interrupt, trigger=machine.Pin.IRQ_FALLING | machine.Pin.IRQ_RISING)
asyncio.create_task(self.task())
@@ -69,6 +71,10 @@ class Buttons:
# print(f'B{keycode} {now}')
self.pressed.append(keycode)
self.int_flag.set()
if keycode == self.VOLDOWN:
self.timer_manager.schedule(time.ticks_ms() + 5000, self.long_press_shutdown)
if button.value() == 1 and keycode == self.VOLDOWN:
self.timer_manager.cancel(self.long_press_shutdown)
async def task(self):
while True:
@@ -76,3 +82,9 @@ class Buttons:
while len(self.pressed) > 0:
what = self.pressed.pop()
safe_callback(lambda: self.cb.onButtonPressed(what), "button callback")
def long_press_shutdown(self):
if self.hwconfig.get_on_battery():
self.hwconfig.power_off()
else:
machine.reset()

View File

@@ -4,10 +4,14 @@ Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
'''
import asyncio
import board
import hwconfig
import json
import machine
import network
import os
import time
import ubinascii
from array import array
from microdot import Microdot, redirect, send_file, Request
@@ -33,7 +37,7 @@ def start_webserver(config_, app_):
nfc = app.get_nfc()
playlist_db = app.get_playlist_db()
leds = app.get_leds()
timer_manager = app.get_timer_manager()
timer_manager = TimerManager()
@webapp.before_request
@@ -215,11 +219,17 @@ async def audiofile_upload(request):
data = array('b', range(4096))
bytes_copied = 0
while True:
bytes_read = await request.stream.readinto(data)
try:
bytes_read = await request.stream.readinto(data)
except OSError as ex:
return f'read error: {ex}', 500
if bytes_read == 0:
# End of body
break
bytes_written = newfile.write(data[:bytes_read])
try:
bytes_written = newfile.write(data[:bytes_read])
except OSError as ex:
return f'write error: {ex}', 500
if bytes_written != bytes_read:
# short writes shouldn't happen
return 'write failure', 500
@@ -257,12 +267,22 @@ async def audiofile_delete(request):
@webapp.route('/api/v1/reboot/<method>', methods=['POST'])
async def reboot(request, method):
if hwconfig.get_on_battery():
return 'not allowed: usb not connected', 403
if method == 'bootloader':
leds.set_state(LedManager.REBOOTING)
timer_manager.schedule(time.ticks_ms() + 1500, machine.bootloader)
elif method =='application':
elif method == 'application':
leds.set_state(LedManager.REBOOTING)
timer_manager.schedule(time.ticks_ms() + 1500, machine.reset)
else:
return 'method not supported', 400
return '', 204
@webapp.route('/api/v1/info', methods=['GET'])
async def get_info(request):
mac = ubinascii.hexlify(network.WLAN().config('mac'), ':').decode()
return {'version': board.version,
'mac': mac}