Compare commits
17 Commits
97e0bf1056
...
e0a7082f79
| Author | SHA1 | Date | |
|---|---|---|---|
| e0a7082f79 | |||
| 5bb6a89e8c | |||
| c931c8fe8e | |||
| 19afb2f936 | |||
| 3c23fc1446 | |||
| 111ae65ebc | |||
| 1356ea06ab | |||
| e07ee46518 | |||
| cd5515ddad | |||
| 99ad8582f0 | |||
| ae875950cd | |||
| 340aea6be6 | |||
| f64bbc27fd | |||
| abb880baca | |||
| a59f00ad60 | |||
| 135ad11de9 | |||
| ff52e989a2 |
@@ -17,9 +17,9 @@ jobs:
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: firmware-RPi-Pico-W
|
||||
path: software/lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2
|
||||
path: software/build/firmware-*.uf2
|
||||
- name: Upload firmware w/ filesystem
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: firmware-RPi-Pico-W-with-fs
|
||||
path: software/lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware-filesystem-*.uf2
|
||||
path: software/build/firmware-filesystem-*.uf2
|
||||
|
||||
3
software/boards/RPI_PICO_W/manifest-Rev1.py
Normal file
3
software/boards/RPI_PICO_W/manifest-Rev1.py
Normal file
@@ -0,0 +1,3 @@
|
||||
include("manifest.py")
|
||||
|
||||
module("hwconfig.py", "../../src/hwconfig_Rev1")
|
||||
3
software/boards/RPI_PICO_W/manifest-breadboard.py
Normal file
3
software/boards/RPI_PICO_W/manifest-breadboard.py
Normal file
@@ -0,0 +1,3 @@
|
||||
include("manifest.py")
|
||||
|
||||
module("hwconfig.py", "../../src/hwconfig_breadboard")
|
||||
@@ -15,3 +15,10 @@ module("microdot.py", "../../lib/microdot/src/microdot/")
|
||||
# TonberryPico modules
|
||||
module("audiocore.py", "../../modules/audiocore")
|
||||
module("rp2_neopixel.py", "../../modules")
|
||||
|
||||
module("main.py", "../../src")
|
||||
module("app.py", "../../src")
|
||||
module("mp3player.py", "../../src")
|
||||
module("webserver.py", "../../src")
|
||||
package("utils", base_path="../../src")
|
||||
package("nfc", base_path="../../src")
|
||||
|
||||
5
software/boards/tonberry_unix/manifest.py
Normal file
5
software/boards/tonberry_unix/manifest.py
Normal file
@@ -0,0 +1,5 @@
|
||||
include("$(PORT_DIR)/variants/manifest.py")
|
||||
|
||||
include("$(MPY_DIR)/extmod/asyncio")
|
||||
|
||||
module("microdot.py", "../../lib/microdot/src/microdot/")
|
||||
31
software/boards/tonberry_unix/mpconfigvariant.h
Normal file
31
software/boards/tonberry_unix/mpconfigvariant.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of the MicroPython project, http://micropython.org/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2019 Damien P. George
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Set base feature level.
|
||||
#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_EXTRA_FEATURES)
|
||||
|
||||
// Enable extra Unix features.
|
||||
#include "mpconfigvariant_common.h"
|
||||
3
software/boards/tonberry_unix/mpconfigvariant.mk
Normal file
3
software/boards/tonberry_unix/mpconfigvariant.mk
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is the default variant when you `make` the Unix port.
|
||||
|
||||
FROZEN_MANIFEST ?= $(VARIANT_DIR)/manifest.py
|
||||
126
software/boards/tonberry_unix/mpconfigvariant_common.h
Normal file
126
software/boards/tonberry_unix/mpconfigvariant_common.h
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* This file is part of the MicroPython project, http://micropython.org/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2022 Jim Mussared
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// This file enables and configures features common to all variants
|
||||
// other than "minimal".
|
||||
|
||||
// Send raise KeyboardInterrupt directly from the signal handler rather than
|
||||
// scheduling it into the VM.
|
||||
#define MICROPY_ASYNC_KBD_INTR (!MICROPY_PY_THREAD_GIL)
|
||||
|
||||
// Enable helpers for printing debugging information.
|
||||
#ifndef MICROPY_DEBUG_PRINTERS
|
||||
#define MICROPY_DEBUG_PRINTERS (1)
|
||||
#endif
|
||||
|
||||
// Enable floating point by default.
|
||||
#ifndef MICROPY_FLOAT_IMPL
|
||||
#define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_DOUBLE)
|
||||
#endif
|
||||
|
||||
// Don't use native _Float16 because it increases code size by a lot.
|
||||
#ifndef MICROPY_FLOAT_USE_NATIVE_FLT16
|
||||
#define MICROPY_FLOAT_USE_NATIVE_FLT16 (0)
|
||||
#endif
|
||||
|
||||
// Enable arbitrary precision long-int by default.
|
||||
#ifndef MICROPY_LONGINT_IMPL
|
||||
#define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_MPZ)
|
||||
#endif
|
||||
|
||||
// Enable use of C libraries that need read/write/lseek/fsync, e.g. axtls.
|
||||
#define MICROPY_STREAMS_POSIX_API (1)
|
||||
|
||||
// REPL conveniences.
|
||||
#define MICROPY_REPL_EMACS_WORDS_MOVE (1)
|
||||
#define MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE (1)
|
||||
#define MICROPY_USE_READLINE_HISTORY (1)
|
||||
#ifndef MICROPY_READLINE_HISTORY_SIZE
|
||||
#define MICROPY_READLINE_HISTORY_SIZE (50)
|
||||
#endif
|
||||
|
||||
// Seed random on import.
|
||||
#define MICROPY_PY_RANDOM_SEED_INIT_FUNC (mp_random_seed_init())
|
||||
|
||||
// Allow exception details in low-memory conditions.
|
||||
#define MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF (1)
|
||||
#define MICROPY_EMERGENCY_EXCEPTION_BUF_SIZE (256)
|
||||
|
||||
// Allow loading of .mpy files.
|
||||
#define MICROPY_PERSISTENT_CODE_LOAD (1)
|
||||
|
||||
// Extra memory debugging.
|
||||
#define MICROPY_MALLOC_USES_ALLOCATED_SIZE (1)
|
||||
#define MICROPY_MEM_STATS (1)
|
||||
|
||||
// Enable a small performance boost for the VM.
|
||||
#define MICROPY_OPT_COMPUTED_GOTO (1)
|
||||
|
||||
// Return number of collected objects from gc.collect().
|
||||
#define MICROPY_PY_GC_COLLECT_RETVAL (1)
|
||||
|
||||
// Enable detailed error messages and warnings.
|
||||
#define MICROPY_ERROR_REPORTING (MICROPY_ERROR_REPORTING_DETAILED)
|
||||
#define MICROPY_WARNINGS (1)
|
||||
#define MICROPY_PY_STR_BYTES_CMP_WARN (1)
|
||||
|
||||
// Configure the "sys" module with features not usually enabled on bare-metal.
|
||||
#define MICROPY_PY_SYS_ATEXIT (1)
|
||||
#define MICROPY_PY_SYS_EXC_INFO (1)
|
||||
|
||||
// Configure the "os" module with extra unix features.
|
||||
#define MICROPY_PY_OS_INCLUDEFILE "ports/unix/modos.c"
|
||||
#define MICROPY_PY_OS_ERRNO (1)
|
||||
#define MICROPY_PY_OS_GETENV_PUTENV_UNSETENV (1)
|
||||
#define MICROPY_PY_OS_SYSTEM (1)
|
||||
#define MICROPY_PY_OS_URANDOM (1)
|
||||
|
||||
// Enable the unix-specific "time" module.
|
||||
#define MICROPY_PY_TIME (1)
|
||||
#define MICROPY_PY_TIME_TIME_TIME_NS (1)
|
||||
#define MICROPY_PY_TIME_CUSTOM_SLEEP (1)
|
||||
#define MICROPY_PY_TIME_INCLUDEFILE "ports/unix/modtime.c"
|
||||
|
||||
#if MICROPY_PY_SSL
|
||||
#define MICROPY_PY_HASHLIB_MD5 (1)
|
||||
#define MICROPY_PY_HASHLIB_SHA1 (1)
|
||||
#define MICROPY_PY_CRYPTOLIB (1)
|
||||
#endif
|
||||
|
||||
// The "select" module is enabled by default, but disable select.select().
|
||||
#define MICROPY_PY_SELECT_POSIX_OPTIMISATIONS (1)
|
||||
#define MICROPY_PY_SELECT_SELECT (0)
|
||||
|
||||
// Enable the "websocket" module.
|
||||
#define MICROPY_PY_WEBSOCKET (1)
|
||||
|
||||
// Enable the "machine" module, mostly for machine.mem*.
|
||||
#define MICROPY_PY_MACHINE (1)
|
||||
#define MICROPY_PY_MACHINE_PULSE (1)
|
||||
#define MICROPY_PY_MACHINE_PIN_BASE (1)
|
||||
|
||||
#define MICROPY_VFS_ROM (1)
|
||||
#define MICROPY_VFS_ROM_IOCTL (0)
|
||||
@@ -6,40 +6,50 @@ set -eu
|
||||
|
||||
( cd lib/micropython
|
||||
make -C mpy-cross -j "$(nproc)"
|
||||
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W clean
|
||||
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W \
|
||||
USER_C_MODULES="$TOPDIR"/modules/micropython.cmake -j "$(nproc)"
|
||||
|
||||
# build tonberry specific unix port of micropython
|
||||
make -C ports/unix VARIANT_DIR="$TOPDIR"/boards/tonberry_unix clean
|
||||
make -C ports/unix VARIANT_DIR="$TOPDIR"/boards/tonberry_unix -j "$(nproc)"
|
||||
)
|
||||
|
||||
( cd tools/mklittlefs
|
||||
make -j "$(nproc)"
|
||||
)
|
||||
|
||||
PICOTOOL=picotool
|
||||
if ! command -v $PICOTOOL >/dev/null 2>&1; then
|
||||
echo "system picotool not found, checking SDK build dir"
|
||||
PICOTOOL=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/_deps/picotool-build/picotool
|
||||
if ! command -v $PICOTOOL >/dev/null 2>&1; then
|
||||
echo "No picotool found, exiting"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
BUILDDIR=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/
|
||||
OUTDIR=$(pwd)/build
|
||||
mkdir -p "$OUTDIR"
|
||||
FS_STAGE_DIR=$(mktemp -d)
|
||||
mkdir "$FS_STAGE_DIR"/fs
|
||||
trap 'rm -rf $FS_STAGE_DIR' EXIT
|
||||
for hwconfig in src/hwconfig_*.py; do
|
||||
tools/mklittlefs/mklittlefs -p 256 -s 868352 -c "$FS_STAGE_DIR"/fs "$FS_STAGE_DIR"/filesystem.bin
|
||||
|
||||
for hwconfig in boards/RPI_PICO_W/manifest-*.py; do
|
||||
hwconfig_base=$(basename "$hwconfig")
|
||||
hwname=${hwconfig_base##hwconfig_}
|
||||
hwname=${hwconfig_base##manifest-}
|
||||
hwname=${hwname%%.py}
|
||||
find src/ -iname '*.py' \! -iname 'hwconfig_*.py' | cpio -pdm "$FS_STAGE_DIR"
|
||||
cp "$hwconfig" "$FS_STAGE_DIR"/src/hwconfig.py
|
||||
tools/mklittlefs/mklittlefs -p 256 -s 868352 -c "$FS_STAGE_DIR"/src $BUILDDIR/filesystem.bin
|
||||
hwconfig_abs=$(realpath "$hwconfig")
|
||||
( cd lib/micropython
|
||||
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W clean
|
||||
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W \
|
||||
USER_C_MODULES="$TOPDIR"/modules/micropython.cmake \
|
||||
FROZEN_MANIFEST="$hwconfig_abs" -j "$(nproc)"
|
||||
)
|
||||
PICOTOOL=picotool
|
||||
if ! command -v $PICOTOOL >/dev/null 2>&1; then
|
||||
echo "system picotool not found, checking SDK build dir"
|
||||
PICOTOOL=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/_deps/picotool-build/picotool
|
||||
if ! command -v $PICOTOOL >/dev/null 2>&1; then
|
||||
echo "No picotool found, exiting"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
truncate -s 2M $BUILDDIR/firmware-filesystem.bin
|
||||
dd if=$BUILDDIR/firmware.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k
|
||||
dd if=$BUILDDIR/filesystem.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k seek=1200
|
||||
$PICOTOOL uf2 convert $BUILDDIR/firmware-filesystem.bin $BUILDDIR/firmware-filesystem-"$hwname".uf2
|
||||
rm -r "${FS_STAGE_DIR:?}"/*
|
||||
dd if="$FS_STAGE_DIR"/filesystem.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k seek=1200
|
||||
cp $BUILDDIR/firmware.uf2 "$OUTDIR"/firmware-"$hwname".uf2
|
||||
$PICOTOOL uf2 convert $BUILDDIR/firmware-filesystem.bin "$OUTDIR"/firmware-filesystem-"$hwname".uf2
|
||||
done
|
||||
|
||||
echo "Output in $BUILDDIR/firmware.uf2"
|
||||
echo "Images with filesystem in" ${BUILDDIR}firmware-filesystem-*.uf2
|
||||
echo "Output in" "${OUTDIR}"/firmware-*.uf2
|
||||
echo "Images with filesystem in" "${OUTDIR}"/firmware-filesystem-*.uf2
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
set -eu
|
||||
|
||||
TOPDIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
check_command()
|
||||
{
|
||||
name=$1
|
||||
@@ -16,7 +18,7 @@ check_command lsusb
|
||||
check_command picotool
|
||||
|
||||
DEVICEPATH=/dev/disk/by-label/RPI-RP2
|
||||
IMAGEPATH=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/
|
||||
IMAGEPATH=${TOPDIR}/build
|
||||
REVISION=Rev1
|
||||
|
||||
flash_via_mountpoint()
|
||||
@@ -41,7 +43,7 @@ flash_via_picotool()
|
||||
local device="${bus_device[1]//[!0-9]/}"
|
||||
echo "Found RP2 with serial $serial on Bus $bus Device $device"
|
||||
|
||||
picotool load --bus "$bus" --address "$device" "$IMAGEFILE"
|
||||
picotool load --update --bus "$bus" --address "$device" "$IMAGEFILE"
|
||||
}
|
||||
|
||||
FLASH_VIA_MOUNTPOINT=0
|
||||
@@ -83,7 +85,7 @@ if [ $# -gt 0 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
IMAGEFILE="$IMAGEPATH"/firmware-filesystem-$REVISION.uf2
|
||||
IMAGEFILE="$IMAGEPATH"/firmware-$REVISION.uf2
|
||||
|
||||
if [ "$FLASH_VIA_MOUNTPOINT" -eq 0 ]; then
|
||||
flash_via_picotool
|
||||
|
||||
@@ -6,7 +6,8 @@ import time
|
||||
from utils import TimerManager
|
||||
|
||||
|
||||
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds'))
|
||||
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds',
|
||||
'config'))
|
||||
|
||||
# Should be ~ 6dB steps
|
||||
VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251]
|
||||
@@ -14,11 +15,12 @@ VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251]
|
||||
|
||||
class PlayerApp:
|
||||
class TagStateMachine:
|
||||
def __init__(self, parent, timer_manager):
|
||||
def __init__(self, parent, timer_manager, timeout=5000):
|
||||
self.parent = parent
|
||||
self.timer_manager = timer_manager
|
||||
self.current_tag = None
|
||||
self.current_tag_time = time.ticks_ms()
|
||||
self.timeout = timeout
|
||||
|
||||
def onTagChange(self, new_tag):
|
||||
if new_tag is not None:
|
||||
@@ -31,7 +33,7 @@ class PlayerApp:
|
||||
self.current_tag = new_tag
|
||||
self.parent.onNewTag(new_tag)
|
||||
else:
|
||||
self.timer_manager.schedule(time.ticks_ms() + 5000, self.onTagRemoveDelay)
|
||||
self.timer_manager.schedule(time.ticks_ms() + self.timeout, self.onTagRemoveDelay)
|
||||
|
||||
def onTagRemoveDelay(self):
|
||||
if self.current_tag is not None:
|
||||
@@ -40,7 +42,10 @@ class PlayerApp:
|
||||
|
||||
def __init__(self, deps: Dependencies):
|
||||
self.timer_manager = TimerManager()
|
||||
self.tag_state_machine = self.TagStateMachine(self, self.timer_manager)
|
||||
self.config = deps.config(self)
|
||||
self.tag_timeout_ms = self.config.get_tag_timeout() * 1000
|
||||
self.idle_timeout_ms = self.config.get_idle_timeout() * 1000
|
||||
self.tag_state_machine = self.TagStateMachine(self, self.timer_manager, self.tag_timeout_ms)
|
||||
self.player = deps.mp3player(self)
|
||||
self.nfc = deps.nfcreader(self.tag_state_machine)
|
||||
self.playlist_db = deps.playlistdb(self)
|
||||
@@ -103,7 +108,7 @@ class PlayerApp:
|
||||
self.hwconfig.power_off()
|
||||
else:
|
||||
# Check again in a minute
|
||||
self.timer_manager.schedule(time.ticks_ms() + 60*1000, self.onIdleTimeout)
|
||||
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
|
||||
|
||||
def _set_playlist(self, tag: bytes):
|
||||
if self.playlist is not None:
|
||||
@@ -144,7 +149,7 @@ class PlayerApp:
|
||||
self._onActive()
|
||||
|
||||
def _onIdle(self):
|
||||
self.timer_manager.schedule(time.ticks_ms() + 60*1000, self.onIdleTimeout)
|
||||
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
|
||||
self.leds.set_state(self.leds.IDLE)
|
||||
|
||||
def _onActive(self):
|
||||
|
||||
@@ -28,7 +28,6 @@ RC522_SS = Pin.board.GP13
|
||||
|
||||
# WS2812
|
||||
LED_DIN = Pin.board.GP16
|
||||
LED_COUNT = 1
|
||||
|
||||
# Buttons
|
||||
BUTTON_VOLUP = Pin.board.GP17
|
||||
@@ -27,7 +27,6 @@ RC522_SS = Pin.board.GP13
|
||||
|
||||
# WS2812
|
||||
LED_DIN = Pin.board.GP16
|
||||
LED_COUNT = 1
|
||||
|
||||
# Buttons
|
||||
BUTTON_VOLUP = Pin.board.GP17
|
||||
@@ -1,58 +0,0 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
# Run with mpremote.py run src/led_test.py
|
||||
|
||||
from machine import Pin
|
||||
from math import pi, sin, pow
|
||||
from micropython import const
|
||||
from rp2_neopixel import NeoPixel
|
||||
from time import sleep, ticks_ms
|
||||
import asyncio
|
||||
|
||||
pin = Pin.board.GP16
|
||||
leds = const(10)
|
||||
brightness = 0.5
|
||||
|
||||
np = NeoPixel(pin, leds)
|
||||
|
||||
# test fill and write
|
||||
|
||||
print("LEDs should now turn red")
|
||||
np.fill((255, 0, 0))
|
||||
np.write()
|
||||
sleep(1)
|
||||
|
||||
print("LEDs should now turn green")
|
||||
np.fill((0, 255, 0))
|
||||
np.write()
|
||||
sleep(1)
|
||||
|
||||
print("LEDs should now turn blue")
|
||||
np.fill((0, 0, 255))
|
||||
np.write()
|
||||
sleep(1)
|
||||
|
||||
|
||||
# test async
|
||||
def gamma(value, X=2.2):
|
||||
return min(max(int(brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
|
||||
|
||||
|
||||
async def rainbow(np, period=10):
|
||||
count = 0.0
|
||||
while True:
|
||||
for i in range(leds):
|
||||
ofs = (count + i) % leds
|
||||
np[i] = (gamma((sin(ofs / leds * 2 * pi) + 1) * 127),
|
||||
gamma((sin(ofs / leds * 2 * pi + 2/3*pi) + 1) * 127),
|
||||
gamma((sin(ofs / leds * 2 * pi + 4/3*pi) + 1) * 127))
|
||||
count += 0.2
|
||||
before = ticks_ms()
|
||||
await np.async_write()
|
||||
now = ticks_ms()
|
||||
if before + 20 > now:
|
||||
await asyncio.sleep_ms(20 - (now - before))
|
||||
|
||||
print("LEDs should now start rainbowing")
|
||||
asyncio.run(rainbow(np))
|
||||
@@ -9,6 +9,7 @@ import micropython
|
||||
import network
|
||||
import os
|
||||
import time
|
||||
import ubinascii
|
||||
|
||||
# Own modules
|
||||
import app
|
||||
@@ -17,7 +18,8 @@ from mfrc522 import MFRC522
|
||||
from mp3player import MP3Player
|
||||
from nfc import Nfc
|
||||
from rp2_neopixel import NeoPixel
|
||||
from utils import BTreeFileManager, Buttons, SDContext, TimerManager, LedManager
|
||||
from utils import BTreeFileManager, Buttons, SDContext, TimerManager, LedManager, Configuration
|
||||
from webserver import start_webserver
|
||||
|
||||
try:
|
||||
import hwconfig
|
||||
@@ -40,17 +42,30 @@ def setup_wifi():
|
||||
wlan.config(ssid=f"TonberryPicoAP_{machine.unique_id().hex()}", security=wlan.SEC_OPEN)
|
||||
wlan.active(True)
|
||||
|
||||
# disable power management
|
||||
wlan.config(pm=network.WLAN.PM_NONE)
|
||||
|
||||
mac = ubinascii.hexlify(network.WLAN().config('mac'), ':').decode()
|
||||
print(f" mac: {mac}")
|
||||
print(f" channel: {wlan.config('channel')}")
|
||||
print(f" essid: {wlan.config('essid')}")
|
||||
print(f" txpower: {wlan.config('txpower')}")
|
||||
print(f"ifconfig: {wlan.ifconfig()}")
|
||||
|
||||
|
||||
DB_PATH = '/sd/tonberry.db'
|
||||
|
||||
config = Configuration()
|
||||
|
||||
|
||||
def run():
|
||||
asyncio.new_event_loop()
|
||||
# Setup LEDs
|
||||
np = NeoPixel(hwconfig.LED_DIN, hwconfig.LED_COUNT, sm=1)
|
||||
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
|
||||
|
||||
# Wifi with default config
|
||||
setup_wifi()
|
||||
start_webserver()
|
||||
|
||||
# Setup MP3 player
|
||||
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
|
||||
@@ -79,7 +94,8 @@ def run():
|
||||
pin_next=hwconfig.BUTTON_NEXT),
|
||||
playlistdb=lambda _: playlistdb,
|
||||
hwconfig=lambda _: hwconfig,
|
||||
leds=lambda _: LedManager(np))
|
||||
leds=lambda _: LedManager(np),
|
||||
config=lambda _: config)
|
||||
the_app = app.PlayerApp(deps)
|
||||
|
||||
# Start
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
from mfrc522 import MFRC522
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
delay_sum = 0
|
||||
delay_count = 0
|
||||
max_delay = 0
|
||||
|
||||
|
||||
async def latency_test():
|
||||
global delay_sum
|
||||
global delay_count
|
||||
global max_delay
|
||||
global min_delay
|
||||
min_delay = 0xffffffff
|
||||
await asyncio.sleep_ms(1)
|
||||
while True:
|
||||
for _ in range(2000):
|
||||
before = time.ticks_us()
|
||||
await asyncio.sleep(0)
|
||||
after = time.ticks_us()
|
||||
delay = after - before
|
||||
delay_sum += delay
|
||||
delay_count += 1
|
||||
if delay > max_delay:
|
||||
max_delay = delay
|
||||
if delay < min_delay:
|
||||
min_delay = delay
|
||||
await asyncio.sleep_ms(1)
|
||||
print(f"delay (min / max / avg) [µs]: ({min_delay} / {max_delay} / {delay/delay_sum})")
|
||||
|
||||
|
||||
def uid_to_string(uid: list):
|
||||
return '0x' + ''.join(f'{i:02x}' for i in uid)
|
||||
|
||||
|
||||
async def get_tag_uid(reader: MFRC522, poll_interval_ms: int = 50) -> list:
|
||||
'''
|
||||
The maximum measured delay with poll_interval_ms=50 and a reader with tocard_retries=5 is
|
||||
15.9 ms:
|
||||
delay (min / max / avg) [µs]: (360 / 15945 / 1.892923e-06)
|
||||
|
||||
The maximum measured delay dropped to 11.6 ms by setting tocard_retries=1:
|
||||
delay (min / max / avg) [µs]: (368 / 11696 / 6.204211e-06)
|
||||
'''
|
||||
while True:
|
||||
reader.init()
|
||||
|
||||
# For now we omit the tag type
|
||||
(stat, _) = reader.request(reader.REQIDL)
|
||||
if stat == reader.OK:
|
||||
(stat, uid) = reader.SelectTagSN()
|
||||
if stat == reader.OK:
|
||||
print(f"uid={uid_to_string(uid)}")
|
||||
|
||||
await asyncio.sleep_ms(poll_interval_ms)
|
||||
|
||||
|
||||
def main():
|
||||
reader = MFRC522(spi_id=1, sck=10, miso=12, mosi=11, cs=13, rst=9, tocard_retries=1)
|
||||
|
||||
print("")
|
||||
print("Please place card on reader")
|
||||
print("")
|
||||
|
||||
asyncio.create_task(get_tag_uid(reader))
|
||||
asyncio.create_task(latency_test())
|
||||
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,38 +0,0 @@
|
||||
import rp2
|
||||
import network
|
||||
import ubinascii
|
||||
from microdot import Microdot
|
||||
|
||||
rp2.country('DE')
|
||||
|
||||
wlan = network.WLAN(network.AP_IF)
|
||||
wlan.config(ssid='TonberryPico', security=network.WLAN.SEC_OPEN)
|
||||
# Important: we cannot change the ip in station mode, otherwise dhcp won't work!
|
||||
# wlan.ipconfig(addr4='10.0.0.1')
|
||||
wlan.active(True) # loads the firmware
|
||||
while wlan.active() is False:
|
||||
pass
|
||||
wlan.config(pm=network.WLAN.PM_NONE)
|
||||
|
||||
mac = ubinascii.hexlify(network.WLAN().config('mac'), ':').decode()
|
||||
print(f" mac: {mac}")
|
||||
print(f" channel: {wlan.config('channel')}")
|
||||
print(f" essid: {wlan.config('essid')}")
|
||||
print(f" txpower: {wlan.config('txpower')}")
|
||||
print(f"ifconfig: {wlan.ifconfig()}")
|
||||
|
||||
app = Microdot()
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
print("wohoo, a guest :)")
|
||||
print(f" app: {request.app}")
|
||||
print(f" client: {request.client_addr}")
|
||||
print(f" method: {request.method}")
|
||||
print(f" url: {request.url}")
|
||||
print(f" headers: {request.headers}")
|
||||
print(f" cookies: {request.cookies}")
|
||||
return "TonberryPico says 'Hello World!'"
|
||||
|
||||
app.run(port=80)
|
||||
@@ -1,113 +0,0 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import aiorepl
|
||||
import asyncio
|
||||
import machine
|
||||
import micropython
|
||||
import os
|
||||
import time
|
||||
from machine import Pin
|
||||
from math import pi, sin, pow
|
||||
from micropython import const
|
||||
|
||||
# Own modules
|
||||
from audiocore import Audiocore
|
||||
from mp3player import MP3Player
|
||||
from rp2_neopixel import NeoPixel
|
||||
from rp2_sd import SDCard
|
||||
|
||||
micropython.alloc_emergency_exception_buf(100)
|
||||
|
||||
leds = const(10)
|
||||
brightness = 0.5
|
||||
|
||||
|
||||
def gamma(value, X=2.2):
|
||||
return min(max(int(brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
|
||||
|
||||
|
||||
async def rainbow(np, period=10):
|
||||
count = 0.0
|
||||
while True:
|
||||
for i in range(leds):
|
||||
ofs = (count + i) % leds
|
||||
np[i] = (gamma((sin(ofs / leds * 2 * pi) + 1) * 127),
|
||||
gamma((sin(ofs / leds * 2 * pi + 2/3*pi) + 1) * 127),
|
||||
gamma((sin(ofs / leds * 2 * pi + 4/3*pi) + 1) * 127))
|
||||
count += 0.2
|
||||
before = time.ticks_ms()
|
||||
await np.async_write()
|
||||
now = time.ticks_ms()
|
||||
if before + 20 > now:
|
||||
await asyncio.sleep_ms(20 - (now - before))
|
||||
|
||||
|
||||
# Set 8 mA drive strength and fast slew rate
|
||||
machine.mem32[0x4001c004 + 6*4] = 0x67
|
||||
machine.mem32[0x4001c004 + 7*4] = 0x67
|
||||
machine.mem32[0x4001c004 + 8*4] = 0x67
|
||||
|
||||
|
||||
def list_sd():
|
||||
try:
|
||||
sd = SDCard(mosi=Pin(3), miso=Pin(4), sck=Pin(2), ss=Pin(5), baudrate=15000000)
|
||||
except OSError:
|
||||
for i in range(leds):
|
||||
np[i] = (255, 0, 0)
|
||||
np.write()
|
||||
return
|
||||
try:
|
||||
os.mount(sd, '/sd')
|
||||
print(os.listdir(b'/sd'))
|
||||
except OSError as ex:
|
||||
print(f"{ex}")
|
||||
|
||||
|
||||
delay_sum = 0
|
||||
delay_count = 0
|
||||
max_delay = 0
|
||||
|
||||
|
||||
async def latency_test():
|
||||
global delay_sum
|
||||
global delay_count
|
||||
global max_delay
|
||||
await asyncio.sleep_ms(1)
|
||||
while True:
|
||||
for _ in range(2000):
|
||||
before = time.ticks_us()
|
||||
await asyncio.sleep(0)
|
||||
after = time.ticks_us()
|
||||
delay = after - before
|
||||
delay_sum += delay
|
||||
delay_count += 1
|
||||
if delay > max_delay:
|
||||
max_delay = delay
|
||||
await asyncio.sleep_ms(1)
|
||||
print(f"Max delay {max_delay} us, average {delay/delay_sum} us")
|
||||
|
||||
pin = Pin.board.GP16
|
||||
np = NeoPixel(pin, leds)
|
||||
|
||||
# Test SD card
|
||||
list_sd()
|
||||
|
||||
# Test NeoPixel
|
||||
asyncio.create_task(rainbow(np))
|
||||
|
||||
# Test audio
|
||||
audioctx = Audiocore(Pin(8), Pin(6))
|
||||
|
||||
player = MP3Player(audioctx)
|
||||
|
||||
# high prio for proc 1
|
||||
machine.mem32[0x40030000 + 0x00] = 0x10
|
||||
|
||||
testfiles = [b'/sd/' + name for name in os.listdir(b'/sd') if name.endswith(b'mp3')]
|
||||
player.set_playlist(testfiles)
|
||||
asyncio.create_task(player.task())
|
||||
|
||||
|
||||
asyncio.create_task(aiorepl.task({'player': player}))
|
||||
asyncio.get_event_loop().run_forever()
|
||||
53
software/src/tonberry.schema.json
Normal file
53
software/src/tonberry.schema.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"PlaybackPosition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position_seconds": { "type": "number" },
|
||||
"device_uptime": { "type": "number" }
|
||||
},
|
||||
"required": ["position_seconds"]
|
||||
},
|
||||
"AudioFile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"filename": { "type": "string" },
|
||||
"size_bytes": { "type": "integer" },
|
||||
"duration_seconds": { "type": "number" },
|
||||
"last_played_uptime": { "type": "number" },
|
||||
"playback_position": { "$ref": "#/definitions/PlaybackPosition" }
|
||||
},
|
||||
"required": ["id", "filename"]
|
||||
},
|
||||
"Playlist": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"name": { "type": "string" },
|
||||
"audio_files": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/AudioFile" }
|
||||
},
|
||||
"current_track_index": { "type": "integer", "minimum": 0 },
|
||||
"last_played_uptime": { "type": "number" },
|
||||
"playback_position": { "$ref": "#/definitions/PlaybackPosition" }
|
||||
},
|
||||
"required": ["id", "name", "audio_files"]
|
||||
},
|
||||
"NfcTag": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uid": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"linked_type": {
|
||||
"type": "string",
|
||||
"enum": ["audio_file", "playlist"]
|
||||
},
|
||||
"linked_id": { "type": "string", "format": "uuid" }
|
||||
},
|
||||
"required": ["uid", "linked_type", "linked_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
from utils.buttons import Buttons
|
||||
from utils.config import Configuration
|
||||
from utils.leds import LedManager
|
||||
from utils.mbrpartition import MBRPartition
|
||||
from utils.pinindex import get_pin_index
|
||||
@@ -9,5 +10,5 @@ from utils.playlistdb import BTreeDB, BTreeFileManager
|
||||
from utils.sdcontext import SDContext
|
||||
from utils.timer import TimerManager
|
||||
|
||||
__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "get_pin_index", "LedManager", "MBRPartition", "SDContext",
|
||||
"TimerManager"]
|
||||
__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "Configuration", "get_pin_index", "LedManager", "MBRPartition",
|
||||
"SDContext", "TimerManager"]
|
||||
|
||||
59
software/src/utils/config.py
Normal file
59
software/src/utils/config.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
from errno import ENOENT
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
class Configuration:
|
||||
DEFAULT_CONFIG = {
|
||||
'LED_COUNT': 1,
|
||||
'IDLE_TIMEOUT_SECS': 60,
|
||||
'TAG_TIMEOUT_SECS': 5,
|
||||
}
|
||||
|
||||
def __init__(self, config_path='/config.json'):
|
||||
self.config_path = config_path
|
||||
try:
|
||||
with open(self.config_path, 'r') as conf_file:
|
||||
self.config = json.load(conf_file)
|
||||
except OSError as ex:
|
||||
if ex.errno == ENOENT:
|
||||
self.config = Configuration.DEFAULT_CONFIG
|
||||
self._save()
|
||||
else:
|
||||
raise
|
||||
except ValueError as ex:
|
||||
print(f"Warning: Could not load configuration {self.config_path}:\n{ex}")
|
||||
self._move_config_to_backup()
|
||||
self.config = Configuration.DEFAULT_CONFIG
|
||||
|
||||
def _move_config_to_backup(self):
|
||||
# Remove old backup
|
||||
try:
|
||||
os.remove(self.config_path + '.bup')
|
||||
os.rename(self.config_path, self.config_path + '.bup')
|
||||
except OSError as ex:
|
||||
if ex.errno != ENOENT:
|
||||
raise
|
||||
os.sync()
|
||||
|
||||
def _save(self):
|
||||
with open(self.config_path + '.new', 'w') as conf_file:
|
||||
json.dump(self.config, conf_file)
|
||||
self._move_config_to_backup()
|
||||
os.rename(self.config_path + '.new', self.config_path)
|
||||
os.sync()
|
||||
|
||||
def _get(self, key):
|
||||
return self.config.get(key, self.DEFAULT_CONFIG[key])
|
||||
|
||||
def get_led_count(self):
|
||||
return self._get('LED_COUNT')
|
||||
|
||||
def get_idle_timeout(self):
|
||||
return self._get('IDLE_TIMEOUT_SECS')
|
||||
|
||||
def get_tag_timeout(self):
|
||||
return self._get('TAG_TIMEOUT_SECS')
|
||||
41
software/src/webserver.py
Normal file
41
software/src/webserver.py
Normal file
@@ -0,0 +1,41 @@
|
||||
'''
|
||||
SPDX-License-Identifier: MIT
|
||||
Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
|
||||
'''
|
||||
|
||||
import asyncio
|
||||
|
||||
from microdot import Microdot
|
||||
|
||||
webapp = Microdot()
|
||||
server = None
|
||||
|
||||
|
||||
def start_webserver():
|
||||
global server
|
||||
server = asyncio.create_task(webapp.start_server(port=80))
|
||||
|
||||
|
||||
@webapp.route('/')
|
||||
async def index(request):
|
||||
print("wohoo, a guest :)")
|
||||
print(f" app: {request.app}")
|
||||
print(f" client: {request.client_addr}")
|
||||
print(f" method: {request.method}")
|
||||
print(f" url: {request.url}")
|
||||
print(f" headers: {request.headers}")
|
||||
print(f" cookies: {request.cookies}")
|
||||
return "TonberryPico says 'Hello World!'"
|
||||
|
||||
|
||||
@webapp.route('/api/v1/filesystem', methods=['POST'])
|
||||
async def filesystem_post(request):
|
||||
# curl -X POST -d "burp" http://192.168.4.1/api/v1/filesystem
|
||||
print(request)
|
||||
return {'success': False}
|
||||
|
||||
|
||||
@webapp.route('/api/v1/playlist', methods=['POST'])
|
||||
async def playlist_post(request):
|
||||
print(request)
|
||||
return {'success': False}
|
||||
@@ -124,6 +124,19 @@ class FakeLeds:
|
||||
self.state = state
|
||||
|
||||
|
||||
class FakeConfig:
|
||||
def __init__(self): pass
|
||||
|
||||
def get_led_count(self):
|
||||
return 1
|
||||
|
||||
def get_idle_timeout(self):
|
||||
return 60
|
||||
|
||||
def get_tag_timeout(self):
|
||||
return 5
|
||||
|
||||
|
||||
def fake_open(filename, mode):
|
||||
return FakeFile(filename, mode)
|
||||
|
||||
@@ -136,13 +149,14 @@ def faketimermanager(monkeypatch):
|
||||
|
||||
|
||||
def _makedeps(mp3player=FakeMp3Player, nfcreader=FakeNfcReader, buttons=FakeButtons,
|
||||
playlistdb=FakePlaylistDb, hwconfig=FakeHwconfig, leds=FakeLeds):
|
||||
playlistdb=FakePlaylistDb, hwconfig=FakeHwconfig, leds=FakeLeds, config=FakeConfig):
|
||||
return app.Dependencies(mp3player=lambda _: mp3player() if callable(mp3player) else mp3player,
|
||||
nfcreader=lambda x: nfcreader(x) if callable(nfcreader) else nfcreader,
|
||||
buttons=lambda _: buttons() if callable(buttons) else buttons,
|
||||
playlistdb=lambda _: playlistdb() if callable(playlistdb) else playlistdb,
|
||||
hwconfig=lambda _: hwconfig() if callable(hwconfig) else hwconfig,
|
||||
leds=lambda _: leds() if callable(leds) else leds)
|
||||
leds=lambda _: leds() if callable(leds) else leds,
|
||||
config=lambda _: config() if callable(config) else config)
|
||||
|
||||
|
||||
def test_construct_app(micropythonify, faketimermanager):
|
||||
|
||||
Reference in New Issue
Block a user