Compare commits
11 Commits
d8eb61e967
...
2cf88b26ee
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cf88b26ee | |||
| 73da134a12 | |||
| 5c8a61eb27 | |||
| 4c85683fcb | |||
| 355a8bd345 | |||
| f46c045589 | |||
| fe1c1eadf7 | |||
| 743188e1a4 | |||
| cd5939f4ee | |||
| 02954cd87c | |||
| 58f8526d7e |
@@ -357,7 +357,9 @@
|
|||||||
'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)',
|
'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)',
|
||||||
'root.TAGMODE': 'Tag mode',
|
'root.TAGMODE': 'Tag mode',
|
||||||
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain',
|
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain',
|
||||||
'root.VOLUME_MAX': 'Maximum volume (0-255)'
|
'root.VOLUME_MAX': 'Maximum volume (0-255)',
|
||||||
|
'root.VOLUME_BOOT': 'Volume at startup (0-255)',
|
||||||
|
'root.LED_MAX': 'Maximum LED brightness (0-255)'
|
||||||
};
|
};
|
||||||
const config_input_override = {
|
const config_input_override = {
|
||||||
'root.TAGMODE': {
|
'root.TAGMODE': {
|
||||||
@@ -399,6 +401,12 @@
|
|||||||
},
|
},
|
||||||
'root.VOLUME_MAX': {
|
'root.VOLUME_MAX': {
|
||||||
'input-type': 'number'
|
'input-type': 'number'
|
||||||
|
},
|
||||||
|
'root.VOLUME_BOOT': {
|
||||||
|
'input-type': 'number'
|
||||||
|
},
|
||||||
|
'root.LED_MAX': {
|
||||||
|
'input-type': 'number'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,18 @@ class PlayerApp:
|
|||||||
self.leds = deps.leds(self)
|
self.leds = deps.leds(self)
|
||||||
self.tag_mode = self.config.get_tagmode()
|
self.tag_mode = self.config.get_tagmode()
|
||||||
self.volume_max = self.config.get_volume_max()
|
self.volume_max = self.config.get_volume_max()
|
||||||
|
self.volume_pos = 3 # fallback if config.get_volume_boot is nonsense
|
||||||
|
try:
|
||||||
|
for idx, val in enumerate(VOLUME_CURVE):
|
||||||
|
if val >= self.config.get_volume_boot():
|
||||||
|
self.volume_pos = idx
|
||||||
|
break
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
self.playing_tag = None
|
self.playing_tag = None
|
||||||
self.playlist = None
|
self.playlist = None
|
||||||
self.buttons = deps.buttons(self) if deps.buttons is not None else None
|
self.buttons = deps.buttons(self) if deps.buttons is not None else None
|
||||||
self.mp3file = None
|
self.mp3file = None
|
||||||
self.volume_pos = 3
|
|
||||||
self.paused = False
|
self.paused = False
|
||||||
self.playing = False
|
self.playing = False
|
||||||
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ machine.mem32[0x40030000 + 0x00] = 0x10
|
|||||||
|
|
||||||
def setup_wifi(ssid='', passphrase='', sec=network.WLAN.SEC_WPA2_WPA3):
|
def setup_wifi(ssid='', passphrase='', sec=network.WLAN.SEC_WPA2_WPA3):
|
||||||
network.hostname("TonberryPico")
|
network.hostname("TonberryPico")
|
||||||
if ssid == '':
|
if ssid is None or ssid == '':
|
||||||
apname = f"TonberryPicoAP_{machine.unique_id().hex()}"
|
apname = f"TonberryPicoAP_{machine.unique_id().hex()}"
|
||||||
print(f"Create AP {apname}")
|
print(f"Create AP {apname}")
|
||||||
wlan = network.WLAN(network.WLAN.IF_AP)
|
wlan = network.WLAN(network.WLAN.IF_AP)
|
||||||
@@ -50,7 +50,7 @@ def setup_wifi(ssid='', passphrase='', sec=network.WLAN.SEC_WPA2_WPA3):
|
|||||||
print(f"Connect to SSID {ssid} with passphrase {passphrase}...")
|
print(f"Connect to SSID {ssid} with passphrase {passphrase}...")
|
||||||
wlan = network.WLAN()
|
wlan = network.WLAN()
|
||||||
wlan.active(True)
|
wlan.active(True)
|
||||||
wlan.connect(ssid, passphrase, security=sec)
|
wlan.connect(ssid, passphrase if passphrase is not None else '', security=sec)
|
||||||
|
|
||||||
# configure power management
|
# configure power management
|
||||||
wlan.config(pm=network.WLAN.PM_PERFORMANCE)
|
wlan.config(pm=network.WLAN.PM_PERFORMANCE)
|
||||||
@@ -76,7 +76,8 @@ config = Configuration()
|
|||||||
|
|
||||||
# Setup LEDs
|
# Setup LEDs
|
||||||
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
|
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
|
||||||
np.fill((32, 32, 0))
|
led_max = config.get_led_max()
|
||||||
|
np.fill((led_max, led_max, 0))
|
||||||
np.write()
|
np.write()
|
||||||
|
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ def run():
|
|||||||
asyncio.new_event_loop()
|
asyncio.new_event_loop()
|
||||||
|
|
||||||
if machine.Pin(hwconfig.BUTTONS[1], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0:
|
if machine.Pin(hwconfig.BUTTONS[1], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0:
|
||||||
np.fill((0, 0, 32))
|
np.fill((0, 0, led_max))
|
||||||
np.write()
|
np.write()
|
||||||
# Force default access point
|
# Force default access point
|
||||||
setup_wifi('', '')
|
setup_wifi('', '')
|
||||||
@@ -116,7 +117,7 @@ def run():
|
|||||||
buttons=lambda the_app: Buttons(the_app, config, hwconfig),
|
buttons=lambda the_app: Buttons(the_app, config, hwconfig),
|
||||||
playlistdb=lambda _: playlistdb,
|
playlistdb=lambda _: playlistdb,
|
||||||
hwconfig=lambda _: hwconfig,
|
hwconfig=lambda _: hwconfig,
|
||||||
leds=lambda _: LedManager(np),
|
leds=lambda _: LedManager(np, config),
|
||||||
config=lambda _: config)
|
config=lambda _: config)
|
||||||
the_app = app.PlayerApp(deps)
|
the_app = app.PlayerApp(deps)
|
||||||
|
|
||||||
@@ -149,7 +150,9 @@ def builddb():
|
|||||||
|
|
||||||
def error_blink():
|
def error_blink():
|
||||||
while True:
|
while True:
|
||||||
np.fill((32, 0, 0))
|
if machine.Pin(hwconfig.BUTTONS[0], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0:
|
||||||
|
machine.reset()
|
||||||
|
np.fill((led_max, 0, 0))
|
||||||
np.write()
|
np.write()
|
||||||
time.sleep_ms(500)
|
time.sleep_ms(500)
|
||||||
np.fill((0, 0, 0))
|
np.fill((0, 0, 0))
|
||||||
@@ -166,5 +169,5 @@ if __name__ == '__main__':
|
|||||||
sys.print_exception(ex)
|
sys.print_exception(ex)
|
||||||
error_blink()
|
error_blink()
|
||||||
else:
|
else:
|
||||||
np.fill((32, 0, 0))
|
np.fill((led_max, 0, 0))
|
||||||
np.write()
|
np.write()
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ class Configuration:
|
|||||||
'SSID': '',
|
'SSID': '',
|
||||||
'PASSPHRASE': '',
|
'PASSPHRASE': '',
|
||||||
},
|
},
|
||||||
'VOLUME_MAX': 255
|
'VOLUME_MAX': 255,
|
||||||
|
'VOLUME_BOOT': 16,
|
||||||
|
'LED_MAX': 255,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, config_path='/config.json'):
|
def __init__(self, config_path='/config.json'):
|
||||||
@@ -101,6 +103,12 @@ class Configuration:
|
|||||||
def get_volume_max(self) -> int:
|
def get_volume_max(self) -> int:
|
||||||
return self._get('VOLUME_MAX')
|
return self._get('VOLUME_MAX')
|
||||||
|
|
||||||
|
def get_led_max(self) -> int:
|
||||||
|
return self._get('LED_MAX')
|
||||||
|
|
||||||
|
def get_volume_boot(self) -> int:
|
||||||
|
return self._get('VOLUME_BOOT')
|
||||||
|
|
||||||
# For the web API
|
# For the web API
|
||||||
def get_config(self) -> Mapping[str, Any]:
|
def get_config(self) -> Mapping[str, Any]:
|
||||||
return self.config
|
return self.config
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ class LedManager:
|
|||||||
PLAYING = const(1)
|
PLAYING = const(1)
|
||||||
REBOOTING = const(2)
|
REBOOTING = const(2)
|
||||||
|
|
||||||
def __init__(self, np):
|
def __init__(self, np, config):
|
||||||
self.led_state = LedManager.IDLE
|
self.led_state = LedManager.IDLE
|
||||||
|
self.brightness = config.get_led_max() / 255
|
||||||
self.np = np
|
self.np = np
|
||||||
self.brightness = 0.1
|
|
||||||
self.leds = len(self.np)
|
self.leds = len(self.np)
|
||||||
asyncio.create_task(self.run())
|
asyncio.create_task(self.run())
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import board
|
import board
|
||||||
|
import errno
|
||||||
import hwconfig
|
import hwconfig
|
||||||
import json
|
import json
|
||||||
import machine
|
import machine
|
||||||
@@ -202,6 +203,25 @@ async def audiofiles_get(request):
|
|||||||
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}
|
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}
|
||||||
|
|
||||||
|
|
||||||
|
async def stream_to_file(stream, file_, length):
|
||||||
|
data = array('b', range(16384))
|
||||||
|
bytes_copied = 0
|
||||||
|
while True:
|
||||||
|
bytes_read = await stream.readinto(data)
|
||||||
|
if bytes_read == 0:
|
||||||
|
# End of body
|
||||||
|
break
|
||||||
|
bytes_written = file_.write(data[:bytes_read])
|
||||||
|
if bytes_written != bytes_read:
|
||||||
|
# short writes shouldn't happen
|
||||||
|
raise OSError(errno.EIO, 'unexpected short write')
|
||||||
|
bytes_copied += bytes_written
|
||||||
|
if bytes_copied == length:
|
||||||
|
break
|
||||||
|
app.reset_idle_timeout()
|
||||||
|
return bytes_copied
|
||||||
|
|
||||||
|
|
||||||
@webapp.route('/api/v1/audiofiles', methods=['POST'])
|
@webapp.route('/api/v1/audiofiles', methods=['POST'])
|
||||||
async def audiofile_upload(request):
|
async def audiofile_upload(request):
|
||||||
if 'type' not in request.args or request.args['type'] not in ['file', 'directory']:
|
if 'type' not in request.args or request.args['type'] not in ['file', 'directory']:
|
||||||
@@ -218,26 +238,13 @@ async def audiofile_upload(request):
|
|||||||
os.mkdir(path)
|
os.mkdir(path)
|
||||||
return '', 204
|
return '', 204
|
||||||
with open(path, 'wb') as newfile:
|
with open(path, 'wb') as newfile:
|
||||||
data = array('b', range(4096))
|
try:
|
||||||
bytes_copied = 0
|
if length > Request.max_body_length:
|
||||||
while True:
|
bytes_copied = await stream_to_file(request.stream, newfile, length)
|
||||||
try:
|
else:
|
||||||
bytes_read = await request.stream.readinto(data)
|
bytes_copied = newfile.write(request.body)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
return f'read error: {ex}', 500
|
return f'error writing data to file: {ex}', 500
|
||||||
if bytes_read == 0:
|
|
||||||
# End of body
|
|
||||||
break
|
|
||||||
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
|
|
||||||
bytes_copied += bytes_written
|
|
||||||
if bytes_copied == length:
|
|
||||||
break
|
|
||||||
if bytes_copied == length:
|
if bytes_copied == length:
|
||||||
return '', 204
|
return '', 204
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ class FakeConfig:
|
|||||||
def get_volume_max(self):
|
def get_volume_max(self):
|
||||||
return 255
|
return 255
|
||||||
|
|
||||||
|
def get_volume_boot(self):
|
||||||
|
return 16
|
||||||
|
|
||||||
|
|
||||||
def fake_open(filename, mode):
|
def fake_open(filename, mode):
|
||||||
return FakeFile(filename, mode)
|
return FakeFile(filename, mode)
|
||||||
@@ -170,7 +173,7 @@ def test_construct_app(micropythonify, faketimermanager):
|
|||||||
deps = _makedeps(mp3player=fake_mp3)
|
deps = _makedeps(mp3player=fake_mp3)
|
||||||
dut = app.PlayerApp(deps)
|
dut = app.PlayerApp(deps)
|
||||||
fake_mp3 = dut.player
|
fake_mp3 = dut.player
|
||||||
assert fake_mp3.volume is not None
|
assert fake_mp3.volume is not None and fake_mp3.volume >= 16
|
||||||
|
|
||||||
|
|
||||||
def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):
|
def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
|||||||
Reference in New Issue
Block a user