Compare commits
18 Commits
944b6b8f14
...
sd-multibl
| Author | SHA1 | Date | |
|---|---|---|---|
| aa41334ba7 | |||
| 3537a2f1bb | |||
| 39a9c68aae | |||
| 6dee7fff7e | |||
| 6976aa6963 | |||
| 763305c659 | |||
| 6d18437863 | |||
| 2cf88b26ee | |||
| 73da134a12 | |||
| 5c8a61eb27 | |||
| 4c85683fcb | |||
| 355a8bd345 | |||
| f46c045589 | |||
| fe1c1eadf7 | |||
| 743188e1a4 | |||
| cd5939f4ee | |||
| 02954cd87c | |||
| 58f8526d7e |
@@ -357,7 +357,12 @@
|
||||
'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)',
|
||||
'root.TAGMODE': 'Tag mode',
|
||||
'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)',
|
||||
'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': {
|
||||
@@ -391,14 +396,29 @@
|
||||
'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'
|
||||
},
|
||||
'root.VOLUME_BOOT': {
|
||||
'input-type': 'number'
|
||||
},
|
||||
'root.LED_MAX': {
|
||||
'input-type': 'number'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -788,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')
|
||||
@@ -822,7 +846,6 @@
|
||||
if (type === 'directory') {
|
||||
const nested = document.createElement('ul');
|
||||
node.appendChild(nested);
|
||||
node.classList.add('expanded');
|
||||
parent.appendChild(node);
|
||||
return nested;
|
||||
}
|
||||
@@ -909,7 +932,7 @@
|
||||
}
|
||||
if (donecount + 1 === totalcount) {
|
||||
// Reload file list from device
|
||||
onShow();
|
||||
onShow('refresh');
|
||||
} else {
|
||||
uploadFileHelper(totalcount, donecount + 1, destdir, files.slice(1));
|
||||
}
|
||||
@@ -936,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() {
|
||||
@@ -957,7 +980,7 @@
|
||||
}
|
||||
}
|
||||
// Reload file list from device
|
||||
onShow();
|
||||
onShow('refresh');
|
||||
}
|
||||
|
||||
let tree = (() => {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#include "py/obj.h"
|
||||
#include "sd.h"
|
||||
|
||||
// Include MicroPython API.
|
||||
#include "py/mperrno.h"
|
||||
#include "py/mpprint.h"
|
||||
#include "py/runtime.h"
|
||||
|
||||
// This module is RP2 specific
|
||||
@@ -13,6 +16,14 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
void sd_printf(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
mp_vprintf(&mp_sys_stdout_print, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
const mp_obj_type_t sdcard_type;
|
||||
struct sdcard_obj {
|
||||
mp_obj_base_t base;
|
||||
@@ -89,11 +100,14 @@ static mp_obj_t sdcard_writeblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj
|
||||
if (bufinfo.len % SD_SECTOR_SIZE != 0)
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("Buffer length is invalid"));
|
||||
const int nblocks = bufinfo.len / SD_SECTOR_SIZE;
|
||||
for (int block = 0; block < nblocks; block++) {
|
||||
// TODO: Implement CMD25 write multiple blocks
|
||||
if (!sd_writeblock(&self->sd_context, start_block + block, bufinfo.buf + block * SD_SECTOR_SIZE))
|
||||
mp_raise_OSError(MP_EIO);
|
||||
bool ret;
|
||||
if (nblocks > 1) {
|
||||
ret = sd_writeblocks(&self->sd_context, start_block, nblocks, bufinfo.buf);
|
||||
} else {
|
||||
ret = sd_writeblock(&self->sd_context, start_block, bufinfo.buf);
|
||||
}
|
||||
if (!ret)
|
||||
mp_raise_OSError(MP_EIO);
|
||||
return mp_const_none;
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_writeblocks_obj, sdcard_writeblocks);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// #define SD_DEBUG
|
||||
extern void sd_printf(const char *fmt, ...);
|
||||
|
||||
#define SD_R1_ILLEGAL_COMMAND (1 << 2)
|
||||
|
||||
@@ -26,14 +26,14 @@ static bool sd_early_init(void)
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
if (sd_cmd(0, 0, 1, &buf)) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("CMD0 resp %02hhx\n", buf);
|
||||
sd_printf("CMD0 resp %02x\n", buf);
|
||||
#endif
|
||||
if (buf == 0x01) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#ifdef SD_DEBUG
|
||||
printf("CMD0 timeout, try again...\n");
|
||||
sd_printf("CMD0 timeout, try again...\n");
|
||||
#endif
|
||||
}
|
||||
return false;
|
||||
@@ -44,14 +44,14 @@ static bool sd_check_interface_condition(void)
|
||||
uint8_t buf[5];
|
||||
if (sd_cmd(8, 0x000001AA, 5, buf)) {
|
||||
if ((buf[3] & 0xf) != 0x1 || buf[4] != 0xAA) {
|
||||
printf("sd_init: check interface condition failed\n");
|
||||
sd_printf("sd_init: check interface condition failed\n");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (buf[0] & SD_R1_ILLEGAL_COMMAND) {
|
||||
printf("sd_init: check interface condition returned illegal command - old card?\n");
|
||||
sd_printf("sd_init: check interface condition returned illegal command - old card?\n");
|
||||
} else {
|
||||
printf("sd_init: check interface condition failed\n");
|
||||
sd_printf("sd_init: check interface condition failed\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -72,12 +72,12 @@ static bool sd_send_op_cond(void)
|
||||
if (!result) {
|
||||
if (use_acmd && buf & SD_R1_ILLEGAL_COMMAND) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("sd_init: card does not understand ACMD41, try CMD1...\n");
|
||||
sd_printf("sd_init: card does not understand ACMD41, try CMD1...\n");
|
||||
#endif
|
||||
use_acmd = false;
|
||||
continue;
|
||||
} else if (buf != 0x01) {
|
||||
printf("sd_init: send_op_cond failed\n");
|
||||
sd_printf("sd_init: send_op_cond failed\n");
|
||||
return false;
|
||||
} else {
|
||||
continue;
|
||||
@@ -87,7 +87,7 @@ static bool sd_send_op_cond(void)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
printf("sd_init: send_op_cond: timeout waiting for !idle\n");
|
||||
sd_printf("sd_init: send_op_cond: timeout waiting for !idle\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ static void sd_dump_cid [[maybe_unused]] (void)
|
||||
const uint8_t crc = sd_crc7(15, buf);
|
||||
const uint8_t card_crc = buf[15] >> 1;
|
||||
if (card_crc != crc) {
|
||||
printf("CRC mismatch: Got %02hhx, expected %02hhx\n", card_crc, crc);
|
||||
sd_printf("CRC mismatch: Got %02x, expected %02x\n", card_crc, crc);
|
||||
// Some cheap SD cards always report CRC=0, don't fail in that case
|
||||
if (card_crc != 0) {
|
||||
return;
|
||||
@@ -122,8 +122,8 @@ static void sd_dump_cid [[maybe_unused]] (void)
|
||||
uint32_t psn = buf[9] << 24 | buf[10] << 16 | buf[11] << 8 | buf[12];
|
||||
int mdt_year = 2000 + ((buf[13] & 0xf) << 4 | (buf[14] & 0xf0) >> 4);
|
||||
int mdt_month = buf[14] & 0x0f;
|
||||
printf("CID: mid: %02hhx, oid: %.2s, pnm: %.5s, prv: %02hhx, psn: %08" PRIx32 ", mdt_year: %d, mdt_month: %d\n",
|
||||
mid, oid, pnm, prv, psn, mdt_year, mdt_month);
|
||||
sd_printf("CID: mid: %02x, oid: %.2s, pnm: %.5s, prv: %02x, psn: %08" PRIx32 ", mdt_year: %d, mdt_month: %d\n",
|
||||
mid, oid, pnm, prv, psn, mdt_year, mdt_month);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,13 +131,13 @@ static bool sd_read_csd(struct sd_context *sd_context)
|
||||
{
|
||||
uint8_t buf[16];
|
||||
if (!sd_cmd_read(9, 0, 16, buf)) {
|
||||
printf("Failed to read CSD\n");
|
||||
sd_printf("Failed to read CSD\n");
|
||||
return false;
|
||||
}
|
||||
const uint8_t crc = sd_crc7(15, buf);
|
||||
const uint8_t card_crc = buf[15] >> 1;
|
||||
if (card_crc != crc) {
|
||||
printf("CRC mismatch: Got %02hhx, expected %02hhx\n", card_crc, crc);
|
||||
sd_printf("CRC mismatch: Got %02x, expected %02x\n", card_crc, crc);
|
||||
// Some cheap SD cards always report CRC=0, don't fail in that case
|
||||
if (card_crc != 0) {
|
||||
return false;
|
||||
@@ -151,12 +151,12 @@ static bool sd_read_csd(struct sd_context *sd_context)
|
||||
switch (csd_ver) {
|
||||
case 0: {
|
||||
if (sd_context->sdhc_sdxc) {
|
||||
printf("sd_init: Got CSD v1.0 but card is SDHC/SDXC?\n");
|
||||
sd_printf("sd_init: Got CSD v1.0 but card is SDHC/SDXC?\n");
|
||||
return false;
|
||||
}
|
||||
const unsigned read_bl_len = buf[5] & 0xf;
|
||||
if (read_bl_len < 9 || read_bl_len > 11) {
|
||||
printf("Invalid read_bl_len in CSD 1.0\n");
|
||||
sd_printf("Invalid read_bl_len in CSD 1.0\n");
|
||||
return false;
|
||||
}
|
||||
blocksize = 1 << (buf[5] & 0xf);
|
||||
@@ -176,15 +176,15 @@ static bool sd_read_csd(struct sd_context *sd_context)
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
printf("sd_init: Got CSD v3.0, but SDUC does not support SPI.\n");
|
||||
sd_printf("sd_init: Got CSD v3.0, but SDUC does not support SPI.\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
sd_context->blocks = blocks;
|
||||
sd_context->blocksize = blocksize;
|
||||
#ifdef SD_DEBUG
|
||||
printf("CSD version %u.0, blocksize %u, blocks %u, capacity %llu MiB, max speed %u\n", version, blocksize, blocks,
|
||||
((uint64_t)blocksize * blocks) / (1024 * 1024), max_speed);
|
||||
sd_printf("CSD version %u.0, blocksize %u, blocks %u, capacity %u MiB, max speed %u\n", version, blocksize, blocks,
|
||||
(uint32_t)(((uint64_t)blocksize * blocks) / (1024 * 1024)), max_speed);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
@@ -205,11 +205,11 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
|
||||
|
||||
uint32_t ocr;
|
||||
if (!sd_read_ocr(&ocr)) {
|
||||
printf("sd_init: read OCR failed\n");
|
||||
sd_printf("sd_init: read OCR failed\n");
|
||||
goto out_spi;
|
||||
}
|
||||
if ((ocr & 0x00380000) != 0x00380000) {
|
||||
printf("sd_init: unsupported card voltage range\n");
|
||||
sd_printf("sd_init: unsupported card voltage range\n");
|
||||
goto out_spi;
|
||||
}
|
||||
|
||||
@@ -219,11 +219,11 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
|
||||
sd_spi_set_bitrate(rate);
|
||||
|
||||
if (!sd_read_ocr(&ocr)) {
|
||||
printf("sd_init: read OCR failed\n");
|
||||
sd_printf("sd_init: read OCR failed\n");
|
||||
goto out_spi;
|
||||
}
|
||||
if (!(ocr & (1 << 31))) {
|
||||
printf("sd_init: card not powered up but !idle?\n");
|
||||
sd_printf("sd_init: card not powered up but !idle?\n");
|
||||
goto out_spi;
|
||||
}
|
||||
sd_context->sdhc_sdxc = (ocr & (1 << 30));
|
||||
@@ -234,20 +234,20 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
|
||||
|
||||
if (sd_context->blocksize != SD_SECTOR_SIZE) {
|
||||
if (sd_context->blocksize != 1024 && sd_context->blocksize != 2048) {
|
||||
printf("sd_init: Unsupported block size %u\n", sd_context->blocksize);
|
||||
sd_printf("sd_init: Unsupported block size %u\n", sd_context->blocksize);
|
||||
goto out_spi;
|
||||
}
|
||||
// Attempt SET_BLOCKLEN command
|
||||
uint8_t resp[1];
|
||||
if (!sd_cmd(16, SD_SECTOR_SIZE, 1, resp)) {
|
||||
printf("sd_init: SET_BLOCKLEN failed\n");
|
||||
sd_printf("sd_init: SET_BLOCKLEN failed\n");
|
||||
goto out_spi;
|
||||
}
|
||||
// Successfully set blocksize to SD_SECTOR_SIZE, adjust context
|
||||
sd_context->blocks *= sd_context->blocksize / SD_SECTOR_SIZE;
|
||||
#ifdef SD_DEBUG
|
||||
printf("Adjusted blocksize from %u to 512, card now has %u blocks\n", sd_context->blocksize,
|
||||
sd_context->blocks);
|
||||
sd_printf("Adjusted blocksize from %u to 512, card now has %u blocks\n", sd_context->blocksize,
|
||||
sd_context->blocks);
|
||||
#endif
|
||||
sd_context->blocksize = SD_SECTOR_SIZE;
|
||||
}
|
||||
@@ -307,7 +307,7 @@ bool sd_readblock_complete(struct sd_context *sd_context)
|
||||
|
||||
bool sd_readblock_is_complete(struct sd_context *sd_context) { return sd_cmd_read_is_complete(); }
|
||||
|
||||
bool sd_writeblock(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE])
|
||||
bool sd_writeblock(struct sd_context *sd_context, const size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE])
|
||||
{
|
||||
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
||||
return false;
|
||||
@@ -319,3 +319,20 @@ bool sd_writeblock(struct sd_context *sd_context, size_t sector_num, uint8_t buf
|
||||
}
|
||||
return sd_cmd_write(24, addr, SD_SECTOR_SIZE, buffer);
|
||||
}
|
||||
|
||||
bool sd_writeblocks(struct sd_context *sd_context, const size_t sector_num, const size_t sectors, uint8_t *const buffer)
|
||||
{
|
||||
if (!sd_context->initialized || sector_num + sectors >= sd_context->blocks)
|
||||
return false;
|
||||
|
||||
if (!sd_context->sdhc_sdxc) {
|
||||
// Don't use multi-block writes for SDSC for now
|
||||
// Need to configure WRITE_BL_LEN correctly
|
||||
for (size_t sector = 0; sector < sectors; ++sector) {
|
||||
if (!sd_writeblock(sd_context, sector_num + sector, buffer + sector * SD_SECTOR_SIZE))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return sd_cmd_write_multiple(25, sector_num, sectors, SD_SECTOR_SIZE, buffer);
|
||||
}
|
||||
|
||||
@@ -24,3 +24,4 @@ bool sd_readblock_complete(struct sd_context *context);
|
||||
bool sd_readblock_is_complete(struct sd_context *context);
|
||||
|
||||
bool sd_writeblock(struct sd_context *context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE]);
|
||||
bool sd_writeblocks(struct sd_context *sd_context, const size_t sector_num, const size_t sectors, uint8_t *buffer);
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <pico/time.h>
|
||||
#include <string.h>
|
||||
|
||||
extern void sd_printf(const char *fmt, ...);
|
||||
|
||||
typedef enum { DMA_READ_TOKEN, DMA_READ, DMA_IDLE } sd_dma_state;
|
||||
|
||||
struct sd_dma_context {
|
||||
@@ -222,7 +224,7 @@ bool sd_cmd_read_complete(void)
|
||||
sd_spi_read_blocking(0xff, &buf, 1);
|
||||
if (sd_spi_context.sd_dma_context.read_token_buf != 0xfe) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("read failed: invalid read token %02hhx\n", sd_spi_context.sd_dma_context.read_token_buf);
|
||||
sd_printf("read failed: invalid read token %02x\n", sd_spi_context.sd_dma_context.read_token_buf);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
@@ -231,7 +233,7 @@ bool sd_cmd_read_complete(void)
|
||||
const uint16_t act_crc = sd_spi_context.sd_dma_context.crc_buf[0] << 8 | sd_spi_context.sd_dma_context.crc_buf[1];
|
||||
if (act_crc != expect_crc) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("read CRC fail: got %04hx, expected %04hx\n", act_crc, expect_crc);
|
||||
sd_printf("read CRC fail: got %04x, expected %04x\n", act_crc, expect_crc);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
@@ -239,10 +241,9 @@ bool sd_cmd_read_complete(void)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen])
|
||||
static bool sd_cmd_write_begin(uint8_t cmd, uint32_t arg)
|
||||
{
|
||||
uint8_t buf[2];
|
||||
const uint16_t crc = sd_crc16(datalen, data);
|
||||
uint8_t buf[1];
|
||||
sd_spi_cmd_send(cmd, arg);
|
||||
// Read up to 8 garbage bytes (0xff), followed by R1 (MSB is zero)
|
||||
bool got_r1 = false;
|
||||
@@ -253,9 +254,20 @@ bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[cons
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!got_r1 || buf[0] != 0x00)
|
||||
goto abort;
|
||||
buf[0] = 0xfe;
|
||||
if (!got_r1 || buf[0] != 0x00) {
|
||||
#ifdef SD_DEBUG
|
||||
sd_printf("write cmd fail: %02x\n", buf[0]);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool sd_cmd_write_block(uint8_t token, unsigned datalen, uint8_t data[const static datalen])
|
||||
{
|
||||
uint8_t buf[2];
|
||||
const uint16_t crc = sd_crc16(datalen, data);
|
||||
buf[0] = token;
|
||||
sd_spi_write_blocking(buf, 1);
|
||||
sd_spi_write_blocking(data, datalen);
|
||||
buf[0] = crc >> 8;
|
||||
@@ -264,11 +276,16 @@ bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[cons
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
if ((buf[0] & 0x1f) != 0x5) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("Write fail: %2hhx\n", buf[0]);
|
||||
sd_printf("Write fail: %2x\n", buf[0]);
|
||||
#endif
|
||||
goto abort;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool sd_cmd_write_wait_nbusy(void)
|
||||
{
|
||||
uint8_t buf[1];
|
||||
int timeout = 0;
|
||||
bool got_done = false;
|
||||
for (timeout = 0; timeout < 131072; ++timeout) {
|
||||
@@ -279,18 +296,73 @@ bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[cons
|
||||
}
|
||||
}
|
||||
#ifdef SD_DEBUG
|
||||
printf("dbg write end: %d, %2hhx\n", timeout, buf[0]);
|
||||
sd_printf("dbg write end: %d, %2x\n", timeout, buf[0]);
|
||||
#endif
|
||||
if (!got_done)
|
||||
return got_done;
|
||||
}
|
||||
|
||||
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen])
|
||||
{
|
||||
#ifdef SD_DEBUG
|
||||
sd_printf("write 1 block at %u\n", arg);
|
||||
#endif
|
||||
uint8_t buf[2];
|
||||
if (!sd_cmd_write_begin(cmd, arg))
|
||||
goto abort;
|
||||
if (!sd_cmd_write_block(0xfe, datalen, data))
|
||||
goto abort;
|
||||
if (!sd_cmd_write_wait_nbusy())
|
||||
goto abort;
|
||||
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
#ifdef SD_DEBUG
|
||||
sd_printf("write ok\n");
|
||||
#endif
|
||||
return true;
|
||||
|
||||
abort:
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
#ifdef SD_DEBUG
|
||||
sd_printf("write fail\n");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sd_cmd_write_multiple(uint8_t cmd, uint32_t arg, unsigned blocks, unsigned datalen, uint8_t *const data)
|
||||
{
|
||||
#ifdef SD_DEBUG
|
||||
sd_printf("write %u blocks at %u\n", blocks, arg);
|
||||
#endif
|
||||
uint8_t buf[2];
|
||||
if (!sd_cmd_write_begin(cmd, arg))
|
||||
goto abort;
|
||||
for (unsigned i = 0; i < blocks; ++i) {
|
||||
if (!sd_cmd_write_block(0b11111100, datalen, data + datalen * i))
|
||||
goto abort;
|
||||
if (!sd_cmd_write_wait_nbusy())
|
||||
goto abort;
|
||||
}
|
||||
buf[0] = 0b11111101;
|
||||
buf[1] = 0xff;
|
||||
sd_spi_write_blocking(buf, 2);
|
||||
if (!sd_cmd_write_wait_nbusy())
|
||||
goto abort;
|
||||
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
#ifdef SD_DEBUG
|
||||
sd_printf("write ok\n");
|
||||
#endif
|
||||
return true;
|
||||
|
||||
abort:
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
#ifdef SD_DEBUG
|
||||
sd_printf("write fail\n");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,3 +27,4 @@ bool sd_cmd_read_complete(void);
|
||||
bool sd_cmd_read_is_complete(void);
|
||||
|
||||
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen]);
|
||||
bool sd_cmd_write_multiple(uint8_t cmd, uint32_t arg, unsigned blocks, unsigned datalen, uint8_t *const data);
|
||||
|
||||
@@ -53,11 +53,18 @@ class PlayerApp:
|
||||
self.leds = deps.leds(self)
|
||||
self.tag_mode = self.config.get_tagmode()
|
||||
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.playlist = None
|
||||
self.buttons = deps.buttons(self) if deps.buttons is not None else None
|
||||
self.mp3file = None
|
||||
self.volume_pos = 3
|
||||
self.paused = False
|
||||
self.playing = False
|
||||
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||
@@ -75,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()
|
||||
|
||||
@@ -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 == '':
|
||||
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, security=sec)
|
||||
wlan.connect(ssid, passphrase if passphrase is not None else '', security=security)
|
||||
|
||||
# configure power management
|
||||
wlan.config(pm=network.WLAN.PM_PERFORMANCE)
|
||||
@@ -76,7 +74,8 @@ config = Configuration()
|
||||
|
||||
# Setup LEDs
|
||||
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()
|
||||
|
||||
|
||||
@@ -84,25 +83,26 @@ def run():
|
||||
asyncio.new_event_loop()
|
||||
|
||||
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()
|
||||
# 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:
|
||||
|
||||
@@ -116,7 +116,7 @@ def run():
|
||||
buttons=lambda the_app: Buttons(the_app, config, hwconfig),
|
||||
playlistdb=lambda _: playlistdb,
|
||||
hwconfig=lambda _: hwconfig,
|
||||
leds=lambda _: LedManager(np),
|
||||
leds=lambda _: LedManager(np, config),
|
||||
config=lambda _: config)
|
||||
the_app = app.PlayerApp(deps)
|
||||
|
||||
@@ -129,27 +129,11 @@ 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:
|
||||
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()
|
||||
time.sleep_ms(500)
|
||||
np.fill((0, 0, 0))
|
||||
@@ -166,5 +150,5 @@ if __name__ == '__main__':
|
||||
sys.print_exception(ex)
|
||||
error_blink()
|
||||
else:
|
||||
np.fill((32, 0, 0))
|
||||
np.fill((led_max, 0, 0))
|
||||
np.write()
|
||||
|
||||
@@ -26,8 +26,11 @@ class Configuration:
|
||||
'WIFI': {
|
||||
'SSID': '',
|
||||
'PASSPHRASE': '',
|
||||
'SECURITY': 'wpa_wpa2',
|
||||
},
|
||||
'VOLUME_MAX': 255
|
||||
'VOLUME_MAX': 255,
|
||||
'VOLUME_BOOT': 16,
|
||||
'LED_MAX': 255,
|
||||
}
|
||||
|
||||
def __init__(self, config_path='/config.json'):
|
||||
@@ -98,9 +101,18 @@ 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')
|
||||
|
||||
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
|
||||
def get_config(self) -> Mapping[str, Any]:
|
||||
return self.config
|
||||
@@ -118,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()
|
||||
|
||||
@@ -12,10 +12,10 @@ class LedManager:
|
||||
PLAYING = const(1)
|
||||
REBOOTING = const(2)
|
||||
|
||||
def __init__(self, np):
|
||||
def __init__(self, np, config):
|
||||
self.led_state = LedManager.IDLE
|
||||
self.brightness = config.get_led_max() / 255
|
||||
self.np = np
|
||||
self.brightness = 0.1
|
||||
self.leds = len(self.np)
|
||||
asyncio.create_task(self.run())
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
|
||||
|
||||
import asyncio
|
||||
import board
|
||||
import errno
|
||||
import hwconfig
|
||||
import json
|
||||
import machine
|
||||
@@ -202,6 +203,25 @@ async def audiofiles_get(request):
|
||||
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}
|
||||
|
||||
|
||||
async def stream_to_file(stream, file_, length):
|
||||
data = array('b', range(4096))
|
||||
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'])
|
||||
async def audiofile_upload(request):
|
||||
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)
|
||||
return '', 204
|
||||
with open(path, 'wb') as newfile:
|
||||
data = array('b', range(4096))
|
||||
bytes_copied = 0
|
||||
while True:
|
||||
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
|
||||
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
|
||||
try:
|
||||
if length > Request.max_body_length:
|
||||
bytes_copied = await stream_to_file(request.stream, newfile, length)
|
||||
else:
|
||||
bytes_copied = newfile.write(request.body)
|
||||
except OSError as ex:
|
||||
return f'error writing data to file: {ex}', 500
|
||||
if bytes_copied == length:
|
||||
return '', 204
|
||||
else:
|
||||
|
||||
@@ -139,6 +139,12 @@ class FakeConfig:
|
||||
def get_tagmode(self):
|
||||
return 'tagremains'
|
||||
|
||||
def get_volume_max(self):
|
||||
return 255
|
||||
|
||||
def get_volume_boot(self):
|
||||
return 16
|
||||
|
||||
|
||||
def fake_open(filename, mode):
|
||||
return FakeFile(filename, mode)
|
||||
@@ -167,7 +173,7 @@ def test_construct_app(micropythonify, faketimermanager):
|
||||
deps = _makedeps(mp3player=fake_mp3)
|
||||
dut = app.PlayerApp(deps)
|
||||
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):
|
||||
|
||||
@@ -15,6 +15,8 @@ option(ENABLE_PLAY_TEST "Enable mp3 playback test" OFF)
|
||||
option(ENABLE_SD_READ_CRC "Enable crc check when reading from sd card" OFF)
|
||||
option(ENABLE_SD_DEBUG "Enable debug output for sd card driver" OFF)
|
||||
|
||||
set(PICO_USE_FASTEST_SUPPORTED_CLOCK 1)
|
||||
|
||||
# initialize the Raspberry Pi Pico SDK
|
||||
pico_sdk_init()
|
||||
|
||||
@@ -62,7 +64,7 @@ add_subdirectory(../../lib/helix_mp3 helix_mp3)
|
||||
|
||||
target_link_libraries(standalone_mp3 PRIVATE pico_stdlib hardware_dma hardware_spi hardware_sync hardware_pio helix_mp3)
|
||||
target_include_directories(standalone_mp3 PRIVATE ${SD_LIB_DIR})
|
||||
target_compile_options(standalone_mp3 PRIVATE -Og -DSD_DEBUG)
|
||||
target_compile_options(standalone_mp3 PRIVATE -Og)
|
||||
|
||||
|
||||
pico_add_extra_outputs(standalone_mp3)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "sd.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -20,6 +21,14 @@ extern void sd_spi_dbg_clk(const int div, const int frac);
|
||||
|
||||
extern void sd_spi_dbg_loop(void);
|
||||
|
||||
void sd_printf(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
#define MAX_VOLUME 0x8000u
|
||||
|
||||
void __time_critical_func(volume_adjust)(int16_t *restrict buf, size_t samples, uint16_t scalef)
|
||||
@@ -157,7 +166,7 @@ static void write_test(struct sd_context *sd_context)
|
||||
data_buffer[i] ^= 0xff;
|
||||
}
|
||||
|
||||
if (!sd_writeblock(sd_context, 0, data_buffer)) {
|
||||
if (!sd_writeblocks(sd_context, 0, sizeof(data_buffer) / SD_SECTOR_SIZE, data_buffer)) {
|
||||
printf("sd_writeblock failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user