Got short sounds playing straight out of internal flash with upload_track and ffmpeg
ffmpeg -i luis_fonsi_despacito.mp3 -t 3 -ar 8000 -ac 1 -f u8 despacito.raw
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"board": {
|
||||||
|
"active_layer": 0,
|
||||||
|
"active_layer_preset": "",
|
||||||
|
"auto_track_width": true,
|
||||||
|
"hidden_netclasses": [],
|
||||||
|
"hidden_nets": [],
|
||||||
|
"high_contrast_mode": 0,
|
||||||
|
"net_color_mode": 1,
|
||||||
|
"opacity": {
|
||||||
|
"images": 0.6,
|
||||||
|
"pads": 1.0,
|
||||||
|
"shapes": 1.0,
|
||||||
|
"tracks": 1.0,
|
||||||
|
"vias": 1.0,
|
||||||
|
"zones": 0.6
|
||||||
|
},
|
||||||
|
"selection_filter": {
|
||||||
|
"dimensions": true,
|
||||||
|
"footprints": true,
|
||||||
|
"graphics": true,
|
||||||
|
"keepouts": true,
|
||||||
|
"lockedItems": false,
|
||||||
|
"otherItems": true,
|
||||||
|
"pads": true,
|
||||||
|
"text": true,
|
||||||
|
"tracks": true,
|
||||||
|
"vias": true,
|
||||||
|
"zones": true
|
||||||
|
},
|
||||||
|
"visible_items": [
|
||||||
|
"vias",
|
||||||
|
"footprint_text",
|
||||||
|
"footprint_anchors",
|
||||||
|
"ratsnest",
|
||||||
|
"grid",
|
||||||
|
"footprints_front",
|
||||||
|
"footprints_back",
|
||||||
|
"footprint_values",
|
||||||
|
"footprint_references",
|
||||||
|
"tracks",
|
||||||
|
"drc_errors",
|
||||||
|
"drawing_sheet",
|
||||||
|
"bitmaps",
|
||||||
|
"pads",
|
||||||
|
"zones",
|
||||||
|
"drc_warnings",
|
||||||
|
"locked_item_shadows",
|
||||||
|
"conflict_shadows",
|
||||||
|
"shapes"
|
||||||
|
],
|
||||||
|
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
|
||||||
|
"zone_display_mode": 0
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"repo_type": "",
|
||||||
|
"repo_username": "",
|
||||||
|
"ssh_key": ""
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"filename": "baby-mobile-panel.kicad_prl",
|
||||||
|
"version": 5
|
||||||
|
},
|
||||||
|
"net_inspector_panel": {
|
||||||
|
"col_hidden": [],
|
||||||
|
"col_order": [],
|
||||||
|
"col_widths": [],
|
||||||
|
"custom_group_rules": [],
|
||||||
|
"expanded_rows": [],
|
||||||
|
"filter_by_net_name": true,
|
||||||
|
"filter_by_netclass": true,
|
||||||
|
"filter_text": "",
|
||||||
|
"group_by_constraint": false,
|
||||||
|
"group_by_netclass": false,
|
||||||
|
"show_unconnected_nets": false,
|
||||||
|
"show_zero_pad_nets": false,
|
||||||
|
"sort_ascending": true,
|
||||||
|
"sorting_column": -1
|
||||||
|
},
|
||||||
|
"open_jobsets": [],
|
||||||
|
"project": {
|
||||||
|
"files": []
|
||||||
|
},
|
||||||
|
"schematic": {
|
||||||
|
"selection_filter": {
|
||||||
|
"graphics": true,
|
||||||
|
"images": true,
|
||||||
|
"labels": true,
|
||||||
|
"lockedItems": false,
|
||||||
|
"otherItems": true,
|
||||||
|
"pins": true,
|
||||||
|
"symbols": true,
|
||||||
|
"text": true,
|
||||||
|
"wires": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"board": {
|
||||||
|
"3dviewports": [],
|
||||||
|
"design_settings": {
|
||||||
|
"defaults": {
|
||||||
|
"apply_defaults_to_fp_fields": false,
|
||||||
|
"apply_defaults_to_fp_shapes": false,
|
||||||
|
"apply_defaults_to_fp_text": false,
|
||||||
|
"board_outline_line_width": 0.05,
|
||||||
|
"copper_line_width": 0.2,
|
||||||
|
"copper_text_italic": false,
|
||||||
|
"copper_text_size_h": 1.5,
|
||||||
|
"copper_text_size_v": 1.5,
|
||||||
|
"copper_text_thickness": 0.3,
|
||||||
|
"copper_text_upright": false,
|
||||||
|
"courtyard_line_width": 0.05,
|
||||||
|
"dimension_precision": 4,
|
||||||
|
"dimension_units": 3,
|
||||||
|
"dimensions": {
|
||||||
|
"arrow_length": 1270000,
|
||||||
|
"extension_offset": 500000,
|
||||||
|
"keep_text_aligned": true,
|
||||||
|
"suppress_zeroes": true,
|
||||||
|
"text_position": 0,
|
||||||
|
"units_format": 0
|
||||||
|
},
|
||||||
|
"fab_line_width": 0.1,
|
||||||
|
"fab_text_italic": false,
|
||||||
|
"fab_text_size_h": 1.0,
|
||||||
|
"fab_text_size_v": 1.0,
|
||||||
|
"fab_text_thickness": 0.15,
|
||||||
|
"fab_text_upright": false,
|
||||||
|
"other_line_width": 0.1,
|
||||||
|
"other_text_italic": false,
|
||||||
|
"other_text_size_h": 1.0,
|
||||||
|
"other_text_size_v": 1.0,
|
||||||
|
"other_text_thickness": 0.15,
|
||||||
|
"other_text_upright": false,
|
||||||
|
"pads": {
|
||||||
|
"drill": 0.8,
|
||||||
|
"height": 1.27,
|
||||||
|
"width": 2.54
|
||||||
|
},
|
||||||
|
"silk_line_width": 0.1,
|
||||||
|
"silk_text_italic": false,
|
||||||
|
"silk_text_size_h": 1.0,
|
||||||
|
"silk_text_size_v": 1.0,
|
||||||
|
"silk_text_thickness": 0.1,
|
||||||
|
"silk_text_upright": false,
|
||||||
|
"zones": {
|
||||||
|
"min_clearance": 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diff_pair_dimensions": [],
|
||||||
|
"drc_exclusions": [],
|
||||||
|
"meta": {
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"rule_severities": {
|
||||||
|
"annular_width": "error",
|
||||||
|
"clearance": "error",
|
||||||
|
"connection_width": "warning",
|
||||||
|
"copper_edge_clearance": "error",
|
||||||
|
"copper_sliver": "warning",
|
||||||
|
"courtyards_overlap": "error",
|
||||||
|
"creepage": "error",
|
||||||
|
"diff_pair_gap_out_of_range": "error",
|
||||||
|
"diff_pair_uncoupled_length_too_long": "error",
|
||||||
|
"drill_out_of_range": "error",
|
||||||
|
"duplicate_footprints": "warning",
|
||||||
|
"extra_footprint": "warning",
|
||||||
|
"footprint": "error",
|
||||||
|
"footprint_filters_mismatch": "ignore",
|
||||||
|
"footprint_symbol_mismatch": "warning",
|
||||||
|
"footprint_type_mismatch": "ignore",
|
||||||
|
"hole_clearance": "error",
|
||||||
|
"hole_to_hole": "warning",
|
||||||
|
"holes_co_located": "warning",
|
||||||
|
"invalid_outline": "error",
|
||||||
|
"isolated_copper": "warning",
|
||||||
|
"item_on_disabled_layer": "error",
|
||||||
|
"items_not_allowed": "error",
|
||||||
|
"length_out_of_range": "error",
|
||||||
|
"lib_footprint_issues": "warning",
|
||||||
|
"lib_footprint_mismatch": "warning",
|
||||||
|
"malformed_courtyard": "error",
|
||||||
|
"microvia_drill_out_of_range": "error",
|
||||||
|
"mirrored_text_on_front_layer": "warning",
|
||||||
|
"missing_courtyard": "ignore",
|
||||||
|
"missing_footprint": "warning",
|
||||||
|
"net_conflict": "warning",
|
||||||
|
"nonmirrored_text_on_back_layer": "warning",
|
||||||
|
"npth_inside_courtyard": "ignore",
|
||||||
|
"padstack": "warning",
|
||||||
|
"pth_inside_courtyard": "ignore",
|
||||||
|
"shorting_items": "error",
|
||||||
|
"silk_edge_clearance": "warning",
|
||||||
|
"silk_over_copper": "warning",
|
||||||
|
"silk_overlap": "warning",
|
||||||
|
"skew_out_of_range": "error",
|
||||||
|
"solder_mask_bridge": "error",
|
||||||
|
"starved_thermal": "error",
|
||||||
|
"text_height": "warning",
|
||||||
|
"text_on_edge_cuts": "error",
|
||||||
|
"text_thickness": "warning",
|
||||||
|
"through_hole_pad_without_hole": "error",
|
||||||
|
"too_many_vias": "error",
|
||||||
|
"track_angle": "error",
|
||||||
|
"track_dangling": "warning",
|
||||||
|
"track_segment_length": "error",
|
||||||
|
"track_width": "error",
|
||||||
|
"tracks_crossing": "error",
|
||||||
|
"unconnected_items": "error",
|
||||||
|
"unresolved_variable": "error",
|
||||||
|
"via_dangling": "warning",
|
||||||
|
"zones_intersect": "error"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"max_error": 0.005,
|
||||||
|
"min_clearance": 0.0,
|
||||||
|
"min_connection": 0.0,
|
||||||
|
"min_copper_edge_clearance": 0.5,
|
||||||
|
"min_groove_width": 0.0,
|
||||||
|
"min_hole_clearance": 0.25,
|
||||||
|
"min_hole_to_hole": 0.25,
|
||||||
|
"min_microvia_diameter": 0.2,
|
||||||
|
"min_microvia_drill": 0.1,
|
||||||
|
"min_resolved_spokes": 2,
|
||||||
|
"min_silk_clearance": 0.0,
|
||||||
|
"min_text_height": 0.8,
|
||||||
|
"min_text_thickness": 0.08,
|
||||||
|
"min_through_hole_diameter": 0.3,
|
||||||
|
"min_track_width": 0.15,
|
||||||
|
"min_via_annular_width": 0.1,
|
||||||
|
"min_via_diameter": 0.4,
|
||||||
|
"solder_mask_to_copper_clearance": 0.0,
|
||||||
|
"use_height_for_length_calcs": true
|
||||||
|
},
|
||||||
|
"teardrop_options": [
|
||||||
|
{
|
||||||
|
"td_onpthpad": true,
|
||||||
|
"td_onroundshapesonly": false,
|
||||||
|
"td_onsmdpad": true,
|
||||||
|
"td_ontrackend": false,
|
||||||
|
"td_onvia": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"teardrop_parameters": [
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_round_shape",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_rect_shape",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_track_end",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"track_widths": [],
|
||||||
|
"tuning_pattern_settings": {
|
||||||
|
"diff_pair_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 1.0
|
||||||
|
},
|
||||||
|
"diff_pair_skew_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 0.6
|
||||||
|
},
|
||||||
|
"single_track_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 0.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"via_dimensions": [],
|
||||||
|
"zones_allow_external_fillets": false
|
||||||
|
},
|
||||||
|
"ipc2581": {
|
||||||
|
"dist": "",
|
||||||
|
"distpn": "",
|
||||||
|
"internal_id": "",
|
||||||
|
"mfg": "",
|
||||||
|
"mpn": ""
|
||||||
|
},
|
||||||
|
"layer_pairs": [],
|
||||||
|
"layer_presets": [],
|
||||||
|
"viewports": []
|
||||||
|
},
|
||||||
|
"boards": [],
|
||||||
|
"cvpcb": {
|
||||||
|
"equivalence_files": []
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"pinned_footprint_libs": [],
|
||||||
|
"pinned_symbol_libs": []
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"filename": "baby-mobile-panel.kicad_pro",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"net_settings": {
|
||||||
|
"classes": [
|
||||||
|
{
|
||||||
|
"bus_width": 12,
|
||||||
|
"clearance": 0.2,
|
||||||
|
"diff_pair_gap": 0.25,
|
||||||
|
"diff_pair_via_gap": 0.25,
|
||||||
|
"diff_pair_width": 0.2,
|
||||||
|
"line_style": 0,
|
||||||
|
"microvia_diameter": 0.3,
|
||||||
|
"microvia_drill": 0.1,
|
||||||
|
"name": "Default",
|
||||||
|
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||||
|
"priority": 2147483647,
|
||||||
|
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||||
|
"track_width": 0.2,
|
||||||
|
"via_diameter": 0.6,
|
||||||
|
"via_drill": 0.3,
|
||||||
|
"wire_width": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"version": 4
|
||||||
|
},
|
||||||
|
"net_colors": null,
|
||||||
|
"netclass_assignments": null,
|
||||||
|
"netclass_patterns": []
|
||||||
|
},
|
||||||
|
"pcbnew": {
|
||||||
|
"last_paths": {
|
||||||
|
"gencad": "",
|
||||||
|
"idf": "",
|
||||||
|
"netlist": "",
|
||||||
|
"plot": "",
|
||||||
|
"pos_files": "",
|
||||||
|
"specctra_dsn": "",
|
||||||
|
"step": "",
|
||||||
|
"svg": "",
|
||||||
|
"vrml": ""
|
||||||
|
},
|
||||||
|
"page_layout_descr_file": ""
|
||||||
|
},
|
||||||
|
"schematic": {
|
||||||
|
"legacy_lib_dir": "",
|
||||||
|
"legacy_lib_list": []
|
||||||
|
},
|
||||||
|
"sheets": [],
|
||||||
|
"text_variables": {}
|
||||||
|
}
|
||||||
@@ -48,6 +48,20 @@
|
|||||||
#include <Adafruit_TinyUSB.h>
|
#include <Adafruit_TinyUSB.h>
|
||||||
#include <bluefruit.h>
|
#include <bluefruit.h>
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// POC MODE: stream audio from internal LittleFS instead of SPI flash.
|
||||||
|
// Upload tracks via BLE (CMD 0x02 [track_num] + data packets).
|
||||||
|
// WAV files are accepted — 44-byte PCM header is stripped on receipt.
|
||||||
|
// Disable this define to revert to full SPI flash mode.
|
||||||
|
// ============================================================
|
||||||
|
#define POC_INTERNAL_FLASH
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
#include <Adafruit_LittleFS.h>
|
||||||
|
#include <InternalFileSystem.h>
|
||||||
|
using namespace Adafruit_LittleFS_Namespace;
|
||||||
|
#endif
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// PIN DEFINITIONS — adjust for your PCB
|
// PIN DEFINITIONS — adjust for your PCB
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -56,25 +70,27 @@
|
|||||||
// Map these to your actual PCB connections.
|
// Map these to your actual PCB connections.
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
#define PIN_AUDIO_PWM 13 // Any PWM-capable pin
|
#define PIN_AUDIO_PWM 8 // Any PWM-capable pin
|
||||||
#define PIN_AMP_SD 14 // PAM8302A shutdown (HIGH=enabled)
|
#define PIN_AMP_SD 14 // PAM8302A shutdown (HIGH=enabled)
|
||||||
|
|
||||||
// Motor
|
// Motor
|
||||||
#define PIN_MOTOR_PWM 15 // PWM to MOSFET gate
|
#define PIN_MOTOR_PWM 15 // PWM to MOSFET gate
|
||||||
|
|
||||||
// SPI Flash
|
// SPI Flash
|
||||||
#define PIN_FLASH_CS 5 // Flash chip select
|
#define PIN_FLASH_CS 2 // Flash chip select
|
||||||
// SPI MOSI/MISO/SCK use default SPI pins
|
// SPI MOSI/MISO/SCK use default SPI pins
|
||||||
|
|
||||||
// Buttons (directly to GPIO, active LOW with internal pull-up)
|
// Buttons (directly to GPIO, active LOW with internal pull-up)
|
||||||
#define PIN_BTN1 2
|
#define PIN_BTN1 3
|
||||||
#define PIN_BTN2 3
|
#define PIN_BTN2 4
|
||||||
#define PIN_BTN3 4
|
#define PIN_BTN3 5
|
||||||
#define PIN_BTN4 28
|
#define PIN_BTN4 3
|
||||||
#define PIN_BTN5 29
|
/*
|
||||||
#define PIN_BTN6 30
|
#define PIN_BTN5 4
|
||||||
#define PIN_BTN7 31
|
#define PIN_BTN6 5
|
||||||
#define PIN_BTN8 12
|
#define PIN_BTN7 3
|
||||||
|
#define PIN_BTN8 4
|
||||||
|
*/
|
||||||
|
|
||||||
// PIN_LED is 11 (LED_R)
|
// PIN_LED is 11 (LED_R)
|
||||||
// LED_B is 13/12
|
// LED_B is 13/12
|
||||||
@@ -82,9 +98,9 @@
|
|||||||
|
|
||||||
const uint8_t BTN_PINS[] = {
|
const uint8_t BTN_PINS[] = {
|
||||||
PIN_BTN1, PIN_BTN2, PIN_BTN3, PIN_BTN4,
|
PIN_BTN1, PIN_BTN2, PIN_BTN3, PIN_BTN4,
|
||||||
PIN_BTN5, PIN_BTN6, PIN_BTN7, PIN_BTN8
|
/*PIN_BTN5, PIN_BTN6, PIN_BTN7, PIN_BTN8*/
|
||||||
};
|
};
|
||||||
#define NUM_BUTTONS 8
|
#define NUM_BUTTONS 4
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FLASH CONSTANTS
|
// FLASH CONSTANTS
|
||||||
@@ -170,6 +186,12 @@ volatile bool g_bleUploading = false;
|
|||||||
volatile uint32_t g_bleWriteAddr = 0;
|
volatile uint32_t g_bleWriteAddr = 0;
|
||||||
volatile uint32_t g_bleWriteLen = 0;
|
volatile uint32_t g_bleWriteLen = 0;
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
File g_pocFile(InternalFS); // open file handle (read or write)
|
||||||
|
uint8_t g_pocWriteTrack = 0; // track slot being written via BLE
|
||||||
|
bool g_pocSkipHeader = false; // strip WAV header from first data packet
|
||||||
|
#endif
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// GLOBAL STATE
|
// GLOBAL STATE
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -199,6 +221,11 @@ unsigned long g_lastActivity = 0;
|
|||||||
// USB connected flag
|
// USB connected flag
|
||||||
volatile bool g_usbConnected = false;
|
volatile bool g_usbConnected = false;
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
static void pocFilename(uint8_t n, char *buf) { // buf must be >=16 bytes
|
||||||
|
snprintf(buf, 16, "/track%d.raw", n);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// SPI FLASH DRIVER
|
// SPI FLASH DRIVER
|
||||||
@@ -313,6 +340,21 @@ void flashPageProgram(uint32_t addr, const uint8_t *data, uint16_t len) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void loadTrackTable() {
|
void loadTrackTable() {
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
g_numTracks = 0;
|
||||||
|
char fname[16];
|
||||||
|
for (uint8_t i = 0; i < MAX_TRACKS; i++) {
|
||||||
|
pocFilename(i, fname);
|
||||||
|
File f(InternalFS);
|
||||||
|
if (!f.open(fname, FILE_O_READ)) break;
|
||||||
|
g_trackLen[i] = f.size();
|
||||||
|
f.close();
|
||||||
|
g_numTracks = i + 1;
|
||||||
|
}
|
||||||
|
Serial.print("Found ");
|
||||||
|
Serial.print(g_numTracks);
|
||||||
|
Serial.println(" tracks in internal flash");
|
||||||
|
#else
|
||||||
uint8_t header[4];
|
uint8_t header[4];
|
||||||
flashReadBytes(TRACK_TABLE_ADDR, header, 4);
|
flashReadBytes(TRACK_TABLE_ADDR, header, 4);
|
||||||
|
|
||||||
@@ -346,9 +388,13 @@ void loadTrackTable() {
|
|||||||
Serial.print(g_trackLen[i] / SAMPLE_RATE);
|
Serial.print(g_trackLen[i] / SAMPLE_RATE);
|
||||||
Serial.println("s");
|
Serial.println("s");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeTrackTable() {
|
void writeTrackTable() {
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
return; // no table needed; LittleFS files are the store
|
||||||
|
#else
|
||||||
// Erase sector 0
|
// Erase sector 0
|
||||||
flashEraseSector(0);
|
flashEraseSector(0);
|
||||||
|
|
||||||
@@ -382,6 +428,7 @@ void writeTrackTable() {
|
|||||||
uint16_t chunk = min((uint16_t)FLASH_PAGE, (uint16_t)(total - offset));
|
uint16_t chunk = min((uint16_t)FLASH_PAGE, (uint16_t)(total - offset));
|
||||||
flashPageProgram(offset, &table[offset], chunk);
|
flashPageProgram(offset, &table[offset], chunk);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -475,6 +522,24 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 0x02: // Start audio write
|
case 0x02: // Start audio write
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
if (len >= 2) {
|
||||||
|
g_pocWriteTrack = data[1];
|
||||||
|
if (g_pocFile) g_pocFile.close();
|
||||||
|
char fname[16];
|
||||||
|
pocFilename(g_pocWriteTrack, fname);
|
||||||
|
if (!g_pocFile.open(fname, FILE_O_WRITE)) {
|
||||||
|
Serial.print("BLE: cannot open ");
|
||||||
|
Serial.println(fname);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
g_bleUploading = true;
|
||||||
|
g_pocSkipHeader = true;
|
||||||
|
g_bleWriteLen = 0;
|
||||||
|
Serial.print("BLE: Start write track ");
|
||||||
|
Serial.println(g_pocWriteTrack);
|
||||||
|
}
|
||||||
|
#else
|
||||||
if (len >= 4) {
|
if (len >= 4) {
|
||||||
g_bleWriteAddr = ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | data[3];
|
g_bleWriteAddr = ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | data[3];
|
||||||
g_bleUploading = true;
|
g_bleUploading = true;
|
||||||
@@ -483,9 +548,14 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
Serial.print("BLE: Start write at 0x");
|
Serial.print("BLE: Start write at 0x");
|
||||||
Serial.println(g_bleWriteAddr, HEX);
|
Serial.println(g_bleWriteAddr, HEX);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 0x03: // Finish upload
|
case 0x03: // Finish upload
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
if (g_pocFile) g_pocFile.close();
|
||||||
|
g_pocSkipHeader = false;
|
||||||
|
#endif
|
||||||
g_bleUploading = false;
|
g_bleUploading = false;
|
||||||
loadTrackTable();
|
loadTrackTable();
|
||||||
Serial.println("BLE: Upload complete");
|
Serial.println("BLE: Upload complete");
|
||||||
@@ -510,6 +580,27 @@ void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
if (!g_bleUploading) return;
|
if (!g_bleUploading) return;
|
||||||
g_lastActivity = millis();
|
g_lastActivity = millis();
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
uint8_t *src = data;
|
||||||
|
uint16_t srcLen = len;
|
||||||
|
|
||||||
|
// Strip 44-byte WAV header from first packet if present
|
||||||
|
if (g_pocSkipHeader) {
|
||||||
|
g_pocSkipHeader = false;
|
||||||
|
if (srcLen >= 4 && src[0]=='R' && src[1]=='I' && src[2]=='F' && src[3]=='F') {
|
||||||
|
if (srcLen > 44) {
|
||||||
|
src += 44;
|
||||||
|
srcLen -= 44;
|
||||||
|
} else {
|
||||||
|
// Header spans packets — drop whole packet (document: send raw PCM instead)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_pocFile.write(src, srcLen);
|
||||||
|
g_bleWriteLen += srcLen;
|
||||||
|
#else
|
||||||
// Erase new sectors as we cross boundaries
|
// Erase new sectors as we cross boundaries
|
||||||
uint32_t endAddr = g_bleWriteAddr + len;
|
uint32_t endAddr = g_bleWriteAddr + len;
|
||||||
uint32_t currentSector = g_bleWriteAddr / FLASH_SECTOR;
|
uint32_t currentSector = g_bleWriteAddr / FLASH_SECTOR;
|
||||||
@@ -529,6 +620,7 @@ void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
|
|
||||||
g_bleWriteAddr += len;
|
g_bleWriteAddr += len;
|
||||||
g_bleWriteLen += len;
|
g_bleWriteLen += len;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Update status characteristic with bytes written
|
// Update status characteristic with bytes written
|
||||||
uint8_t stat[4];
|
uint8_t stat[4];
|
||||||
@@ -594,14 +686,20 @@ void setupBLE() {
|
|||||||
// For Arduino compatibility, we use analogWrite for the PWM
|
// For Arduino compatibility, we use analogWrite for the PWM
|
||||||
// and a software timer for sample feeding.
|
// and a software timer for sample feeding.
|
||||||
|
|
||||||
// Timer callback for audio sample rate
|
// micros() timestamp for next audio sample (loop-driven at 8kHz)
|
||||||
volatile bool g_timerFired = false;
|
uint32_t g_nextSampleUs = 0;
|
||||||
|
|
||||||
void timerCallback(void) {
|
// Software gain: 1=unity, 2=2x, etc. (clips at 0/255). Adjustable via 'u'/'d' serial.
|
||||||
if (!g_playing) return;
|
static uint8_t g_audioGain = 3;
|
||||||
|
|
||||||
// Output sample
|
// Called from loop() every 125µs while g_playing
|
||||||
analogWrite(PIN_AUDIO_PWM, g_audioBuf[g_activeBuf][g_bufPos]);
|
void audioTick(void) {
|
||||||
|
// Output sample with software gain (stretches away from center 128)
|
||||||
|
int16_t s = (int16_t)g_audioBuf[g_activeBuf][g_bufPos] - 128;
|
||||||
|
s *= g_audioGain;
|
||||||
|
if (s > 127) s = 127;
|
||||||
|
if (s < -128) s = -128;
|
||||||
|
analogWrite(PIN_AUDIO_PWM, (uint8_t)(s + 128));
|
||||||
g_bufPos++;
|
g_bufPos++;
|
||||||
|
|
||||||
if (g_bufPos >= AUDIO_BUF_SIZE) {
|
if (g_bufPos >= AUDIO_BUF_SIZE) {
|
||||||
@@ -610,36 +708,57 @@ void timerCallback(void) {
|
|||||||
g_bufPos = 0;
|
g_bufPos = 0;
|
||||||
|
|
||||||
if (!g_bufReady[g_activeBuf]) {
|
if (!g_bufReady[g_activeBuf]) {
|
||||||
// Buffer underrun
|
// Buffer underrun — stop cleanly
|
||||||
g_playing = false;
|
g_playing = false;
|
||||||
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use nRF52 SoftwareTimer (built into Adafruit BSP)
|
|
||||||
SoftwareTimer audioTimer;
|
|
||||||
|
|
||||||
void audioTimerHandler(TimerHandle_t xTimer) {
|
|
||||||
timerCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
void audioInit() {
|
void audioInit() {
|
||||||
pinMode(PIN_AUDIO_PWM, OUTPUT);
|
pinMode(PIN_AUDIO_PWM, OUTPUT);
|
||||||
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
||||||
analogWriteResolution(8); // 8-bit PWM
|
analogWriteResolution(8); // 8-bit PWM
|
||||||
|
|
||||||
// Create a FreeRTOS software timer at 8kHz
|
|
||||||
// Note: For better timing, use a hardware TIMER peripheral
|
|
||||||
// This works well enough for 8kHz audio
|
|
||||||
audioTimer.begin(1000 / 8, audioTimerHandler, NULL, true); // ~8kHz
|
|
||||||
// Better approach: use nrf_drv_timer for precise 125µs intervals
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void audioStart(uint8_t trackNum) {
|
void audioStart(uint8_t trackNum) {
|
||||||
|
Serial.print("Playing track");
|
||||||
|
Serial.print(trackNum);
|
||||||
|
Serial.print(" of ");
|
||||||
|
Serial.println(g_numTracks);
|
||||||
if (trackNum >= g_numTracks) return;
|
if (trackNum >= g_numTracks) return;
|
||||||
|
|
||||||
g_currentTrack = trackNum;
|
g_currentTrack = trackNum;
|
||||||
|
g_playEnd = g_trackLen[trackNum];
|
||||||
|
g_nextReadAddr = 0;
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
if (g_pocFile) g_pocFile.close();
|
||||||
|
char fname[16];
|
||||||
|
pocFilename(trackNum, fname);
|
||||||
|
if (!g_pocFile.open(fname, FILE_O_READ)) {
|
||||||
|
Serial.print("audioStart: cannot open ");
|
||||||
|
Serial.println(fname);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-fill buf 0
|
||||||
|
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
|
||||||
|
g_pocFile.read(g_audioBuf[0], toRead);
|
||||||
|
g_nextReadAddr += toRead;
|
||||||
|
g_bufReady[0] = true;
|
||||||
|
|
||||||
|
// Pre-fill buf 1
|
||||||
|
if (g_nextReadAddr < g_playEnd) {
|
||||||
|
toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
|
||||||
|
g_pocFile.read(g_audioBuf[1], toRead);
|
||||||
|
g_nextReadAddr += toRead;
|
||||||
|
g_bufReady[1] = true;
|
||||||
|
} else {
|
||||||
|
memset(g_audioBuf[1], SILENCE, AUDIO_BUF_SIZE);
|
||||||
|
g_bufReady[1] = true;
|
||||||
|
}
|
||||||
|
#else
|
||||||
uint32_t start = g_trackStart[trackNum];
|
uint32_t start = g_trackStart[trackNum];
|
||||||
g_playEnd = start + g_trackLen[trackNum];
|
g_playEnd = start + g_trackLen[trackNum];
|
||||||
g_nextReadAddr = start;
|
g_nextReadAddr = start;
|
||||||
@@ -659,6 +778,7 @@ void audioStart(uint8_t trackNum) {
|
|||||||
memset(g_audioBuf[1], SILENCE, AUDIO_BUF_SIZE);
|
memset(g_audioBuf[1], SILENCE, AUDIO_BUF_SIZE);
|
||||||
g_bufReady[1] = true;
|
g_bufReady[1] = true;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
g_activeBuf = 0;
|
g_activeBuf = 0;
|
||||||
g_bufPos = 0;
|
g_bufPos = 0;
|
||||||
@@ -668,17 +788,20 @@ void audioStart(uint8_t trackNum) {
|
|||||||
delay(10);
|
delay(10);
|
||||||
|
|
||||||
g_playing = true;
|
g_playing = true;
|
||||||
audioTimer.start();
|
g_nextSampleUs = micros();
|
||||||
|
|
||||||
Serial.print("Playing track ");
|
Serial.print("Playing track ");
|
||||||
Serial.println(trackNum + 1);
|
Serial.println(trackNum + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void audioStop() {
|
void audioStop() {
|
||||||
audioTimer.stop();
|
|
||||||
g_playing = false;
|
g_playing = false;
|
||||||
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
if (g_pocFile) g_pocFile.close();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Disable amp
|
// Disable amp
|
||||||
digitalWrite(PIN_AMP_SD, LOW);
|
digitalWrite(PIN_AMP_SD, LOW);
|
||||||
|
|
||||||
@@ -720,9 +843,10 @@ void buttonsInit() {
|
|||||||
// Returns 1-8, or 0 if none pressed
|
// Returns 1-8, or 0 if none pressed
|
||||||
uint8_t buttonRead() {
|
uint8_t buttonRead() {
|
||||||
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
|
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
|
||||||
if (digitalRead(BTN_PINS[i]) == LOW) {
|
// Buttons pull to ground
|
||||||
|
if (analogRead(BTN_PINS[i]) < 128) {
|
||||||
delay(20); // debounce
|
delay(20); // debounce
|
||||||
if (digitalRead(BTN_PINS[i]) == LOW) {
|
if (analogRead(BTN_PINS[i]) < 128) {
|
||||||
return i + 1;
|
return i + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -731,8 +855,16 @@ uint8_t buttonRead() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void waitButtonRelease(uint8_t btn) {
|
void waitButtonRelease(uint8_t btn) {
|
||||||
while (buttonRead() == btn) {
|
unsigned long deadline = millis() + 300;
|
||||||
delay(10);
|
while (buttonRead() == btn && millis() < deadline) {
|
||||||
|
// Keep audio running while waiting for button release
|
||||||
|
if (g_playing) {
|
||||||
|
uint32_t now = micros();
|
||||||
|
if ((int32_t)(now - g_nextSampleUs) >= 0) {
|
||||||
|
g_nextSampleUs += 125;
|
||||||
|
audioTick();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,7 +877,9 @@ void enterDeepSleep() {
|
|||||||
Serial.println("Entering deep sleep...");
|
Serial.println("Entering deep sleep...");
|
||||||
audioStop();
|
audioStop();
|
||||||
motorStop();
|
motorStop();
|
||||||
|
#ifndef POC_INTERNAL_FLASH
|
||||||
flashSleep();
|
flashSleep();
|
||||||
|
#endif
|
||||||
digitalWrite(PIN_LED, LOW);
|
digitalWrite(PIN_LED, LOW);
|
||||||
|
|
||||||
// Stop BLE advertising to save power
|
// Stop BLE advertising to save power
|
||||||
@@ -798,11 +932,16 @@ void setup() {
|
|||||||
Serial.println("nRF52840 + IS25LP128F + PAM8302A");
|
Serial.println("nRF52840 + IS25LP128F + PAM8302A");
|
||||||
|
|
||||||
// Pin setup
|
// Pin setup
|
||||||
pinMode(PIN_FLASH_CS, OUTPUT);
|
|
||||||
pinMode(PIN_AMP_SD, OUTPUT);
|
pinMode(PIN_AMP_SD, OUTPUT);
|
||||||
digitalWrite(PIN_FLASH_CS, HIGH);
|
|
||||||
digitalWrite(PIN_AMP_SD, LOW);
|
digitalWrite(PIN_AMP_SD, LOW);
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
InternalFS.begin();
|
||||||
|
Serial.println("InternalFS mounted");
|
||||||
|
#else
|
||||||
|
pinMode(PIN_FLASH_CS, OUTPUT);
|
||||||
|
digitalWrite(PIN_FLASH_CS, HIGH);
|
||||||
|
|
||||||
// SPI
|
// SPI
|
||||||
SPI.begin();
|
SPI.begin();
|
||||||
|
|
||||||
@@ -819,6 +958,7 @@ void setup() {
|
|||||||
} else if (jedec == 0x000000 || jedec == 0xFFFFFF) {
|
} else if (jedec == 0x000000 || jedec == 0xFFFFFF) {
|
||||||
Serial.println("WARNING: No flash detected! Check SPI wiring.");
|
Serial.println("WARNING: No flash detected! Check SPI wiring.");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Load tracks
|
// Load tracks
|
||||||
loadTrackTable();
|
loadTrackTable();
|
||||||
@@ -832,7 +972,70 @@ void setup() {
|
|||||||
// Audio
|
// Audio
|
||||||
audioInit();
|
audioInit();
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
// Boot test tone: 440Hz square wave for 1 second
|
||||||
|
// If you hear a beep, the amp/PWM chain is working.
|
||||||
|
Serial.println("Boot tone...");
|
||||||
|
digitalWrite(PIN_AMP_SD, HIGH);
|
||||||
|
delay(20);
|
||||||
|
for (int i = 0; i < 8000; i++) {
|
||||||
|
// 440Hz at 8kHz sample rate: 8000/440 ≈ 18 samples/cycle
|
||||||
|
analogWrite(PIN_AUDIO_PWM, (i % 18) < 9 ? 255 : 0);
|
||||||
|
delayMicroseconds(125);
|
||||||
|
}
|
||||||
|
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
||||||
|
delay(20);
|
||||||
|
digitalWrite(PIN_AMP_SD, LOW);
|
||||||
|
Serial.println("Boot tone done");
|
||||||
|
|
||||||
|
// Hex dump track 0 + blocking playback diagnostic
|
||||||
|
// (after audioInit so amp/PWM are ready)
|
||||||
|
if (g_numTracks > 0) {
|
||||||
|
char diagName[16]; pocFilename(0, diagName);
|
||||||
|
File diagF(InternalFS);
|
||||||
|
if (diagF.open(diagName, FILE_O_READ)) {
|
||||||
|
uint32_t fsz = diagF.size();
|
||||||
|
Serial.print("Track0 size: "); Serial.println(fsz);
|
||||||
|
uint8_t hbuf[64];
|
||||||
|
int nr = diagF.read(hbuf, sizeof(hbuf));
|
||||||
|
diagF.close();
|
||||||
|
Serial.print("First "); Serial.print(nr); Serial.println(" bytes (hex):");
|
||||||
|
for (int i = 0; i < nr; i++) {
|
||||||
|
if (hbuf[i] < 0x10) Serial.print("0");
|
||||||
|
Serial.print(hbuf[i], HEX);
|
||||||
|
Serial.print(i % 16 == 15 ? "\n" : " ");
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
} else {
|
||||||
|
Serial.println("Cannot open track0 for read!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Diag: blocking play track0 for 3s...");
|
||||||
|
audioStart(0);
|
||||||
|
uint32_t diagEnd = millis() + 3000;
|
||||||
|
while (millis() < diagEnd && g_playing) {
|
||||||
|
uint32_t now = micros();
|
||||||
|
if ((int32_t)(now - g_nextSampleUs) >= 0) {
|
||||||
|
g_nextSampleUs += 125;
|
||||||
|
audioTick();
|
||||||
|
}
|
||||||
|
for (uint8_t b = 0; b < 2; b++) {
|
||||||
|
if (!g_bufReady[b] && g_nextReadAddr < g_playEnd) {
|
||||||
|
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
|
||||||
|
g_pocFile.read(g_audioBuf[b], toRead);
|
||||||
|
for (uint32_t i = toRead; i < AUDIO_BUF_SIZE; i++) g_audioBuf[b][i] = SILENCE;
|
||||||
|
g_nextReadAddr += toRead;
|
||||||
|
g_bufReady[b] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audioStop();
|
||||||
|
Serial.println("Diag done");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// USB Mass Storage
|
// USB Mass Storage
|
||||||
|
#ifndef POC_INTERNAL_FLASH
|
||||||
usb_msc.setID("BabyMobile", "Audio Drive", "2.0");
|
usb_msc.setID("BabyMobile", "Audio Drive", "2.0");
|
||||||
usb_msc.setReadWriteCallback(msc_read_cb, msc_write_cb, msc_flush_cb);
|
usb_msc.setReadWriteCallback(msc_read_cb, msc_write_cb, msc_flush_cb);
|
||||||
//usb_msc.setStartStopCallback(msc_start_stop_cb); // unused in nrf?
|
//usb_msc.setStartStopCallback(msc_start_stop_cb); // unused in nrf?
|
||||||
@@ -840,6 +1043,7 @@ void setup() {
|
|||||||
//usb_msc.setReadOnly(false);
|
//usb_msc.setReadOnly(false);
|
||||||
usb_msc.setUnitReady(true);
|
usb_msc.setUnitReady(true);
|
||||||
usb_msc.begin();
|
usb_msc.begin();
|
||||||
|
#endif
|
||||||
|
|
||||||
// BLE
|
// BLE
|
||||||
setupBLE();
|
setupBLE();
|
||||||
@@ -855,7 +1059,164 @@ void setup() {
|
|||||||
// MAIN LOOP
|
// MAIN LOOP
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SERIAL UPLOAD STATE MACHINE (POC_INTERNAL_FLASH only)
|
||||||
|
// ============================================================
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
enum SerUploadState { SER_IDLE, SER_RECEIVING };
|
||||||
|
static SerUploadState g_serState = SER_IDLE;
|
||||||
|
static char g_serLineBuf[64] = {0};
|
||||||
|
static uint8_t g_serLineLen = 0;
|
||||||
|
static uint8_t g_serTrack = 0;
|
||||||
|
static uint32_t g_serBytesExpected = 0;
|
||||||
|
static uint32_t g_serBytesReceived = 0;
|
||||||
|
static bool g_serSkipHeader = false;
|
||||||
|
static File g_serFile(InternalFS);
|
||||||
|
|
||||||
|
static void serUploadTick() {
|
||||||
|
if (g_serState == SER_IDLE) {
|
||||||
|
// Accumulate characters until newline
|
||||||
|
while (Serial.available()) {
|
||||||
|
// Serial.print("r");
|
||||||
|
char c = (char)Serial.read();
|
||||||
|
if (c == '\n' || c == '\r') {
|
||||||
|
g_serLineBuf[g_serLineLen] = '\0';
|
||||||
|
if (g_serLineLen == 0) { g_serLineLen = 0; break; }
|
||||||
|
// Parse: UPLOAD <track> <len>
|
||||||
|
unsigned int utrk = 0, ulen = 0;
|
||||||
|
if (sscanf(g_serLineBuf, "UPLOAD %u %u", &utrk, &ulen) == 2) {
|
||||||
|
g_serTrack = (uint8_t)utrk;
|
||||||
|
g_serBytesExpected = (uint32_t)ulen;
|
||||||
|
g_serBytesReceived = 0;
|
||||||
|
g_serSkipHeader = true;
|
||||||
|
|
||||||
|
char fname[16];
|
||||||
|
pocFilename(g_serTrack, fname);
|
||||||
|
if (g_serFile) g_serFile.close();
|
||||||
|
// Remove first — FILE_O_WRITE has no truncate flag
|
||||||
|
InternalFS.remove(fname);
|
||||||
|
if (!g_serFile.open(fname, FILE_O_WRITE)) {
|
||||||
|
Serial.print("ERR cannot open ");
|
||||||
|
Serial.println(fname);
|
||||||
|
} else {
|
||||||
|
g_serState = SER_RECEIVING;
|
||||||
|
g_usbConnected = true;
|
||||||
|
Serial.println("READY");
|
||||||
|
}
|
||||||
|
} else if (sscanf(g_serLineBuf, "DUMP %u", &utrk) == 1) {
|
||||||
|
// Hex-dump first 256 bytes of a track file
|
||||||
|
char fname[16];
|
||||||
|
pocFilename((uint8_t)utrk, fname);
|
||||||
|
File df(InternalFS);
|
||||||
|
if (df.open(fname, FILE_O_READ)) {
|
||||||
|
uint32_t fsz = df.size();
|
||||||
|
Serial.print("SIZE "); Serial.println(fsz);
|
||||||
|
uint8_t dbuf[16];
|
||||||
|
uint32_t limit = min(fsz, (uint32_t)256);
|
||||||
|
uint32_t off = 0;
|
||||||
|
while (off < limit) {
|
||||||
|
int n = df.read(dbuf, min((uint32_t)sizeof(dbuf), limit - off));
|
||||||
|
if (n <= 0) break;
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
if (dbuf[j] < 0x10) Serial.print("0");
|
||||||
|
Serial.print(dbuf[j], HEX);
|
||||||
|
Serial.print(j % 16 == 15 || (off + j + 1) == limit ? "\n" : " ");
|
||||||
|
}
|
||||||
|
off += n;
|
||||||
|
}
|
||||||
|
df.close();
|
||||||
|
Serial.println("END");
|
||||||
|
} else {
|
||||||
|
Serial.println("NO FILE");
|
||||||
|
}
|
||||||
|
} else if (g_serLineLen == 1 && (g_serLineBuf[0] == 'u' || g_serLineBuf[0] == 'd')) {
|
||||||
|
if (g_serLineBuf[0] == 'u') g_audioGain++;
|
||||||
|
else if (g_audioGain > 1) g_audioGain--;
|
||||||
|
Serial.print("GAIN "); Serial.println(g_audioGain);
|
||||||
|
} else {
|
||||||
|
Serial.print("ERR bad cmd: ");
|
||||||
|
Serial.println(g_serLineBuf);
|
||||||
|
}
|
||||||
|
g_serLineLen = 0;
|
||||||
|
// Serial.print("0");
|
||||||
|
} else {
|
||||||
|
if (g_serLineLen < (sizeof(g_serLineBuf) - 1)) {
|
||||||
|
g_serLineBuf[g_serLineLen++] = c;
|
||||||
|
}
|
||||||
|
// Serial.print(",");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // SER_RECEIVING
|
||||||
|
// Serial.print("e");
|
||||||
|
uint8_t chunk[64];
|
||||||
|
while (Serial.available() && g_serBytesReceived < g_serBytesExpected) {
|
||||||
|
// Serial.print(";");
|
||||||
|
int n = Serial.readBytes(chunk, min((int)sizeof(chunk),
|
||||||
|
(int)(g_serBytesExpected - g_serBytesReceived)));
|
||||||
|
if (n <= 0) break;
|
||||||
|
|
||||||
|
uint8_t *src = chunk;
|
||||||
|
uint16_t srcLen = (uint16_t)n;
|
||||||
|
|
||||||
|
// Strip 44-byte WAV header from first chunk if present
|
||||||
|
if (g_serSkipHeader) {
|
||||||
|
g_serSkipHeader = false;
|
||||||
|
if (srcLen >= 4 && src[0]=='R' && src[1]=='I' && src[2]=='F' && src[3]=='F') {
|
||||||
|
if (srcLen > 44) { src += 44; srcLen -= 44; }
|
||||||
|
else { g_serBytesReceived += n; continue; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t wr = g_serFile.write(src, srcLen);
|
||||||
|
if (wr != (int32_t)srcLen) {
|
||||||
|
// Write failed (LittleFS full or error) — abort immediately.
|
||||||
|
// Do NOT loop printing errors; that fills USB CDC TX and hangs.
|
||||||
|
g_serFile.close();
|
||||||
|
g_usbConnected = false;
|
||||||
|
g_serState = SER_IDLE;
|
||||||
|
Serial.print("ERR WRITE_FAIL at=");
|
||||||
|
Serial.println(g_serBytesReceived);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
g_serBytesReceived += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_serState != SER_RECEIVING) return; // aborted in write-fail handler above
|
||||||
|
|
||||||
|
if (g_serBytesReceived >= g_serBytesExpected) {
|
||||||
|
g_serFile.close();
|
||||||
|
// Reopen to read the actual flushed size from LittleFS
|
||||||
|
char fname2[16]; pocFilename(g_serTrack, fname2);
|
||||||
|
File tmp(InternalFS);
|
||||||
|
uint32_t fsz = 0;
|
||||||
|
if (tmp.open(fname2, FILE_O_READ)) { fsz = tmp.size(); tmp.close(); }
|
||||||
|
g_usbConnected = false;
|
||||||
|
g_serState = SER_IDLE;
|
||||||
|
loadTrackTable();
|
||||||
|
Serial.print("OK ");
|
||||||
|
Serial.print(fsz);
|
||||||
|
Serial.print("/");
|
||||||
|
Serial.println(g_serBytesReceived);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
// ---- Audio sample output (8kHz, loop-driven) ----
|
||||||
|
if (g_playing) {
|
||||||
|
uint32_t now = micros();
|
||||||
|
if ((int32_t)(now - g_nextSampleUs) >= 0) {
|
||||||
|
g_nextSampleUs += 125; // 1/8000s = 125µs
|
||||||
|
audioTick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Serial upload (POC mode) ----
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
serUploadTick();
|
||||||
|
#endif
|
||||||
|
|
||||||
// ---- Refill audio buffers ----
|
// ---- Refill audio buffers ----
|
||||||
if (g_playing) {
|
if (g_playing) {
|
||||||
for (uint8_t b = 0; b < 2; b++) {
|
for (uint8_t b = 0; b < 2; b++) {
|
||||||
@@ -863,7 +1224,11 @@ void loop() {
|
|||||||
uint32_t remaining = g_playEnd - g_nextReadAddr;
|
uint32_t remaining = g_playEnd - g_nextReadAddr;
|
||||||
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, remaining);
|
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, remaining);
|
||||||
|
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
g_pocFile.read(g_audioBuf[b], toRead);
|
||||||
|
#else
|
||||||
flashReadBytes(g_nextReadAddr, g_audioBuf[b], toRead);
|
flashReadBytes(g_nextReadAddr, g_audioBuf[b], toRead);
|
||||||
|
#endif
|
||||||
// Pad with silence
|
// Pad with silence
|
||||||
for (uint32_t i = toRead; i < AUDIO_BUF_SIZE; i++) {
|
for (uint32_t i = toRead; i < AUDIO_BUF_SIZE; i++) {
|
||||||
g_audioBuf[b][i] = SILENCE;
|
g_audioBuf[b][i] = SILENCE;
|
||||||
@@ -884,8 +1249,12 @@ void loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Handle buttons ----
|
// ---- Handle buttons ----
|
||||||
|
// Per-button cooldown prevents phantom/stuck pins from re-firing
|
||||||
|
static unsigned long btnLastMs[NUM_BUTTONS + 1] = {0};
|
||||||
uint8_t btn = buttonRead();
|
uint8_t btn = buttonRead();
|
||||||
if (btn > 0) {
|
if (btn > 0 && millis() - btnLastMs[btn] > 800) {
|
||||||
|
btnLastMs[btn] = millis();
|
||||||
|
Serial.print("BTN"); Serial.println(btn);
|
||||||
g_lastActivity = millis();
|
g_lastActivity = millis();
|
||||||
|
|
||||||
switch (btn) {
|
switch (btn) {
|
||||||
@@ -951,6 +1320,4 @@ void loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay to prevent tight-looping
|
|
||||||
delay(1);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
upload_track.py — serial audio uploader for baby_mobile_v2 POC_INTERNAL_FLASH mode
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python upload_track.py <port> <track_num> <file>
|
||||||
|
|
||||||
|
Supported formats: .raw, .wav, .mp3, .ogg, .flac, .aac, .m4a (anything ffmpeg handles)
|
||||||
|
|
||||||
|
Requires: pyserial (pip install pyserial)
|
||||||
|
ffmpeg in PATH for non-WAV/RAW files
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
|
||||||
|
CHUNK_SIZE = 512
|
||||||
|
BAUD_RATE = 115200
|
||||||
|
TIMEOUT_S = 10.0
|
||||||
|
|
||||||
|
RAW_EXTS = {'.raw'}
|
||||||
|
WAV_EXTS = {'.wav'}
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_pcm(path: str) -> bytes:
|
||||||
|
ext = os.path.splitext(path)[1].lower()
|
||||||
|
|
||||||
|
if ext in RAW_EXTS:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
if ext in WAV_EXTS:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
if data[:4] == b"RIFF":
|
||||||
|
try:
|
||||||
|
audio_format = struct.unpack_from("<H", data, 20)[0]
|
||||||
|
num_channels = struct.unpack_from("<H", data, 22)[0]
|
||||||
|
sample_rate = struct.unpack_from("<I", data, 24)[0]
|
||||||
|
bits_per_samp = struct.unpack_from("<H", data, 34)[0]
|
||||||
|
needs_convert = (audio_format != 1 or num_channels != 1
|
||||||
|
or sample_rate != 8000 or bits_per_samp != 8)
|
||||||
|
except Exception:
|
||||||
|
needs_convert = True
|
||||||
|
|
||||||
|
if not needs_convert:
|
||||||
|
print("WAV is already 8-bit mono 8kHz — stripping header")
|
||||||
|
return data[44:]
|
||||||
|
print("WAV needs resampling — converting via ffmpeg")
|
||||||
|
# fall through to ffmpeg conversion
|
||||||
|
|
||||||
|
# Use ffmpeg for everything else (mp3, ogg, flac, aac, non-conformant wav...)
|
||||||
|
if not shutil.which('ffmpeg'):
|
||||||
|
sys.exit(
|
||||||
|
"ERROR: ffmpeg not found. Install it or convert manually:\n"
|
||||||
|
f" ffmpeg -i \"{path}\" -ar 8000 -ac 1 -f u8 -acodec pcm_u8 out.raw"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Converting {os.path.basename(path)} via ffmpeg...")
|
||||||
|
result = subprocess.run(
|
||||||
|
['ffmpeg', '-y', '-i', path,
|
||||||
|
'-ar', '8000', '-ac', '1', '-f', 'u8', '-acodec', 'pcm_u8', 'pipe:1'],
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
sys.exit(f"ERROR: ffmpeg failed:\n{result.stderr.decode(errors='replace')[-400:]}")
|
||||||
|
|
||||||
|
pcm = result.stdout
|
||||||
|
duration = len(pcm) / 8000
|
||||||
|
print(f" Converted: {len(pcm):,} bytes ({duration:.1f}s)")
|
||||||
|
return pcm
|
||||||
|
|
||||||
|
|
||||||
|
def upload(port: str, track_num: int, pcm: bytes) -> None:
|
||||||
|
total = len(pcm)
|
||||||
|
print(f"Uploading {total} bytes as track {track_num} via {port} ...")
|
||||||
|
|
||||||
|
# dsrdtr=False / rtscts=False prevents Windows from toggling DTR/RTS on
|
||||||
|
# port open, which would reset the nRF52840 and stall the 5-second boot loop.
|
||||||
|
with serial.Serial(port, BAUD_RATE, timeout=2.0,
|
||||||
|
dsrdtr=False, rtscts=False) as ser:
|
||||||
|
# Drain any boot output; send a bare newline first to flush any
|
||||||
|
# partial line sitting in the firmware's line buffer.
|
||||||
|
time.sleep(0.5)
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
ser.write(b"\n")
|
||||||
|
ser.flush()
|
||||||
|
time.sleep(0.1)
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
|
||||||
|
# Send UPLOAD command
|
||||||
|
cmd = f"UPLOAD {track_num} {total}\n"
|
||||||
|
ser.write(cmd.encode())
|
||||||
|
ser.flush()
|
||||||
|
|
||||||
|
# Wait for READY — print everything received so failures are diagnosable
|
||||||
|
deadline = time.time() + TIMEOUT_S
|
||||||
|
while True:
|
||||||
|
if time.time() > deadline:
|
||||||
|
sys.exit("Timed out waiting for READY")
|
||||||
|
line = ser.readline().decode(errors="replace").strip()
|
||||||
|
if line == "READY":
|
||||||
|
break
|
||||||
|
if line:
|
||||||
|
print(f" firmware: {line}")
|
||||||
|
|
||||||
|
# Stream PCM in chunks; stop early if firmware sends ERR
|
||||||
|
sent = 0
|
||||||
|
err_line = None
|
||||||
|
ser.timeout = 0.05 # short timeout so we can poll for ERR while sending
|
||||||
|
while sent < total:
|
||||||
|
chunk = pcm[sent:sent + CHUNK_SIZE]
|
||||||
|
ser.write(chunk)
|
||||||
|
sent += len(chunk)
|
||||||
|
pct = sent * 100 // total
|
||||||
|
print(f"\r {sent}/{total} bytes ({pct}%) ", end="", flush=True)
|
||||||
|
# Check for early ERR from firmware (e.g. LittleFS full)
|
||||||
|
line = ser.readline().decode(errors="replace").strip()
|
||||||
|
if line.startswith("ERR"):
|
||||||
|
err_line = line
|
||||||
|
break
|
||||||
|
print()
|
||||||
|
ser.timeout = 2.0
|
||||||
|
|
||||||
|
if err_line:
|
||||||
|
print(f"ERROR from firmware: {err_line}")
|
||||||
|
# at= tells us how many bytes were accepted before the failure
|
||||||
|
if "at=" in err_line:
|
||||||
|
accepted = int(err_line.split("at=")[1].split()[0])
|
||||||
|
safe = int(accepted * 0.9) # 10% headroom
|
||||||
|
max_s = safe / 8000
|
||||||
|
print(f" LittleFS accepted {accepted} bytes before full.")
|
||||||
|
print(f" Safe clip length: ~{max_s:.1f}s")
|
||||||
|
print(f" Trim with: ffmpeg -i input.mp3 -t {max_s:.0f} -ar 8000 -ac 1 -f u8 out.raw")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Wait for OK
|
||||||
|
deadline = time.time() + TIMEOUT_S
|
||||||
|
while True:
|
||||||
|
if time.time() > deadline:
|
||||||
|
sys.exit("Timed out waiting for OK")
|
||||||
|
line = ser.readline().decode(errors="replace").strip()
|
||||||
|
if line.startswith("OK"):
|
||||||
|
print(f"Upload done: {line}")
|
||||||
|
break
|
||||||
|
if line.startswith("ERR"):
|
||||||
|
print(f"ERROR from firmware: {line}")
|
||||||
|
sys.exit(1)
|
||||||
|
if line:
|
||||||
|
print(f" firmware: {line}")
|
||||||
|
|
||||||
|
# Verify: request hex dump of first 256 bytes and compare
|
||||||
|
print("Verifying...")
|
||||||
|
ser.reset_input_buffer()
|
||||||
|
ser.write(f"DUMP {track_num}\n".encode())
|
||||||
|
ser.flush()
|
||||||
|
|
||||||
|
deadline = time.time() + TIMEOUT_S
|
||||||
|
fw_size = None
|
||||||
|
fw_bytes = bytearray()
|
||||||
|
while True:
|
||||||
|
if time.time() > deadline:
|
||||||
|
print("WARNING: timed out waiting for DUMP response")
|
||||||
|
break
|
||||||
|
line = ser.readline().decode(errors="replace").strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line.startswith("SIZE"):
|
||||||
|
fw_size = int(line.split()[1])
|
||||||
|
elif line == "END":
|
||||||
|
break
|
||||||
|
elif line == "NO FILE":
|
||||||
|
print("ERROR: firmware says file does not exist!")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Parse hex bytes
|
||||||
|
for tok in line.split():
|
||||||
|
try:
|
||||||
|
fw_bytes.append(int(tok, 16))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if fw_size is not None:
|
||||||
|
print(f" Firmware file size: {fw_size} bytes")
|
||||||
|
if fw_size == 0:
|
||||||
|
print(" ERROR: file is empty — write failed (LittleFS full?)")
|
||||||
|
elif fw_size < total:
|
||||||
|
print(f" WARNING: only {fw_size}/{total} bytes written (LittleFS full?)")
|
||||||
|
else:
|
||||||
|
print(f" Size OK: {fw_size}/{total}")
|
||||||
|
|
||||||
|
compare_len = min(len(fw_bytes), len(pcm), 256)
|
||||||
|
if compare_len > 0:
|
||||||
|
mismatches = sum(1 for i in range(compare_len) if fw_bytes[i] != pcm[i])
|
||||||
|
if mismatches == 0:
|
||||||
|
print(f" Data OK: first {compare_len} bytes match")
|
||||||
|
else:
|
||||||
|
print(f" ERROR: {mismatches}/{compare_len} byte mismatches in first {compare_len} bytes")
|
||||||
|
print(f" Expected: {' '.join(f'{b:02X}' for b in pcm[:16])}")
|
||||||
|
print(f" Got: {' '.join(f'{b:02X}' for b in fw_bytes[:16])}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
port = sys.argv[1]
|
||||||
|
track_num = int(sys.argv[2])
|
||||||
|
path = sys.argv[3]
|
||||||
|
|
||||||
|
if not 0 <= track_num < 32:
|
||||||
|
sys.exit("track_num must be 0–31")
|
||||||
|
|
||||||
|
pcm = convert_to_pcm(path)
|
||||||
|
if not pcm:
|
||||||
|
sys.exit("File is empty after conversion")
|
||||||
|
|
||||||
|
upload(port, track_num, pcm)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user