diff --git a/firmware/include/firmware_utils.h b/firmware/include/firmware_utils.h index cc7c05e..0b13822 100644 --- a/firmware/include/firmware_utils.h +++ b/firmware/include/firmware_utils.h @@ -1,22 +1,25 @@ #pragma once -#include "voice_controller.h" - #include #include +#include "voice_controller.h" + // --------------------------------------------------------------------------- // Pure C++ utility functions — no Arduino dependencies. // Extracted here so they can be compiled and tested on native (Unity). // --------------------------------------------------------------------------- /// Human-readable idle summary for the LCD / UI. -inline std::string FwIdleSummary(const MediaSnapshot& media) { - auto mode_label = [](MediaMode m) -> const char* { +inline std::string FwIdleSummary(const MediaSnapshot &media) { + auto mode_label = [](MediaMode m) -> const char * { switch (m) { - case MediaMode::kMp3: return "MP3"; - case MediaMode::kRadio: return "Radio"; - default: return "Idle"; + case MediaMode::kMp3: + return "MP3"; + case MediaMode::kRadio: + return "Radio"; + default: + return "Idle"; } }; @@ -33,16 +36,15 @@ inline std::string FwIdleSummary(const MediaSnapshot& media) { } /// True if a playback_started event should be emitted after applying an intent. -inline bool FwShouldPublishPlaybackStarted(const MediaSnapshot& before, - const MediaSnapshot& after, - const VoiceIntent& intent) { - if (!after.playing) return false; +inline bool FwShouldPublishPlaybackStarted(const MediaSnapshot &before, const MediaSnapshot &after, + const VoiceIntent &intent) { + if (!after.playing) + return false; - if (intent.type == "play" || intent.type == "switch_mode" || - intent.type == "select_station" || intent.type == "next" || - intent.type == "previous") { - return !before.playing || before.station != after.station || - before.track != after.track || before.mode != after.mode; + if (intent.type == "play" || intent.type == "switch_mode" || intent.type == "select_station" || + intent.type == "next" || intent.type == "previous") { + return !before.playing || before.station != after.station || before.track != after.track || + before.mode != after.mode; } return false; @@ -50,37 +52,44 @@ inline bool FwShouldPublishPlaybackStarted(const MediaSnapshot& before, /// Compare semver strings "MAJOR.MINOR.PATCH". /// Returns -1 / 0 / +1 (like strcmp). -inline int FwCompareVersions(const std::string& a, const std::string& b) { - auto parse = [](const std::string& v, int out[3]) { +inline int FwCompareVersions(const std::string &a, const std::string &b) { + auto parse = [](const std::string &v, int out[3]) { int i = 0; std::istringstream ss(v); std::string tok; while (std::getline(ss, tok, '.') && i < 3) { - try { out[i++] = std::stoi(tok); } catch (...) { out[i++] = 0; } + try { + out[i++] = std::stoi(tok); + } catch (...) { + out[i++] = 0; + } } - while (i < 3) out[i++] = 0; + while (i < 3) + out[i++] = 0; }; int va[3] = {}, vb[3] = {}; parse(a, va); parse(b, vb); for (int i = 0; i < 3; ++i) { - if (va[i] < vb[i]) return -1; - if (va[i] > vb[i]) return +1; + if (va[i] < vb[i]) + return -1; + if (va[i] > vb[i]) + return +1; } return 0; } /// Minimal WAV header validation — checks RIFF/WAVE magic. -inline bool FwIsValidWavHeader(const uint8_t* data, size_t len) { - if (!data || len < 12) return false; +inline bool FwIsValidWavHeader(const uint8_t *data, size_t len) { + if (!data || len < 12) + return false; // Bytes 0-3: "RIFF", bytes 8-11: "WAVE" - return data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && - data[3] == 'F' && data[8] == 'W' && data[9] == 'A' && - data[10] == 'V' && data[11] == 'E'; + return data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' && data[8] == 'W' && + data[9] == 'A' && data[10] == 'V' && data[11] == 'E'; } /// Validates that a backend URL starts with http:// or https://. -inline bool FwIsValidBackendUrl(const std::string& url) { +inline bool FwIsValidBackendUrl(const std::string &url) { return url.substr(0, 7) == "http://" || url.substr(0, 8) == "https://"; } @@ -91,50 +100,52 @@ inline bool FwIsValidBackendUrl(const std::string& url) { /// Network entry produced by WifiScanner::Scan(). struct WifiNetwork { std::string ssid; - int rssi = -100; - bool open = false; + int rssi = -100; + bool open = false; int channel = 0; }; /// Map RSSI (dBm) to a 0–100 quality score. /// -50 dBm → 100, -100 dBm → 0, clamped. inline int FwRssiQuality(int rssi) { - if (rssi >= -50) return 100; - if (rssi <= -100) return 0; - return (rssi + 100) * 2; // linear in [-100, -50] + if (rssi >= -50) + return 100; + if (rssi <= -100) + return 0; + return (rssi + 100) * 2; // linear in [-100, -50] } /// Serialize a list of networks to JSON. -/// Output: {"networks":[{"ssid":"...","rssi":-62,"open":false,"channel":6,"quality":76}],"count":N,"duration_ms":T} -inline std::string FwWifiToJson(const std::vector& nets, - uint32_t duration_ms) { +/// Output: +/// {"networks":[{"ssid":"...","rssi":-62,"open":false,"channel":6,"quality":76}],"count":N,"duration_ms":T} +inline std::string FwWifiToJson(const std::vector &nets, uint32_t duration_ms) { std::ostringstream j; j << "{\"networks\":["; for (size_t i = 0; i < nets.size(); ++i) { - const auto& n = nets[i]; - if (i > 0) j << ","; + const auto &n = nets[i]; + if (i > 0) + j << ","; // Escape backslash and double-quote in SSID (minimal safe escaping). std::string ssid_esc; ssid_esc.reserve(n.ssid.size()); for (char c : n.ssid) { - if (c == '"') { ssid_esc += "\\\""; } - else if (c == '\\') { ssid_esc += "\\\\"; } - else { ssid_esc += c; } + if (c == '"') { + ssid_esc += "\\\""; + } else if (c == '\\') { + ssid_esc += "\\\\"; + } else { + ssid_esc += c; + } } - j << "{\"ssid\":\"" << ssid_esc << "\"" - << ",\"rssi\":" << n.rssi - << ",\"open\":" << (n.open ? "true" : "false") - << ",\"channel\":" << n.channel - << ",\"quality\":" << FwRssiQuality(n.rssi) - << "}"; + j << "{\"ssid\":\"" << ssid_esc << "\"" << ",\"rssi\":" << n.rssi + << ",\"open\":" << (n.open ? "true" : "false") << ",\"channel\":" << n.channel + << ",\"quality\":" << FwRssiQuality(n.rssi) << "}"; } - j << "],\"count\":" << nets.size() - << ",\"duration_ms\":" << duration_ms - << "}"; + j << "],\"count\":" << nets.size() << ",\"duration_ms\":" << duration_ms << "}"; return j.str(); } /// Comparator: sort networks by RSSI descending (best signal first). -inline bool FwNetworkBetterSignal(const WifiNetwork& a, const WifiNetwork& b) { +inline bool FwNetworkBetterSignal(const WifiNetwork &a, const WifiNetwork &b) { return a.rssi > b.rssi; } diff --git a/firmware/include/http_backend.h b/firmware/include/http_backend.h index 05fb0fd..e6db0ac 100644 --- a/firmware/include/http_backend.h +++ b/firmware/include/http_backend.h @@ -1,28 +1,23 @@ #pragma once -#include "voice_controller.h" - #include +#include "voice_controller.h" + /// HTTP client that talks to the Mascarade /device/v1/* endpoints. class HttpBackend : public BackendClient { - public: +public: /// @param base_url e.g. "http://192.168.1.42:8000" - explicit HttpBackend(const std::string& base_url); + explicit HttpBackend(const std::string &base_url); - bool SendPlayerEvent(const std::string& device_id, - const std::string& event_name, - const MediaSnapshot& media, - const std::string& detail) override; + bool SendPlayerEvent(const std::string &device_id, const std::string &event_name, + const MediaSnapshot &media, const std::string &detail) override; - VoiceSessionResponse SubmitVoiceSession( - const std::string& device_id, - const MediaSnapshot& media, - const std::vector& wav_data) override; + VoiceSessionResponse SubmitVoiceSession(const std::string &device_id, const MediaSnapshot &media, + const std::vector &wav_data) override; - bool DownloadReplyAudio(const std::string& audio_url, - std::vector* wav_data) override; + bool DownloadReplyAudio(const std::string &audio_url, std::vector *wav_data) override; - private: +private: std::string base_url_; }; diff --git a/firmware/include/i2s_mic.h b/firmware/include/i2s_mic.h index 2fb23c5..2993bdd 100644 --- a/firmware/include/i2s_mic.h +++ b/firmware/include/i2s_mic.h @@ -1,28 +1,27 @@ #pragma once #include -#include - #include +#include /// I2S microphone capture only (ICS-43434 on the Waveshare board). /// Uses the new I2S channel driver (ESP-IDF 5.x) to avoid conflicts /// with the ESP32-audioI2S library (RadioPlayer) which also uses it. class I2sMic { - public: +public: /// Initialise I2S1 for mic capture. /// Default pins for Waveshare ESP32-S3-LCD-1.85: SCK=15, WS=2, SD=39 bool Begin(int sck = 15, int ws = 2, int sd = 39, int sample_rate = 16000); /// Record duration_ms of audio → complete WAV buffer (44-byte header + PCM). - bool Capture(std::vector& wav_out, uint32_t duration_ms); + bool Capture(std::vector &wav_out, uint32_t duration_ms); void End(); int sample_rate() const { return sample_rate_; } - private: - static void WriteWavHeader(uint8_t* dest, uint32_t pcm_bytes, int sr); +private: + static void WriteWavHeader(uint8_t *dest, uint32_t pcm_bytes, int sr); int sample_rate_ = 16000; bool installed_ = false; i2s_chan_handle_t rx_handle_ = nullptr; diff --git a/firmware/include/lcd_ui.h b/firmware/include/lcd_ui.h index dc51425..240808c 100644 --- a/firmware/include/lcd_ui.h +++ b/firmware/include/lcd_ui.h @@ -6,22 +6,18 @@ /// LCD UI renderer for the Waveshare 1.85" round display (ST77916 QSPI). /// Uses ESP32_Display_Panel for hardware abstraction. class LcdUi : public UiRenderer { - public: +public: /// Initialise the display panel and backlight. bool Begin(); /// UiRenderer implementation — draw current state on the round LCD. - void Render(const MediaSnapshot& media, - VoicePhase phase, - const std::string& headline, - const std::string& summary, - bool show_ring) override; + void Render(const MediaSnapshot &media, VoicePhase phase, const std::string &headline, + const std::string &summary, bool show_ring) override; /// Show WiFi status screen (connecting, AP mode, etc.). - void RenderWifi(WifiManager::State state, const std::string& info, - const std::string& ap_ssid = "", - const std::string& ap_pass = "", - const std::string& ap_ip = ""); + void RenderWifi(WifiManager::State state, const std::string &info, + const std::string &ap_ssid = "", const std::string &ap_pass = "", + const std::string &ap_ip = ""); /// Set backlight brightness (0–100). void SetBrightness(uint8_t level); @@ -29,10 +25,10 @@ class LcdUi : public UiRenderer { /// Push current framebuffer to LCD. void Flush(); - private: +private: void DrawBackground(); - void DrawCenteredText(int y, const char* text, uint16_t color, int size); - void DrawStatusBar(const MediaSnapshot& media); + void DrawCenteredText(int y, const char *text, uint16_t color, int size); + void DrawStatusBar(const MediaSnapshot &media); void DrawVoiceRing(VoicePhase phase); bool initialized_ = false; diff --git a/firmware/include/ota_manager.h b/firmware/include/ota_manager.h index d5f78d1..473b9fa 100644 --- a/firmware/include/ota_manager.h +++ b/firmware/include/ota_manager.h @@ -5,11 +5,11 @@ /// Result of an OTA check. struct OtaCheckResult { enum class Status { - kUpToDate, ///< Already on latest version. + kUpToDate, ///< Already on latest version. kUpdateAvailable, ///< Newer firmware found. - kCheckFailed, ///< Could not reach backend. - kFlashFailed, ///< Download or flash error. - kFlashOk, ///< Flash successful — pending reboot. + kCheckFailed, ///< Could not reach backend. + kFlashFailed, ///< Download or flash error. + kFlashOk, ///< Flash successful — pending reboot. }; Status status = Status::kCheckFailed; @@ -33,11 +33,11 @@ struct OtaCheckResult { /// Response: {"latest": "1.0.1", "url": "http://...", "notes": "..."} /// or {"latest": "1.0.0"} when up-to-date class OtaManager { - public: - explicit OtaManager(const std::string& current_version); +public: + explicit OtaManager(const std::string ¤t_version); - void SetBackendUrl(const std::string& url) { backend_url_ = url; } - void SetDeviceId(const std::string& id) { device_id_ = id; } + void SetBackendUrl(const std::string &url) { backend_url_ = url; } + void SetDeviceId(const std::string &id) { device_id_ = id; } /// Check the backend for a newer version and flash if found. /// Blocks during download (typically 30–120s on good WiFi). @@ -46,11 +46,11 @@ class OtaManager { /// Convenience: just check version without flashing. OtaCheckResult CheckOnly(); - const std::string& current_version() const { return current_version_; } + const std::string ¤t_version() const { return current_version_; } - private: +private: OtaCheckResult FetchLatestInfo(); - bool FlashFromUrl(const std::string& url); + bool FlashFromUrl(const std::string &url); std::string current_version_; std::string backend_url_ = "http://192.168.1.42:8000"; diff --git a/firmware/include/voice_controller.h b/firmware/include/voice_controller.h index 32f10ee..3e87f3c 100644 --- a/firmware/include/voice_controller.h +++ b/firmware/include/voice_controller.h @@ -57,73 +57,61 @@ struct VoiceSessionResponse { }; class BackendClient { - public: +public: virtual ~BackendClient() = default; - virtual bool SendPlayerEvent(const std::string& device_id, - const std::string& event_name, - const MediaSnapshot& media, - const std::string& detail) = 0; + virtual bool SendPlayerEvent(const std::string &device_id, const std::string &event_name, + const MediaSnapshot &media, const std::string &detail) = 0; - virtual VoiceSessionResponse SubmitVoiceSession( - const std::string& device_id, - const MediaSnapshot& media, - const std::vector& wav_data) = 0; + virtual VoiceSessionResponse SubmitVoiceSession(const std::string &device_id, + const MediaSnapshot &media, + const std::vector &wav_data) = 0; - virtual bool DownloadReplyAudio(const std::string& audio_url, - std::vector* wav_data) = 0; + virtual bool DownloadReplyAudio(const std::string &audio_url, std::vector *wav_data) = 0; }; class MediaController { - public: +public: virtual ~MediaController() = default; virtual MediaSnapshot Snapshot() const = 0; - virtual void ApplyIntent(const VoiceIntent& intent) = 0; + virtual void ApplyIntent(const VoiceIntent &intent) = 0; virtual void PrepareForReply(PlayerAction action) = 0; virtual void RestoreAfterReply(bool resume_media_after_tts) = 0; - virtual bool PlayReplyAudio(const std::vector& wav_data) = 0; + virtual bool PlayReplyAudio(const std::vector &wav_data) = 0; }; class UiRenderer { - public: +public: virtual ~UiRenderer() = default; - virtual void Render(const MediaSnapshot& media, - VoicePhase phase, - const std::string& headline, - const std::string& summary, - bool show_ring) = 0; + virtual void Render(const MediaSnapshot &media, VoicePhase phase, const std::string &headline, + const std::string &summary, bool show_ring) = 0; }; class VoiceController { - public: - VoiceController(std::string device_id, - BackendClient& backend, - MediaController& media, - UiRenderer& ui); +public: + VoiceController(std::string device_id, BackendClient &backend, MediaController &media, + UiRenderer &ui); void Boot(); bool BeginPushToTalk(); - bool CompletePushToTalk(const std::vector& wav_data); + bool CompletePushToTalk(const std::vector &wav_data); VoicePhase phase() const { return phase_; } - const VoiceSessionResponse& last_response() const { return last_response_; } + const VoiceSessionResponse &last_response() const { return last_response_; } - private: - bool Fail(const std::string& error_text); - void RenderState(const std::string& headline, - const std::string& summary, - bool show_ring); - static std::string IdleSummary(const MediaSnapshot& media); - static bool ShouldPublishPlaybackStarted(const MediaSnapshot& before, - const MediaSnapshot& after, - const VoiceIntent& intent); +private: + bool Fail(const std::string &error_text); + void RenderState(const std::string &headline, const std::string &summary, bool show_ring); + static std::string IdleSummary(const MediaSnapshot &media); + static bool ShouldPublishPlaybackStarted(const MediaSnapshot &before, const MediaSnapshot &after, + const VoiceIntent &intent); std::string device_id_; - BackendClient& backend_; - MediaController& media_; - UiRenderer& ui_; + BackendClient &backend_; + MediaController &media_; + UiRenderer &ui_; VoicePhase phase_ = VoicePhase::kIdle; VoiceSessionResponse last_response_; }; diff --git a/firmware/include/wifi_manager.h b/firmware/include/wifi_manager.h index 8a5e225..cd17b56 100644 --- a/firmware/include/wifi_manager.h +++ b/firmware/include/wifi_manager.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include /// WiFi connection manager with AP fallback and captive portal. /// @@ -14,7 +14,7 @@ /// /// NVS keys: "wifi_ssid", "wifi_pass", "backend_url" class WifiManager { - public: +public: enum class State { kIdle, kConnecting, @@ -26,13 +26,13 @@ class WifiManager { struct ScanResult { std::string ssid; int rssi; - bool open; // no encryption + bool open; // no encryption }; - using OnStateChange = std::function; + using OnStateChange = std::function; /// AP mode settings. - void SetApCredentials(const std::string& ssid, const std::string& password); + void SetApCredentials(const std::string &ssid, const std::string &password); /// Register callback for state changes (for LCD updates). void SetOnStateChange(OnStateChange cb) { on_state_change_ = cb; } @@ -68,15 +68,14 @@ class WifiManager { /// Scan available networks (blocking, ~2s). std::vector Scan(); - private: +private: void LoadCredentials(); - void SaveCredentials(const std::string& ssid, const std::string& pass, - const std::string& backend); - bool TryConnect(const std::string& ssid, const std::string& pass, - uint32_t timeout_ms = 12000); + void SaveCredentials(const std::string &ssid, const std::string &pass, + const std::string &backend); + bool TryConnect(const std::string &ssid, const std::string &pass, uint32_t timeout_ms = 12000); void SetupApWebServer(); void StopApWebServer(); - void SetState(State s, const std::string& info = ""); + void SetState(State s, const std::string &info = ""); State state_ = State::kIdle; OnStateChange on_state_change_; diff --git a/firmware/include/wifi_scanner.h b/firmware/include/wifi_scanner.h index 678e850..8d83616 100644 --- a/firmware/include/wifi_scanner.h +++ b/firmware/include/wifi_scanner.h @@ -1,11 +1,11 @@ #pragma once -#include "firmware_utils.h" // WifiNetwork, FwRssiQuality, FwWifiToJson - #include #include #include +#include "firmware_utils.h" // WifiNetwork, FwRssiQuality, FwWifiToJson + /// Lightweight WiFi scanner. /// /// Usage: @@ -16,18 +16,18 @@ /// Pure utility functions (RssiQuality, ToJson, SortByRssi) live in /// firmware_utils.h and are tested natively via Unity. class WifiScanner { - public: +public: /// Scan available networks, block up to timeout_ms. /// Returns networks sorted by RSSI descending. std::vector Scan(uint32_t timeout_ms = 4000); /// JSON string from the last Scan() call. - const std::string& last_json() const { return last_json_; } + const std::string &last_json() const { return last_json_; } /// Duration of the last Scan() call in ms. uint32_t last_duration_ms() const { return last_duration_ms_; } - private: +private: std::string last_json_; uint32_t last_duration_ms_ = 0; }; diff --git a/firmware/src/font8x8.cpp b/firmware/src/font8x8.cpp index e81abc4..144a661 100644 --- a/firmware/src/font8x8.cpp +++ b/firmware/src/font8x8.cpp @@ -6,132 +6,132 @@ extern const uint8_t font8x8_basic[128][8]; const uint8_t font8x8_basic[128][8] = { - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x00 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x01 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x02 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x03 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x04 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x05 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x06 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x07 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x08 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x09 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0A - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0B - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0C - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0D - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0E - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0F - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x10 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x11 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x12 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x13 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x14 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x15 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x16 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x17 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x18 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x19 - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1A - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1B - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1C - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1D - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1E - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1F - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ' ' - { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00 }, // '!' - { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '"' - { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00 }, // '#' - { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00 }, // '$' - { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00 }, // '%' - { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00 }, // '&' - { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '\'' - { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00 }, // '(' - { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00 }, // ')' - { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 }, // '*' - { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00 }, // '+' - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06 }, // ',' - { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00 }, // '-' - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 }, // '.' - { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00 }, // '/' - { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00 }, // '0' - { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00 }, // '1' - { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00 }, // '2' - { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00 }, // '3' - { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00 }, // '4' - { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00 }, // '5' - { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00 }, // '6' - { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00 }, // '7' - { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00 }, // '8' - { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00 }, // '9' - { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00 }, // ':' - { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06 }, // ';' - { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00 }, // '<' - { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00 }, // '=' - { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00 }, // '>' - { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00 }, // '?' - { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00 }, // '@' - { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00 }, // 'A' - { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00 }, // 'B' - { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00 }, // 'C' - { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00 }, // 'D' - { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00 }, // 'E' - { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00 }, // 'F' - { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00 }, // 'G' - { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00 }, // 'H' - { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // 'I' - { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00 }, // 'J' - { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00 }, // 'K' - { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00 }, // 'L' - { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00 }, // 'M' - { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00 }, // 'N' - { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00 }, // 'O' - { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00 }, // 'P' - { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00 }, // 'Q' - { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00 }, // 'R' - { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00 }, // 'S' - { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // 'T' - { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00 }, // 'U' - { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 }, // 'V' - { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 }, // 'W' - { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00 }, // 'X' - { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00 }, // 'Y' - { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00 }, // 'Z' - { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00 }, // '[' - { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00 }, // '\\' - { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00 }, // ']' - { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00 }, // '^' - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }, // '_' - { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '`' - { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00 }, // 'a' - { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00 }, // 'b' - { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00 }, // 'c' - { 0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00 }, // 'd' - { 0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00 }, // 'e' - { 0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00 }, // 'f' - { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F }, // 'g' - { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00 }, // 'h' - { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // 'i' - { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E }, // 'j' - { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00 }, // 'k' - { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 }, // 'l' - { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00 }, // 'm' - { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00 }, // 'n' - { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00 }, // 'o' - { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F }, // 'p' - { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78 }, // 'q' - { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00 }, // 'r' - { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00 }, // 's' - { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00 }, // 't' - { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00 }, // 'u' - { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 }, // 'v' - { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00 }, // 'w' - { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00 }, // 'x' - { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F }, // 'y' - { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00 }, // 'z' - { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00 }, // '{' - { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00 }, // '|' - { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00 }, // '}' - { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '~' - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x7F + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x01 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x02 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x03 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x04 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x05 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x06 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x07 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x08 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x09 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x0A + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x0B + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x0C + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x0D + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x0E + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x0F + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x10 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x11 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x12 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x13 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x14 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x15 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x16 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x17 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x18 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x19 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x1A + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x1B + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x1C + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x1D + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x1E + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x1F + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' + {0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // '!' + {0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // '"' + {0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // '#' + {0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // '$' + {0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // '%' + {0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // '&' + {0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // '\'' + {0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // '(' + {0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // ')' + {0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // '*' + {0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // '+' + {0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // ',' + {0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // '-' + {0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // '.' + {0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // '/' + {0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // '0' + {0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // '1' + {0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // '2' + {0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // '3' + {0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // '4' + {0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // '5' + {0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // '6' + {0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // '7' + {0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // '8' + {0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // '9' + {0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // ':' + {0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // ';' + {0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // '<' + {0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // '=' + {0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // '>' + {0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // '?' + {0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // '@' + {0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // 'A' + {0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // 'B' + {0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // 'C' + {0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // 'D' + {0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // 'E' + {0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // 'F' + {0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // 'G' + {0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // 'H' + {0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // 'I' + {0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // 'J' + {0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // 'K' + {0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // 'L' + {0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // 'M' + {0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // 'N' + {0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // 'O' + {0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // 'P' + {0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // 'Q' + {0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // 'R' + {0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // 'S' + {0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // 'T' + {0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // 'U' + {0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // 'V' + {0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // 'W' + {0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // 'X' + {0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // 'Y' + {0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // 'Z' + {0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // '[' + {0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // '\\' + {0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // ']' + {0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // '^' + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // '_' + {0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // '`' + {0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // 'a' + {0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // 'b' + {0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // 'c' + {0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00}, // 'd' + {0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00}, // 'e' + {0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00}, // 'f' + {0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // 'g' + {0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // 'h' + {0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // 'i' + {0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // 'j' + {0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // 'k' + {0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // 'l' + {0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // 'm' + {0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // 'n' + {0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // 'o' + {0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // 'p' + {0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // 'q' + {0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // 'r' + {0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // 's' + {0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // 't' + {0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // 'u' + {0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // 'v' + {0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // 'w' + {0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // 'x' + {0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // 'y' + {0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // 'z' + {0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // '{' + {0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // '|' + {0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // '}' + {0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // '~' + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x7F }; diff --git a/firmware/src/http_backend.cpp b/firmware/src/http_backend.cpp index 4903884..8c69c63 100644 --- a/firmware/src/http_backend.cpp +++ b/firmware/src/http_backend.cpp @@ -1,13 +1,14 @@ #include "http_backend.h" #include + #include #include // ArduinoJson for parsing responses #include -HttpBackend::HttpBackend(const std::string& base_url) : base_url_(base_url) { +HttpBackend::HttpBackend(const std::string &base_url) : base_url_(base_url) { // Strip trailing slash. while (!base_url_.empty() && base_url_.back() == '/') { base_url_.pop_back(); @@ -17,10 +18,8 @@ HttpBackend::HttpBackend(const std::string& base_url) : base_url_(base_url) { // --------------------------------------------------------------------------- // POST /device/v1/player/event (JSON body) // --------------------------------------------------------------------------- -bool HttpBackend::SendPlayerEvent(const std::string& device_id, - const std::string& event_name, - const MediaSnapshot& media, - const std::string& detail) { +bool HttpBackend::SendPlayerEvent(const std::string &device_id, const std::string &event_name, + const MediaSnapshot &media, const std::string &detail) { HTTPClient http; const std::string url = base_url_ + "/device/v1/player/event"; http.begin(url.c_str()); @@ -29,22 +28,33 @@ bool HttpBackend::SendPlayerEvent(const std::string& device_id, JsonDocument doc; doc["device_id"] = device_id; - doc["event"] = event_name; - doc["detail"] = detail; + doc["event"] = event_name; + doc["detail"] = detail; // Media state fields switch (media.mode) { - case MediaMode::kMp3: doc["mode"] = "mp3"; break; - case MediaMode::kRadio: doc["mode"] = "radio"; break; - default: doc["mode"] = "idle"; break; + case MediaMode::kMp3: + doc["mode"] = "mp3"; + break; + case MediaMode::kRadio: + doc["mode"] = "radio"; + break; + default: + doc["mode"] = "idle"; + break; } - doc["playing"] = media.playing; - doc["volume"] = media.volume; - if (!media.station.empty()) doc["station"] = media.station; - if (!media.track.empty()) doc["track"] = media.track; - if (media.battery_pct >= 0) doc["battery_pct"] = media.battery_pct; - if (!media.wifi_ssid.empty()) doc["wifi_ssid"] = media.wifi_ssid; - if (media.wifi_rssi != 0) doc["wifi_rssi"] = media.wifi_rssi; + doc["playing"] = media.playing; + doc["volume"] = media.volume; + if (!media.station.empty()) + doc["station"] = media.station; + if (!media.track.empty()) + doc["track"] = media.track; + if (media.battery_pct >= 0) + doc["battery_pct"] = media.battery_pct; + if (!media.wifi_ssid.empty()) + doc["wifi_ssid"] = media.wifi_ssid; + if (media.wifi_rssi != 0) + doc["wifi_rssi"] = media.wifi_rssi; String body; serializeJson(doc, body); @@ -64,30 +74,40 @@ bool HttpBackend::SendPlayerEvent(const std::string& device_id, // POST /device/v1/voice/session (multipart/form-data) // Fields: device_id, mode, current_media (JSON), audio (WAV file) // --------------------------------------------------------------------------- -VoiceSessionResponse HttpBackend::SubmitVoiceSession( - const std::string& device_id, - const MediaSnapshot& media, - const std::vector& wav_data) { +VoiceSessionResponse HttpBackend::SubmitVoiceSession(const std::string &device_id, + const MediaSnapshot &media, + const std::vector &wav_data) { VoiceSessionResponse result; // Build current_media JSON string. JsonDocument media_doc; switch (media.mode) { - case MediaMode::kMp3: media_doc["mode"] = "mp3"; break; - case MediaMode::kRadio: media_doc["mode"] = "radio"; break; - default: media_doc["mode"] = "idle"; break; + case MediaMode::kMp3: + media_doc["mode"] = "mp3"; + break; + case MediaMode::kRadio: + media_doc["mode"] = "radio"; + break; + default: + media_doc["mode"] = "idle"; + break; } media_doc["playing"] = media.playing; - media_doc["volume"] = media.volume; - if (!media.station.empty()) media_doc["station"] = media.station; - if (!media.track.empty()) media_doc["track"] = media.track; - if (media.battery_pct >= 0) media_doc["battery_pct"] = media.battery_pct; - if (!media.wifi_ssid.empty()) media_doc["wifi_ssid"] = media.wifi_ssid; - if (media.wifi_rssi != 0) media_doc["wifi_rssi"] = media.wifi_rssi; + media_doc["volume"] = media.volume; + if (!media.station.empty()) + media_doc["station"] = media.station; + if (!media.track.empty()) + media_doc["track"] = media.track; + if (media.battery_pct >= 0) + media_doc["battery_pct"] = media.battery_pct; + if (!media.wifi_ssid.empty()) + media_doc["wifi_ssid"] = media.wifi_ssid; + if (media.wifi_rssi != 0) + media_doc["wifi_rssi"] = media.wifi_rssi; JsonArray stations = media_doc["available_stations"].to(); - for (const auto& s : media.available_stations) { + for (const auto &s : media.available_stations) { stations.add(s); } @@ -95,9 +115,11 @@ VoiceSessionResponse HttpBackend::SubmitVoiceSession( serializeJson(media_doc, media_json); // Mode string. - const char* mode_str = "idle"; - if (media.mode == MediaMode::kMp3) mode_str = "mp3"; - if (media.mode == MediaMode::kRadio) mode_str = "radio"; + const char *mode_str = "idle"; + if (media.mode == MediaMode::kMp3) + mode_str = "mp3"; + if (media.mode == MediaMode::kRadio) + mode_str = "radio"; // Build multipart body manually (ESP32 HTTPClient has no multipart helper). const String boundary = "----KillLife" + String(millis()); @@ -129,7 +151,7 @@ VoiceSessionResponse HttpBackend::SubmitVoiceSession( http.begin(tcp, url.c_str()); http.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary); http.addHeader("Content-Length", String((unsigned long)total_len)); - http.setTimeout(30000); // STT + LLM + TTS can take a while. + http.setTimeout(30000); // STT + LLM + TTS can take a while. // sendRequest allows streaming the body in parts. // We must use the low-level approach. @@ -142,11 +164,9 @@ VoiceSessionResponse HttpBackend::SubmitVoiceSession( // For ESP32 with PSRAM this is fine up to ~256 KB. std::vector body; body.reserve(total_len); - body.insert(body.end(), (uint8_t*)head.c_str(), - (uint8_t*)head.c_str() + head.length()); + body.insert(body.end(), (uint8_t *)head.c_str(), (uint8_t *)head.c_str() + head.length()); body.insert(body.end(), wav_data.begin(), wav_data.end()); - body.insert(body.end(), (uint8_t*)tail.c_str(), - (uint8_t*)tail.c_str() + tail.length()); + body.insert(body.end(), (uint8_t *)tail.c_str(), (uint8_t *)tail.c_str() + tail.length()); int code = http.POST(body.data(), body.size()); @@ -171,15 +191,15 @@ VoiceSessionResponse HttpBackend::SubmitVoiceSession( return result; } - result.ok = resp_doc["ok"] | false; + result.ok = resp_doc["ok"] | false; result.session_id = resp_doc["session_id"] | ""; result.transcript = resp_doc["transcript"] | ""; result.reply_text = resp_doc["reply_text"] | ""; - result.provider = resp_doc["provider"] | "none"; - result.error = resp_doc["error"] | ""; + result.provider = resp_doc["provider"] | "none"; + result.error = resp_doc["error"] | ""; // Reply audio URL. - const char* audio_url = resp_doc["reply_audio_url"]; + const char *audio_url = resp_doc["reply_audio_url"]; if (audio_url) { result.reply_audio_url = std::string(audio_url); } @@ -187,32 +207,33 @@ VoiceSessionResponse HttpBackend::SubmitVoiceSession( // Intent. JsonObject intent = resp_doc["intent"]; if (intent) { - result.intent.type = intent["type"] | "none"; + result.intent.type = intent["type"] | "none"; result.intent.target = intent["target"] | ""; - result.intent.value = intent["value"] | ""; + result.intent.value = intent["value"] | ""; result.intent.spoken_confirmation = intent["spoken_confirmation"] | ""; result.intent.resume_media_after_tts = intent["resume_media_after_tts"] | false; } // Player action. - const char* pa = resp_doc["player_action"]; + const char *pa = resp_doc["player_action"]; if (pa) { std::string pa_str(pa); - if (pa_str == "duck") result.player_action = PlayerAction::kDuck; - else if (pa_str == "stop_resume") result.player_action = PlayerAction::kStopResume; - else result.player_action = PlayerAction::kNone; + if (pa_str == "duck") + result.player_action = PlayerAction::kDuck; + else if (pa_str == "stop_resume") + result.player_action = PlayerAction::kStopResume; + else + result.player_action = PlayerAction::kNone; } - Serial.printf("[http] voice session OK — transcript: %s\n", - result.transcript.c_str()); + Serial.printf("[http] voice session OK — transcript: %s\n", result.transcript.c_str()); return result; } // --------------------------------------------------------------------------- // GET reply audio WAV // --------------------------------------------------------------------------- -bool HttpBackend::DownloadReplyAudio(const std::string& audio_url, - std::vector* wav_data) { +bool HttpBackend::DownloadReplyAudio(const std::string &audio_url, std::vector *wav_data) { HTTPClient http; http.begin(audio_url.c_str()); http.setTimeout(10000); @@ -225,7 +246,7 @@ bool HttpBackend::DownloadReplyAudio(const std::string& audio_url, } int len = http.getSize(); - WiFiClient* stream = http.getStreamPtr(); + WiFiClient *stream = http.getStreamPtr(); if (!stream) { http.end(); return false; @@ -244,14 +265,15 @@ bool HttpBackend::DownloadReplyAudio(const std::string& audio_url, continue; } size_t to_read = avail; - if (to_read > sizeof(buf)) to_read = sizeof(buf); + if (to_read > sizeof(buf)) + to_read = sizeof(buf); size_t read = stream->readBytes(buf, to_read); wav_data->insert(wav_data->end(), buf, buf + read); - if (len > 0) len -= read; + if (len > 0) + len -= read; } http.end(); - Serial.printf("[http] downloaded %u bytes reply audio\n", - (unsigned)wav_data->size()); - return wav_data->size() > 44; // At least a WAV header. + Serial.printf("[http] downloaded %u bytes reply audio\n", (unsigned)wav_data->size()); + return wav_data->size() > 44; // At least a WAV header. } diff --git a/firmware/src/i2s_mic.cpp b/firmware/src/i2s_mic.cpp index f606a3a..3976143 100644 --- a/firmware/src/i2s_mic.cpp +++ b/firmware/src/i2s_mic.cpp @@ -1,10 +1,11 @@ #include "i2s_mic.h" -#include #include +#include + static constexpr int kDmaBufCount = 8; -static constexpr int kDmaBufLen = 1024; +static constexpr int kDmaBufLen = 1024; // --------------------------------------------------------------------------- bool I2sMic::Begin(int sck, int ws, int sd, int sample_rate) { @@ -29,26 +30,28 @@ bool I2sMic::Begin(int sck, int ws, int sd, int sample_rate) { // Standard mode config — mono left channel, 16-bit i2s_std_config_t std_cfg = { - .clk_cfg = { - .sample_rate_hz = (uint32_t)sample_rate_, - .clk_src = I2S_CLK_SRC_DEFAULT, - .ext_clk_freq_hz = 0, - .mclk_multiple = I2S_MCLK_MULTIPLE_256, - }, - .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, - I2S_SLOT_MODE_MONO), - .gpio_cfg = { - .mclk = I2S_GPIO_UNUSED, - .bclk = (gpio_num_t)sck, - .ws = (gpio_num_t)ws, - .dout = I2S_GPIO_UNUSED, - .din = (gpio_num_t)sd, - .invert_flags = { - .mclk_inv = false, - .bclk_inv = false, - .ws_inv = false, + .clk_cfg = + { + .sample_rate_hz = (uint32_t)sample_rate_, + .clk_src = I2S_CLK_SRC_DEFAULT, + .ext_clk_freq_hz = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, + }, + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = + { + .mclk = I2S_GPIO_UNUSED, + .bclk = (gpio_num_t)sck, + .ws = (gpio_num_t)ws, + .dout = I2S_GPIO_UNUSED, + .din = (gpio_num_t)sd, + .invert_flags = + { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }, }, - }, }; err = i2s_channel_init_std_mode(rx_handle_, &std_cfg); @@ -68,23 +71,24 @@ bool I2sMic::Begin(int sck, int ws, int sd, int sample_rate) { } installed_ = true; - Serial.printf("[mic] ready — SCK=%d WS=%d SD=%d @ %d Hz (new driver)\n", - sck, ws, sd, sample_rate_); + Serial.printf("[mic] ready — SCK=%d WS=%d SD=%d @ %d Hz (new driver)\n", sck, ws, sd, + sample_rate_); return true; } // --------------------------------------------------------------------------- -bool I2sMic::Capture(std::vector& wav_out, uint32_t duration_ms) { - if (!installed_ || !rx_handle_) return false; +bool I2sMic::Capture(std::vector &wav_out, uint32_t duration_ms) { + if (!installed_ || !rx_handle_) + return false; const uint32_t bytes_per_sec = sample_rate_ * sizeof(int16_t); - const uint32_t pcm_bytes = (bytes_per_sec * duration_ms) / 1000; + const uint32_t pcm_bytes = (bytes_per_sec * duration_ms) / 1000; wav_out.resize(44 + pcm_bytes); WriteWavHeader(wav_out.data(), pcm_bytes, sample_rate_); size_t total = 0; - uint8_t* dest = wav_out.data() + 44; + uint8_t *dest = wav_out.data() + 44; while (total < pcm_bytes) { size_t chunk = pcm_bytes - total; @@ -92,14 +96,13 @@ bool I2sMic::Capture(std::vector& wav_out, uint32_t duration_ms) { chunk = kDmaBufLen * sizeof(int16_t); size_t rd = 0; - esp_err_t err = i2s_channel_read(rx_handle_, dest + total, chunk, &rd, - portMAX_DELAY); - if (err != ESP_OK) return false; + esp_err_t err = i2s_channel_read(rx_handle_, dest + total, chunk, &rd, portMAX_DELAY); + if (err != ESP_OK) + return false; total += rd; } - Serial.printf("[mic] captured %u bytes (%u ms)\n", - (unsigned)total, (unsigned)duration_ms); + Serial.printf("[mic] captured %u bytes (%u ms)\n", (unsigned)total, (unsigned)duration_ms); return true; } @@ -114,20 +117,26 @@ void I2sMic::End() { } // --------------------------------------------------------------------------- -void I2sMic::WriteWavHeader(uint8_t* h, uint32_t pcm_bytes, int sr) { - const uint32_t file_size = 36 + pcm_bytes; - const uint16_t channels = 1; - const uint16_t bits = 16; - const uint32_t byte_rate = sr * channels * (bits / 8); +void I2sMic::WriteWavHeader(uint8_t *h, uint32_t pcm_bytes, int sr) { + const uint32_t file_size = 36 + pcm_bytes; + const uint16_t channels = 1; + const uint16_t bits = 16; + const uint32_t byte_rate = sr * channels * (bits / 8); const uint16_t block_align = channels * (bits / 8); - const uint32_t fmt_size = 16; - const uint16_t pcm_fmt = 1; + const uint32_t fmt_size = 16; + const uint16_t pcm_fmt = 1; - memcpy(h + 0, "RIFF", 4); memcpy(h + 4, &file_size, 4); - memcpy(h + 8, "WAVE", 4); memcpy(h + 12, "fmt ", 4); - memcpy(h + 16, &fmt_size, 4); memcpy(h + 20, &pcm_fmt, 2); - memcpy(h + 22, &channels, 2); memcpy(h + 24, &sr, 4); - memcpy(h + 28, &byte_rate, 4); memcpy(h + 32, &block_align, 2); - memcpy(h + 34, &bits, 2); memcpy(h + 36, "data", 4); + memcpy(h + 0, "RIFF", 4); + memcpy(h + 4, &file_size, 4); + memcpy(h + 8, "WAVE", 4); + memcpy(h + 12, "fmt ", 4); + memcpy(h + 16, &fmt_size, 4); + memcpy(h + 20, &pcm_fmt, 2); + memcpy(h + 22, &channels, 2); + memcpy(h + 24, &sr, 4); + memcpy(h + 28, &byte_rate, 4); + memcpy(h + 32, &block_align, 2); + memcpy(h + 34, &bits, 2); + memcpy(h + 36, "data", 4); memcpy(h + 40, &pcm_bytes, 4); } diff --git a/firmware/src/lcd_ui.cpp b/firmware/src/lcd_ui.cpp index b7fef6d..8f2d17f 100644 --- a/firmware/src/lcd_ui.cpp +++ b/firmware/src/lcd_ui.cpp @@ -1,6 +1,7 @@ #include "lcd_ui.h" #include + #include // --------------------------------------------------------------------------- @@ -12,26 +13,26 @@ using Board = esp_panel::board::Board; using LCD = esp_panel::drivers::LCD; -static Board* panel = nullptr; -static LCD* lcd = nullptr; +static Board *panel = nullptr; +static LCD *lcd = nullptr; -static constexpr int kWidth = 360; +static constexpr int kWidth = 360; static constexpr int kHeight = 360; static constexpr int kCenter = 180; // Simple 16-bit RGB565 colors -static constexpr uint16_t kBlack = 0x0000; -static constexpr uint16_t kWhite = 0xFFFF; -static constexpr uint16_t kGreen = 0x07E0; -static constexpr uint16_t kRed = 0xF800; -static constexpr uint16_t kBlue = 0x001F; -static constexpr uint16_t kCyan = 0x07FF; -static constexpr uint16_t kYellow = 0xFFE0; +static constexpr uint16_t kBlack = 0x0000; +static constexpr uint16_t kWhite = 0xFFFF; +static constexpr uint16_t kGreen = 0x07E0; +static constexpr uint16_t kRed = 0xF800; +static constexpr uint16_t kBlue = 0x001F; +static constexpr uint16_t kCyan = 0x07FF; +static constexpr uint16_t kYellow = 0xFFE0; static constexpr uint16_t kDarkGray = 0x4208; -static constexpr uint16_t kOrange = 0xFD20; +static constexpr uint16_t kOrange = 0xFD20; // Framebuffer — 360×360×2 = 259 KB, fits in PSRAM. -static uint16_t* fb = nullptr; +static uint16_t *fb = nullptr; // --------------------------------------------------------------------------- static inline void fbPixel(int x, int y, uint16_t color) { @@ -41,7 +42,8 @@ static inline void fbPixel(int x, int y, uint16_t color) { } static void fbFill(uint16_t color) { - for (int i = 0; i < kWidth * kHeight; i++) fb[i] = color; + for (int i = 0; i < kWidth * kHeight; i++) + fb[i] = color; } static void fbFillRect(int x0, int y0, int w, int h, uint16_t color) { @@ -53,16 +55,18 @@ static void fbFillRect(int x0, int y0, int w, int h, uint16_t color) { } static void fbHLine(int x0, int y, int w, uint16_t color) { - for (int x = x0; x < x0 + w; x++) fbPixel(x, y, color); + for (int x = x0; x < x0 + w; x++) + fbPixel(x, y, color); } // Simple 8×8 font character renderer (built-in). // For a real product, use LVGL or a proper font engine. // This provides basic text for status display. -extern const uint8_t font8x8_basic[128][8]; // from font8x8.cpp +extern const uint8_t font8x8_basic[128][8]; // from font8x8.cpp static void fbChar(int x0, int y0, char ch, uint16_t color, int scale) { - if (ch < 0 || ch > 127) ch = '?'; + if (ch < 0 || ch > 127) + ch = '?'; for (int row = 0; row < 8; row++) { uint8_t bits = font8x8_basic[(int)ch][row]; for (int col = 0; col < 8; col++) { @@ -75,21 +79,30 @@ static void fbChar(int x0, int y0, char ch, uint16_t color, int scale) { } } -static void fbString(int x0, int y0, const char* str, uint16_t color, int scale) { +static void fbString(int x0, int y0, const char *str, uint16_t color, int scale) { int x = x0; while (*str) { - if (*str == '\n') { y0 += 8 * scale + 2; x = x0; str++; continue; } + if (*str == '\n') { + y0 += 8 * scale + 2; + x = x0; + str++; + continue; + } fbChar(x, y0, *str, color, scale); x += 8 * scale; str++; } } -static int textPixelWidth(const char* str, int scale) { +static int textPixelWidth(const char *str, int scale) { int maxw = 0, w = 0; while (*str) { - if (*str == '\n') { if (w > maxw) maxw = w; w = 0; } - else w += 8 * scale; + if (*str == '\n') { + if (w > maxw) + maxw = w; + w = 0; + } else + w += 8 * scale; str++; } return w > maxw ? w : maxw; @@ -99,12 +112,20 @@ static int textPixelWidth(const char* str, int scale) { static void fbCircle(int cx, int cy, int r, uint16_t color) { int x = 0, y = r, d = 3 - 2 * r; while (x <= y) { - fbPixel(cx + x, cy + y, color); fbPixel(cx - x, cy + y, color); - fbPixel(cx + x, cy - y, color); fbPixel(cx - x, cy - y, color); - fbPixel(cx + y, cy + x, color); fbPixel(cx - y, cy + x, color); - fbPixel(cx + y, cy - x, color); fbPixel(cx - y, cy - x, color); - if (d < 0) d += 4 * x + 6; - else { d += 4 * (x - y) + 10; y--; } + fbPixel(cx + x, cy + y, color); + fbPixel(cx - x, cy + y, color); + fbPixel(cx + x, cy - y, color); + fbPixel(cx - x, cy - y, color); + fbPixel(cx + y, cy + x, color); + fbPixel(cx - y, cy + x, color); + fbPixel(cx + y, cy - x, color); + fbPixel(cx - y, cy - x, color); + if (d < 0) + d += 4 * x + 6; + else { + d += 4 * (x - y) + 10; + y--; + } x++; } } @@ -124,19 +145,20 @@ bool LcdUi::Begin() { } // Allocate framebuffer in PSRAM. - fb = (uint16_t*)ps_malloc(kWidth * kHeight * sizeof(uint16_t)); + fb = (uint16_t *)ps_malloc(kWidth * kHeight * sizeof(uint16_t)); if (!fb) { Serial.println("[lcd] FAILED — cannot allocate framebuffer"); return false; } // Backlight on. - auto* bl = panel->getBacklight(); - if (bl) bl->setBrightness(80); // 0–100 + auto *bl = panel->getBacklight(); + if (bl) + bl->setBrightness(80); // 0–100 // Clear to black. fbFill(kBlack); - lcd->drawBitmap(0, 0, kWidth, kHeight, (const uint8_t*)fb); + lcd->drawBitmap(0, 0, kWidth, kHeight, (const uint8_t *)fb); initialized_ = true; Serial.println("[lcd] ready — 360x360 ST77916 QSPI"); @@ -145,21 +167,19 @@ bool LcdUi::Begin() { // --------------------------------------------------------------------------- void LcdUi::SetBrightness(uint8_t level) { - if (!panel) return; - auto* bl = panel->getBacklight(); - if (bl) bl->setBrightness(level); + if (!panel) + return; + auto *bl = panel->getBacklight(); + if (bl) + bl->setBrightness(level); } // --------------------------------------------------------------------------- -void LcdUi::Render(const MediaSnapshot& media, - VoicePhase phase, - const std::string& headline, - const std::string& summary, - bool show_ring) { +void LcdUi::Render(const MediaSnapshot &media, VoicePhase phase, const std::string &headline, + const std::string &summary, bool show_ring) { if (!initialized_) { // Fallback to serial. - Serial.printf("[ui] [%d] %s — %s\n", (int)phase, - headline.c_str(), summary.c_str()); + Serial.printf("[ui] [%d] %s — %s\n", (int)phase, headline.c_str(), summary.c_str()); return; } @@ -182,58 +202,58 @@ void LcdUi::Render(const MediaSnapshot& media, } // --------------------------------------------------------------------------- -void LcdUi::RenderWifi(WifiManager::State state, const std::string& info, - const std::string& ap_ssid, - const std::string& ap_pass, - const std::string& ap_ip) { - if (!initialized_) return; +void LcdUi::RenderWifi(WifiManager::State state, const std::string &info, + const std::string &ap_ssid, const std::string &ap_pass, + const std::string &ap_ip) { + if (!initialized_) + return; DrawBackground(); switch (state) { - case WifiManager::State::kConnecting: - // Pulsing cyan ring. - for (int r = kCenter - 8; r <= kCenter - 4; r++) - fbCircle(kCenter, kCenter, r, kCyan); - DrawCenteredText(kCenter - 40, "WiFi", kWhite, 3); - DrawCenteredText(kCenter, "Connexion...", kCyan, 2); - if (!info.empty()) { - DrawCenteredText(kCenter + 30, info.c_str(), kDarkGray, 2); - } - break; + case WifiManager::State::kConnecting: + // Pulsing cyan ring. + for (int r = kCenter - 8; r <= kCenter - 4; r++) + fbCircle(kCenter, kCenter, r, kCyan); + DrawCenteredText(kCenter - 40, "WiFi", kWhite, 3); + DrawCenteredText(kCenter, "Connexion...", kCyan, 2); + if (!info.empty()) { + DrawCenteredText(kCenter + 30, info.c_str(), kDarkGray, 2); + } + break; - case WifiManager::State::kApMode: - // Orange ring for AP mode. - for (int r = kCenter - 8; r <= kCenter - 4; r++) - fbCircle(kCenter, kCenter, r, kOrange); - DrawCenteredText(60, "MODE CONFIG", kOrange, 2); - DrawCenteredText(kCenter - 50, "WiFi:", kWhite, 2); - DrawCenteredText(kCenter - 25, ap_ssid.c_str(), kCyan, 2); - DrawCenteredText(kCenter + 5, "Pass:", kWhite, 2); - DrawCenteredText(kCenter + 30, ap_pass.c_str(), kCyan, 2); - DrawCenteredText(kCenter + 65, "http://", kDarkGray, 1); - DrawCenteredText(kCenter + 80, ap_ip.c_str(), kGreen, 2); - DrawCenteredText(kHeight - 50, "Connectez-vous au", kDarkGray, 1); - DrawCenteredText(kHeight - 38, "WiFi ci-dessus", kDarkGray, 1); - break; + case WifiManager::State::kApMode: + // Orange ring for AP mode. + for (int r = kCenter - 8; r <= kCenter - 4; r++) + fbCircle(kCenter, kCenter, r, kOrange); + DrawCenteredText(60, "MODE CONFIG", kOrange, 2); + DrawCenteredText(kCenter - 50, "WiFi:", kWhite, 2); + DrawCenteredText(kCenter - 25, ap_ssid.c_str(), kCyan, 2); + DrawCenteredText(kCenter + 5, "Pass:", kWhite, 2); + DrawCenteredText(kCenter + 30, ap_pass.c_str(), kCyan, 2); + DrawCenteredText(kCenter + 65, "http://", kDarkGray, 1); + DrawCenteredText(kCenter + 80, ap_ip.c_str(), kGreen, 2); + DrawCenteredText(kHeight - 50, "Connectez-vous au", kDarkGray, 1); + DrawCenteredText(kHeight - 38, "WiFi ci-dessus", kDarkGray, 1); + break; - case WifiManager::State::kConnected: - for (int r = kCenter - 8; r <= kCenter - 4; r++) - fbCircle(kCenter, kCenter, r, kGreen); - DrawCenteredText(kCenter - 20, "Connecte!", kGreen, 3); - DrawCenteredText(kCenter + 20, info.c_str(), kWhite, 2); - break; + case WifiManager::State::kConnected: + for (int r = kCenter - 8; r <= kCenter - 4; r++) + fbCircle(kCenter, kCenter, r, kGreen); + DrawCenteredText(kCenter - 20, "Connecte!", kGreen, 3); + DrawCenteredText(kCenter + 20, info.c_str(), kWhite, 2); + break; - case WifiManager::State::kFailed: - for (int r = kCenter - 8; r <= kCenter - 4; r++) - fbCircle(kCenter, kCenter, r, kRed); - DrawCenteredText(kCenter - 20, "Echec WiFi", kRed, 3); - DrawCenteredText(kCenter + 20, info.c_str(), kDarkGray, 2); - DrawCenteredText(kCenter + 50, "Appui long=config", kYellow, 1); - break; + case WifiManager::State::kFailed: + for (int r = kCenter - 8; r <= kCenter - 4; r++) + fbCircle(kCenter, kCenter, r, kRed); + DrawCenteredText(kCenter - 20, "Echec WiFi", kRed, 3); + DrawCenteredText(kCenter + 20, info.c_str(), kDarkGray, 2); + DrawCenteredText(kCenter + 50, "Appui long=config", kYellow, 1); + break; - default: - break; + default: + break; } Flush(); @@ -242,7 +262,7 @@ void LcdUi::RenderWifi(WifiManager::State state, const std::string& info, // --------------------------------------------------------------------------- void LcdUi::Flush() { if (initialized_ && lcd && fb) { - lcd->drawBitmap(0, 0, kWidth, kHeight, (const uint8_t*)fb); + lcd->drawBitmap(0, 0, kWidth, kHeight, (const uint8_t *)fb); } } @@ -254,19 +274,22 @@ void LcdUi::DrawBackground() { fbCircle(kCenter, kCenter, kCenter - 2, kDarkGray); } -void LcdUi::DrawCenteredText(int y, const char* text, uint16_t color, int sz) { +void LcdUi::DrawCenteredText(int y, const char *text, uint16_t color, int sz) { int w = textPixelWidth(text, sz); int x = (kWidth - w) / 2; - if (x < 10) x = 10; + if (x < 10) + x = 10; fbString(x, y, text, color, sz); } -void LcdUi::DrawStatusBar(const MediaSnapshot& media) { +void LcdUi::DrawStatusBar(const MediaSnapshot &media) { // Top area: mode + volume. char buf[48]; - const char* mode_str = "IDLE"; - if (media.mode == MediaMode::kMp3) mode_str = "MP3"; - if (media.mode == MediaMode::kRadio) mode_str = "RADIO"; + const char *mode_str = "IDLE"; + if (media.mode == MediaMode::kMp3) + mode_str = "MP3"; + if (media.mode == MediaMode::kRadio) + mode_str = "RADIO"; snprintf(buf, sizeof(buf), "%s vol:%d", mode_str, media.volume); DrawCenteredText(40, buf, kYellow, 2); @@ -287,11 +310,21 @@ void LcdUi::DrawStatusBar(const MediaSnapshot& media) { void LcdUi::DrawVoiceRing(VoicePhase phase) { uint16_t color = kBlue; switch (phase) { - case VoicePhase::kRecording: color = kRed; break; - case VoicePhase::kThinking: color = kOrange; break; - case VoicePhase::kSpeaking: color = kGreen; break; - case VoicePhase::kError: color = kRed; break; - default: color = kBlue; break; + case VoicePhase::kRecording: + color = kRed; + break; + case VoicePhase::kThinking: + color = kOrange; + break; + case VoicePhase::kSpeaking: + color = kGreen; + break; + case VoicePhase::kError: + color = kRed; + break; + default: + color = kBlue; + break; } for (int r = kCenter - 10; r <= kCenter - 5; r++) { fbCircle(kCenter, kCenter, r, color); diff --git a/firmware/src/native_stub.cpp b/firmware/src/native_stub.cpp index dd889f7..7ae748f 100644 --- a/firmware/src/native_stub.cpp +++ b/firmware/src/native_stub.cpp @@ -1,5 +1,7 @@ /* Native build stub — compile guard for host-side toolchain validation only. Actual firmware runs on ESP32 targets (esp32s3_arduino, esp32_arduino). */ #ifdef UNIT_TEST -int main() { return 0; } +int main() { + return 0; +} #endif diff --git a/firmware/src/ota_manager.cpp b/firmware/src/ota_manager.cpp index 9f919e4..0243469 100644 --- a/firmware/src/ota_manager.cpp +++ b/firmware/src/ota_manager.cpp @@ -1,15 +1,16 @@ #include "ota_manager.h" -#include "firmware_utils.h" #include #include + #include #include #include #include -OtaManager::OtaManager(const std::string& current_version) - : current_version_(current_version) {} +#include "firmware_utils.h" + +OtaManager::OtaManager(const std::string ¤t_version) : current_version_(current_version) {} OtaCheckResult OtaManager::CheckOnly() { return FetchLatestInfo(); @@ -75,8 +76,8 @@ OtaCheckResult OtaManager::FetchLatestInfo() { } result.latest_version = resp_doc["latest"].as(); - result.url = resp_doc["url"].as(); - result.notes = resp_doc["notes"].as(); + result.url = resp_doc["url"].as(); + result.notes = resp_doc["notes"].as(); if (FwCompareVersions(current_version_, result.latest_version) >= 0) { result.status = OtaCheckResult::Status::kUpToDate; @@ -87,8 +88,9 @@ OtaCheckResult OtaManager::FetchLatestInfo() { return result; } -bool OtaManager::FlashFromUrl(const std::string& url) { - if (url.empty()) return false; +bool OtaManager::FlashFromUrl(const std::string &url) { + if (url.empty()) + return false; HTTPClient http; http.begin(url.c_str()); @@ -100,12 +102,11 @@ bool OtaManager::FlashFromUrl(const std::string& url) { } const int content_len = http.getSize(); - WiFiClient* stream = http.getStreamPtr(); + WiFiClient *stream = http.getStreamPtr(); if (!Update.begin(content_len > 0 ? content_len : UPDATE_SIZE_UNKNOWN)) { http.end(); - Serial.printf("[OTA] Update.begin failed: %s\n", - Update.errorString()); + Serial.printf("[OTA] Update.begin failed: %s\n", Update.errorString()); return false; } diff --git a/firmware/src/voice_controller.cpp b/firmware/src/voice_controller.cpp index 0fef37b..ce10314 100644 --- a/firmware/src/voice_controller.cpp +++ b/firmware/src/voice_controller.cpp @@ -1,24 +1,19 @@ #include "voice_controller.h" -#include "firmware_utils.h" -#include // yield() — feeds WDT, lets other FreeRTOS tasks run +#include // yield() — feeds WDT, lets other FreeRTOS tasks run #include -VoiceController::VoiceController(std::string device_id, - BackendClient& backend, - MediaController& media, - UiRenderer& ui) - : device_id_(std::move(device_id)), - backend_(backend), - media_(media), - ui_(ui) {} +#include "firmware_utils.h" + +VoiceController::VoiceController(std::string device_id, BackendClient &backend, + MediaController &media, UiRenderer &ui) + : device_id_(std::move(device_id)), backend_(backend), media_(media), ui_(ui) {} void VoiceController::Boot() { phase_ = VoicePhase::kIdle; RenderState("pret", IdleSummary(media_.Snapshot()), false); - backend_.SendPlayerEvent(device_id_, "boot", media_.Snapshot(), - "voice-controller-ready"); + backend_.SendPlayerEvent(device_id_, "boot", media_.Snapshot(), "voice-controller-ready"); } bool VoiceController::BeginPushToTalk() { @@ -31,7 +26,7 @@ bool VoiceController::BeginPushToTalk() { return true; } -bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { +bool VoiceController::CompletePushToTalk(const std::vector &wav_data) { if (phase_ != VoicePhase::kRecording) { return false; } @@ -43,14 +38,12 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { phase_ = VoicePhase::kThinking; RenderState("je reflechis", "Envoi de la requete a Mascarade.", true); - last_response_ = backend_.SubmitVoiceSession(device_id_, media_.Snapshot(), - wav_data); - yield(); // WDT: feed watchdog after HTTP POST round-trip + last_response_ = backend_.SubmitVoiceSession(device_id_, media_.Snapshot(), wav_data); + yield(); // WDT: feed watchdog after HTTP POST round-trip if (!last_response_.ok) { const std::string error_text = - last_response_.error.empty() ? "session vocale en echec" - : last_response_.error; + last_response_.error.empty() ? "session vocale en echec" : last_response_.error; return Fail(error_text); } @@ -59,8 +52,7 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { const MediaSnapshot after = media_.Snapshot(); if (ShouldPublishPlaybackStarted(before, after, last_response_.intent)) { - const std::string detail = - !after.station.empty() ? after.station : after.track; + const std::string detail = !after.station.empty() ? after.station : after.track; backend_.SendPlayerEvent(device_id_, "playback_started", after, detail); } @@ -68,19 +60,16 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { phase_ = VoicePhase::kSpeaking; RenderState("je reponds", - last_response_.reply_text.empty() - ? "Reponse audio en preparation." - : last_response_.reply_text, + last_response_.reply_text.empty() ? "Reponse audio en preparation." + : last_response_.reply_text, true); if (!last_response_.reply_audio_url.empty()) { std::vector reply_audio; - if (backend_.DownloadReplyAudio(last_response_.reply_audio_url, - &reply_audio)) { - yield(); // WDT: feed watchdog after audio download + if (backend_.DownloadReplyAudio(last_response_.reply_audio_url, &reply_audio)) { + yield(); // WDT: feed watchdog after audio download if (!media_.PlayReplyAudio(reply_audio)) { - backend_.SendPlayerEvent(device_id_, "playback_failed", - media_.Snapshot(), + backend_.SendPlayerEvent(device_id_, "playback_failed", media_.Snapshot(), "tts reply playback failed"); } } else { @@ -96,31 +85,29 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { return true; } -bool VoiceController::Fail(const std::string& error_text) { +bool VoiceController::Fail(const std::string &error_text) { phase_ = VoicePhase::kError; last_response_.ok = false; last_response_.error = error_text; RenderState("erreur reseau", error_text, false); - backend_.SendPlayerEvent(device_id_, "voice_error", media_.Snapshot(), - error_text); + backend_.SendPlayerEvent(device_id_, "voice_error", media_.Snapshot(), error_text); media_.RestoreAfterReply(true); phase_ = VoicePhase::kIdle; RenderState("pret", IdleSummary(media_.Snapshot()), false); return false; } -void VoiceController::RenderState(const std::string& headline, - const std::string& summary, +void VoiceController::RenderState(const std::string &headline, const std::string &summary, bool show_ring) { ui_.Render(media_.Snapshot(), phase_, headline, summary, show_ring); } -std::string VoiceController::IdleSummary(const MediaSnapshot& media) { +std::string VoiceController::IdleSummary(const MediaSnapshot &media) { return FwIdleSummary(media); } -bool VoiceController::ShouldPublishPlaybackStarted( - const MediaSnapshot& before, const MediaSnapshot& after, - const VoiceIntent& intent) { +bool VoiceController::ShouldPublishPlaybackStarted(const MediaSnapshot &before, + const MediaSnapshot &after, + const VoiceIntent &intent) { return FwShouldPublishPlaybackStarted(before, after, intent); } diff --git a/firmware/src/wifi_manager.cpp b/firmware/src/wifi_manager.cpp index 96303ab..62ce308 100644 --- a/firmware/src/wifi_manager.cpp +++ b/firmware/src/wifi_manager.cpp @@ -1,23 +1,24 @@ #include "wifi_manager.h" #include -#include -#include + #include #include +#include +#include // --------------------------------------------------------------------------- // NVS namespace // --------------------------------------------------------------------------- -static constexpr const char* kNvsNamespace = "killlife"; +static constexpr const char *kNvsNamespace = "killlife"; static Preferences prefs; // --------------------------------------------------------------------------- // Captive portal // --------------------------------------------------------------------------- -static WebServer* web_server = nullptr; -static DNSServer* dns_server = nullptr; -static WifiManager* g_mgr = nullptr; // back-pointer for handlers +static WebServer *web_server = nullptr; +static DNSServer *dns_server = nullptr; +static WifiManager *g_mgr = nullptr; // back-pointer for handlers // HTML page served by the captive portal. static const char kPortalHtml[] PROGMEM = R"rawhtml( @@ -97,8 +98,7 @@ static const char kSavedHtml[] PROGMEM = R"rawhtml( )rawhtml"; // --------------------------------------------------------------------------- -void WifiManager::SetApCredentials(const std::string& ssid, - const std::string& password) { +void WifiManager::SetApCredentials(const std::string &ssid, const std::string &password) { ap_ssid_ = ssid; ap_password_ = password; } @@ -117,8 +117,8 @@ void WifiManager::Begin() { if (TryConnect(saved_ssid_, saved_pass_)) { current_ssid_ = saved_ssid_; SetState(State::kConnected, WiFi.localIP().toString().c_str()); - Serial.printf("[wifi] connected to %s — %s\n", - current_ssid_.c_str(), WiFi.localIP().toString().c_str()); + Serial.printf("[wifi] connected to %s — %s\n", current_ssid_.c_str(), + WiFi.localIP().toString().c_str()); } else { Serial.println("[wifi] connection failed → AP mode"); StartApMode(); @@ -149,12 +149,11 @@ void WifiManager::StartApMode() { WiFi.disconnect(true); delay(100); - WiFi.mode(WIFI_AP_STA); // AP + STA for scanning. + WiFi.mode(WIFI_AP_STA); // AP + STA for scanning. WiFi.softAP(ap_ssid_.c_str(), ap_password_.c_str()); delay(200); - Serial.printf("[wifi] AP started: %s / %s — http://%s\n", - ap_ssid_.c_str(), ap_password_.c_str(), + Serial.printf("[wifi] AP started: %s / %s — http://%s\n", ap_ssid_.c_str(), ap_password_.c_str(), WiFi.softAPIP().toString().c_str()); SetupApWebServer(); @@ -163,12 +162,14 @@ void WifiManager::StartApMode() { // --------------------------------------------------------------------------- std::string WifiManager::ip() const { - if (state_ != State::kConnected) return ""; + if (state_ != State::kConnected) + return ""; return WiFi.localIP().toString().c_str(); } int WifiManager::rssi() const { - if (state_ != State::kConnected) return 0; + if (state_ != State::kConnected) + return 0; return WiFi.RSSI(); } @@ -192,26 +193,23 @@ std::vector WifiManager::Scan() { WiFi.scanDelete(); // Sort by signal strength. std::sort(results.begin(), results.end(), - [](const ScanResult& a, const ScanResult& b) { - return a.rssi > b.rssi; - }); + [](const ScanResult &a, const ScanResult &b) { return a.rssi > b.rssi; }); return results; } // --------------------------------------------------------------------------- void WifiManager::LoadCredentials() { prefs.begin(kNvsNamespace, true); - saved_ssid_ = prefs.getString("wifi_ssid", "").c_str(); - saved_pass_ = prefs.getString("wifi_pass", "").c_str(); + saved_ssid_ = prefs.getString("wifi_ssid", "").c_str(); + saved_pass_ = prefs.getString("wifi_pass", "").c_str(); backend_url_ = prefs.getString("backend_url", "http://192.168.1.42:8000").c_str(); prefs.end(); - Serial.printf("[wifi] loaded: ssid='%s' backend='%s'\n", - saved_ssid_.c_str(), backend_url_.c_str()); + Serial.printf("[wifi] loaded: ssid='%s' backend='%s'\n", saved_ssid_.c_str(), + backend_url_.c_str()); } -void WifiManager::SaveCredentials(const std::string& ssid, - const std::string& pass, - const std::string& backend) { +void WifiManager::SaveCredentials(const std::string &ssid, const std::string &pass, + const std::string &backend) { prefs.begin(kNvsNamespace, false); prefs.putString("wifi_ssid", ssid.c_str()); prefs.putString("wifi_pass", pass.c_str()); @@ -220,12 +218,11 @@ void WifiManager::SaveCredentials(const std::string& ssid, saved_ssid_ = ssid; saved_pass_ = pass; backend_url_ = backend; - Serial.printf("[wifi] saved: ssid='%s' backend='%s'\n", - ssid.c_str(), backend.c_str()); + Serial.printf("[wifi] saved: ssid='%s' backend='%s'\n", ssid.c_str(), backend.c_str()); } // --------------------------------------------------------------------------- -bool WifiManager::TryConnect(const std::string& ssid, const std::string& pass, +bool WifiManager::TryConnect(const std::string &ssid, const std::string &pass, uint32_t timeout_ms) { WiFi.mode(WIFI_STA); WiFi.begin(ssid.c_str(), pass.c_str()); @@ -242,9 +239,10 @@ bool WifiManager::TryConnect(const std::string& ssid, const std::string& pass, } // --------------------------------------------------------------------------- -void WifiManager::SetState(State s, const std::string& info) { +void WifiManager::SetState(State s, const std::string &info) { state_ = s; - if (on_state_change_) on_state_change_(s, info); + if (on_state_change_) + on_state_change_(s, info); } // --------------------------------------------------------------------------- @@ -269,7 +267,8 @@ void WifiManager::SetupApWebServer() { auto nets = g_mgr->Scan(); String json = "["; for (size_t i = 0; i < nets.size(); i++) { - if (i > 0) json += ","; + if (i > 0) + json += ","; json += "{\"ssid\":\""; // Escape quotes in SSID. String escaped = nets[i].ssid.c_str(); @@ -287,8 +286,8 @@ void WifiManager::SetupApWebServer() { // Save credentials. web_server->on("/save", HTTP_POST, []() { - String ssid = web_server->arg("ssid"); - String pass = web_server->arg("pass"); + String ssid = web_server->arg("ssid"); + String pass = web_server->arg("pass"); String backend = web_server->arg("backend"); if (ssid.isEmpty()) { diff --git a/firmware/src/wifi_scanner.cpp b/firmware/src/wifi_scanner.cpp index 048ab3c..735db01 100644 --- a/firmware/src/wifi_scanner.cpp +++ b/firmware/src/wifi_scanner.cpp @@ -1,8 +1,9 @@ #include "wifi_scanner.h" -#include #include + #include +#include std::vector WifiScanner::Scan(uint32_t timeout_ms) { const uint32_t t_start = millis(); @@ -22,12 +23,13 @@ std::vector WifiScanner::Scan(uint32_t timeout_ms) { result.reserve(static_cast(n)); for (int i = 0; i < n; ++i) { const String ssid = WiFi.SSID(i); - if (ssid.isEmpty()) continue; // skip hidden networks + if (ssid.isEmpty()) + continue; // skip hidden networks WifiNetwork net; - net.ssid = ssid.c_str(); - net.rssi = WiFi.RSSI(i); - net.open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN); + net.ssid = ssid.c_str(); + net.rssi = WiFi.RSSI(i); + net.open = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN); net.channel = WiFi.channel(i); result.push_back(std::move(net)); } @@ -38,10 +40,9 @@ std::vector WifiScanner::Scan(uint32_t timeout_ms) { // Cap elapsed if scan returned very fast (sanity). last_duration_ms_ = elapsed; - last_json_ = FwWifiToJson(result, elapsed); + last_json_ = FwWifiToJson(result, elapsed); - Serial.printf("[wifi_scan] found %zu network(s) in %u ms\n", - result.size(), elapsed); + Serial.printf("[wifi_scan] found %zu network(s) in %u ms\n", result.size(), elapsed); Serial.println(last_json_.c_str()); WiFi.scanDelete(); diff --git a/firmware/src/wifi_scanner.h b/firmware/src/wifi_scanner.h index 02fbcb1..d5e39c2 100644 --- a/firmware/src/wifi_scanner.h +++ b/firmware/src/wifi_scanner.h @@ -1,46 +1,54 @@ #pragma once #ifdef UNIT_TEST -#include #include +#include using String = std::string; -static inline String escape_json_string(const String& s) { - String out; - out.reserve(s.size()); - for (char c : s) { - if (c == '"') out += "\\\""; - else if (c == '\\') out += "\\\\"; - else out += c; - } - return out; +static inline String escape_json_string(const String &s) { + String out; + out.reserve(s.size()); + for (char c : s) { + if (c == '"') + out += "\\\""; + else if (c == '\\') + out += "\\\\"; + else + out += c; + } + return out; } -static inline String itos(int n) { return std::to_string(n); } +static inline String itos(int n) { + return std::to_string(n); +} -#else // Arduino +#else // Arduino #include -static inline String escape_json_string(const String& s) { - String out; - out.reserve(s.length()); - for (size_t i = 0; i < s.length(); i++) { - char c = s[i]; - if (c == '"') out += "\\\""; - else if (c == '\\') out += "\\\\"; - else out += c; - } - return out; +static inline String escape_json_string(const String &s) { + String out; + out.reserve(s.length()); + for (size_t i = 0; i < s.length(); i++) { + char c = s[i]; + if (c == '"') + out += "\\\""; + else if (c == '\\') + out += "\\\\"; + else + out += c; + } + return out; } -static inline String itos(int n) { return String(n); } +static inline String itos(int n) { + return String(n); +} -#endif // UNIT_TEST +#endif // UNIT_TEST // Serialize a single WiFi network entry to a JSON object string. -static inline String serialize_network(const String& ssid, int rssi, int channel, int auth) { - return String("{\"ssid\":\"") + escape_json_string(ssid) + - "\",\"rssi\":" + itos(rssi) + - ",\"channel\":" + itos(channel) + - ",\"auth\":" + itos(auth) + "}"; +static inline String serialize_network(const String &ssid, int rssi, int channel, int auth) { + return String("{\"ssid\":\"") + escape_json_string(ssid) + "\",\"rssi\":" + itos(rssi) + + ",\"channel\":" + itos(channel) + ",\"auth\":" + itos(auth) + "}"; } diff --git a/firmware/test/test_basic/test_basic.cpp b/firmware/test/test_basic/test_basic.cpp index 7a23b29..2cc0f78 100644 --- a/firmware/test/test_basic/test_basic.cpp +++ b/firmware/test/test_basic/test_basic.cpp @@ -3,9 +3,10 @@ /// Covers: IdleSummary, ShouldPublishPlaybackStarted, version comparison, /// WAV header validation, backend URL validation. -#include #include #include +#include + #include "../../include/firmware_utils.h" // --------------------------------------------------------------------------- @@ -16,7 +17,7 @@ void test_idle_summary_idle_mode() { MediaSnapshot m; m.mode = MediaMode::kIdle; m.volume = 40; - m.playing = true; // playing flag ignored in Idle mode + m.playing = true; // playing flag ignored in Idle mode std::string s = FwIdleSummary(m); TEST_ASSERT_EQUAL_STRING("Idle | volume 40", s.c_str()); } @@ -166,23 +167,21 @@ void test_version_patch_downgrade() { void test_wav_valid_header() { // Minimal 12-byte RIFF/WAVE header const uint8_t data[] = { - 'R', 'I', 'F', 'F', // chunk id - 0x24, 0x00, 0x00, 0x00, // chunk size (36 bytes data) - 'W', 'A', 'V', 'E', // format + 'R', 'I', 'F', 'F', // chunk id + 0x24, 0x00, 0x00, 0x00, // chunk size (36 bytes data) + 'W', 'A', 'V', 'E', // format }; TEST_ASSERT_TRUE(FwIsValidWavHeader(data, sizeof(data))); } void test_wav_too_short() { - const uint8_t data[] = { 'R', 'I', 'F', 'F', 0x00 }; + const uint8_t data[] = {'R', 'I', 'F', 'F', 0x00}; TEST_ASSERT_FALSE(FwIsValidWavHeader(data, sizeof(data))); } void test_wav_wrong_magic() { const uint8_t data[] = { - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; TEST_ASSERT_FALSE(FwIsValidWavHeader(data, sizeof(data))); } @@ -278,13 +277,19 @@ void test_wifi_json_escapes_quotes() { } void test_wifi_sort_by_rssi() { - WifiNetwork a; a.ssid = "weak"; a.rssi = -80; - WifiNetwork b; b.ssid = "good"; b.rssi = -55; - WifiNetwork c; c.ssid = "ok"; c.rssi = -70; + WifiNetwork a; + a.ssid = "weak"; + a.rssi = -80; + WifiNetwork b; + b.ssid = "good"; + b.rssi = -55; + WifiNetwork c; + c.ssid = "ok"; + c.rssi = -70; std::vector nets = {a, b, c}; std::sort(nets.begin(), nets.end(), FwNetworkBetterSignal); TEST_ASSERT_EQUAL_STRING("good", nets[0].ssid.c_str()); - TEST_ASSERT_EQUAL_STRING("ok", nets[1].ssid.c_str()); + TEST_ASSERT_EQUAL_STRING("ok", nets[1].ssid.c_str()); TEST_ASSERT_EQUAL_STRING("weak", nets[2].ssid.c_str()); } @@ -305,8 +310,7 @@ void test_wifi_json_open_network() { void test_media_snapshot_defaults() { MediaSnapshot m; - TEST_ASSERT_EQUAL(static_cast(MediaMode::kIdle), - static_cast(m.mode)); + TEST_ASSERT_EQUAL(static_cast(MediaMode::kIdle), static_cast(m.mode)); TEST_ASSERT_FALSE(m.playing); TEST_ASSERT_EQUAL_INT(40, m.volume); TEST_ASSERT_EQUAL_INT(-1, m.battery_pct); @@ -320,7 +324,7 @@ void test_voice_intent_defaults() { // --------------------------------------------------------------------------- -int main(int, char**) { +int main(int, char **) { UNITY_BEGIN(); // IdleSummary diff --git a/firmware/test/test_modules/test_modules.cpp b/firmware/test/test_modules/test_modules.cpp index 55cfed0..ba35ec5 100644 --- a/firmware/test/test_modules/test_modules.cpp +++ b/firmware/test/test_modules/test_modules.cpp @@ -3,9 +3,9 @@ /// checking. Compiles on the native (Linux x86_64) target with -D UNIT_TEST=1. /// No Arduino/ESP32 headers are used. -#include #include #include +#include #include #include @@ -16,29 +16,23 @@ inline void yield() {} // Include the pure-C++ headers (no Arduino deps). -#include "../../include/voice_controller.h" #include "../../include/firmware_utils.h" #include "../../include/ota_manager.h" +#include "../../include/voice_controller.h" // =========================================================================== // VoiceController implementation (copied from voice_controller.cpp to avoid // pulling in Arduino.h which is not available on native). // =========================================================================== -VoiceController::VoiceController(std::string device_id, - BackendClient& backend, - MediaController& media, - UiRenderer& ui) - : device_id_(std::move(device_id)), - backend_(backend), - media_(media), - ui_(ui) {} +VoiceController::VoiceController(std::string device_id, BackendClient &backend, + MediaController &media, UiRenderer &ui) + : device_id_(std::move(device_id)), backend_(backend), media_(media), ui_(ui) {} void VoiceController::Boot() { phase_ = VoicePhase::kIdle; RenderState("pret", IdleSummary(media_.Snapshot()), false); - backend_.SendPlayerEvent(device_id_, "boot", media_.Snapshot(), - "voice-controller-ready"); + backend_.SendPlayerEvent(device_id_, "boot", media_.Snapshot(), "voice-controller-ready"); } bool VoiceController::BeginPushToTalk() { @@ -50,7 +44,7 @@ bool VoiceController::BeginPushToTalk() { return true; } -bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { +bool VoiceController::CompletePushToTalk(const std::vector &wav_data) { if (phase_ != VoicePhase::kRecording) { return false; } @@ -61,14 +55,12 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { phase_ = VoicePhase::kThinking; RenderState("je reflechis", "Envoi de la requete a Mascarade.", true); - last_response_ = backend_.SubmitVoiceSession(device_id_, media_.Snapshot(), - wav_data); + last_response_ = backend_.SubmitVoiceSession(device_id_, media_.Snapshot(), wav_data); yield(); if (!last_response_.ok) { const std::string error_text = - last_response_.error.empty() ? "session vocale en echec" - : last_response_.error; + last_response_.error.empty() ? "session vocale en echec" : last_response_.error; return Fail(error_text); } @@ -77,8 +69,7 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { const MediaSnapshot after = media_.Snapshot(); if (ShouldPublishPlaybackStarted(before, after, last_response_.intent)) { - const std::string detail = - !after.station.empty() ? after.station : after.track; + const std::string detail = !after.station.empty() ? after.station : after.track; backend_.SendPlayerEvent(device_id_, "playback_started", after, detail); } @@ -86,19 +77,16 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { phase_ = VoicePhase::kSpeaking; RenderState("je reponds", - last_response_.reply_text.empty() - ? "Reponse audio en preparation." - : last_response_.reply_text, + last_response_.reply_text.empty() ? "Reponse audio en preparation." + : last_response_.reply_text, true); if (!last_response_.reply_audio_url.empty()) { std::vector reply_audio; - if (backend_.DownloadReplyAudio(last_response_.reply_audio_url, - &reply_audio)) { + if (backend_.DownloadReplyAudio(last_response_.reply_audio_url, &reply_audio)) { yield(); if (!media_.PlayReplyAudio(reply_audio)) { - backend_.SendPlayerEvent(device_id_, "playback_failed", - media_.Snapshot(), + backend_.SendPlayerEvent(device_id_, "playback_failed", media_.Snapshot(), "tts reply playback failed"); } } else { @@ -114,32 +102,30 @@ bool VoiceController::CompletePushToTalk(const std::vector& wav_data) { return true; } -bool VoiceController::Fail(const std::string& error_text) { +bool VoiceController::Fail(const std::string &error_text) { phase_ = VoicePhase::kError; last_response_.ok = false; last_response_.error = error_text; RenderState("erreur reseau", error_text, false); - backend_.SendPlayerEvent(device_id_, "voice_error", media_.Snapshot(), - error_text); + backend_.SendPlayerEvent(device_id_, "voice_error", media_.Snapshot(), error_text); media_.RestoreAfterReply(true); phase_ = VoicePhase::kIdle; RenderState("pret", IdleSummary(media_.Snapshot()), false); return false; } -void VoiceController::RenderState(const std::string& headline, - const std::string& summary, +void VoiceController::RenderState(const std::string &headline, const std::string &summary, bool show_ring) { ui_.Render(media_.Snapshot(), phase_, headline, summary, show_ring); } -std::string VoiceController::IdleSummary(const MediaSnapshot& media) { +std::string VoiceController::IdleSummary(const MediaSnapshot &media) { return FwIdleSummary(media); } -bool VoiceController::ShouldPublishPlaybackStarted( - const MediaSnapshot& before, const MediaSnapshot& after, - const VoiceIntent& intent) { +bool VoiceController::ShouldPublishPlaybackStarted(const MediaSnapshot &before, + const MediaSnapshot &after, + const VoiceIntent &intent) { return FwShouldPublishPlaybackStarted(before, after, intent); } @@ -148,8 +134,7 @@ bool VoiceController::ShouldPublishPlaybackStarted( // We only need the constructor and simple accessors for testing. // =========================================================================== -OtaManager::OtaManager(const std::string& current_version) - : current_version_(current_version) {} +OtaManager::OtaManager(const std::string ¤t_version) : current_version_(current_version) {} // Stubs for methods that depend on Arduino/HTTP — not called in our tests // but needed to satisfy the linker (non-pure-virtual members). @@ -159,8 +144,10 @@ OtaCheckResult OtaManager::CheckOnly() { OtaCheckResult OtaManager::CheckAndUpdate() { OtaCheckResult info = FetchLatestInfo(); - if (info.status == OtaCheckResult::Status::kCheckFailed) return info; - if (info.status == OtaCheckResult::Status::kUpToDate) return info; + if (info.status == OtaCheckResult::Status::kCheckFailed) + return info; + if (info.status == OtaCheckResult::Status::kUpToDate) + return info; if (FlashFromUrl(info.url)) { info.status = OtaCheckResult::Status::kFlashOk; } else { @@ -177,7 +164,7 @@ OtaCheckResult OtaManager::FetchLatestInfo() { return result; } -bool OtaManager::FlashFromUrl(const std::string& /*url*/) { +bool OtaManager::FlashFromUrl(const std::string & /*url*/) { return false; } @@ -186,7 +173,7 @@ bool OtaManager::FlashFromUrl(const std::string& /*url*/) { // =========================================================================== class MockBackend : public BackendClient { - public: +public: VoiceSessionResponse next_voice_response; bool download_succeeds = true; std::vector download_audio; @@ -197,26 +184,23 @@ class MockBackend : public BackendClient { int submit_voice_count = 0; int download_count = 0; - bool SendPlayerEvent(const std::string& /*device_id*/, - const std::string& event_name, - const MediaSnapshot& /*media*/, - const std::string& detail) override { + bool SendPlayerEvent(const std::string & /*device_id*/, const std::string &event_name, + const MediaSnapshot & /*media*/, const std::string &detail) override { ++send_event_count; last_event_name = event_name; last_event_detail = detail; return true; } - VoiceSessionResponse SubmitVoiceSession( - const std::string& /*device_id*/, - const MediaSnapshot& /*media*/, - const std::vector& /*wav_data*/) override { + VoiceSessionResponse SubmitVoiceSession(const std::string & /*device_id*/, + const MediaSnapshot & /*media*/, + const std::vector & /*wav_data*/) override { ++submit_voice_count; return next_voice_response; } - bool DownloadReplyAudio(const std::string& /*audio_url*/, - std::vector* wav_data) override { + bool DownloadReplyAudio(const std::string & /*audio_url*/, + std::vector *wav_data) override { ++download_count; if (download_succeeds && wav_data) { *wav_data = download_audio; @@ -226,7 +210,7 @@ class MockBackend : public BackendClient { }; class MockMedia : public MediaController { - public: +public: MediaSnapshot snapshot; int apply_intent_count = 0; int prepare_count = 0; @@ -239,7 +223,7 @@ class MockMedia : public MediaController { MediaSnapshot Snapshot() const override { return snapshot; } - void ApplyIntent(const VoiceIntent& intent) override { + void ApplyIntent(const VoiceIntent &intent) override { ++apply_intent_count; last_intent = intent; } @@ -254,25 +238,22 @@ class MockMedia : public MediaController { last_restore_resume = resume_media_after_tts; } - bool PlayReplyAudio(const std::vector& /*wav_data*/) override { + bool PlayReplyAudio(const std::vector & /*wav_data*/) override { ++play_reply_count; return play_reply_result; } }; class MockUi : public UiRenderer { - public: +public: int render_count = 0; VoicePhase last_phase = VoicePhase::kIdle; std::string last_headline; std::string last_summary; bool last_show_ring = false; - void Render(const MediaSnapshot& /*media*/, - VoicePhase phase, - const std::string& headline, - const std::string& summary, - bool show_ring) override { + void Render(const MediaSnapshot & /*media*/, VoicePhase phase, const std::string &headline, + const std::string &summary, bool show_ring) override { ++render_count; last_phase = phase; last_headline = headline; @@ -297,8 +278,7 @@ void test_vc_initial_phase_is_idle() { MockMedia media; MockUi ui; VoiceController vc("dev-001", backend, media, ui); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(vc.phase())); } void test_vc_boot_sends_event_and_stays_idle() { @@ -309,8 +289,7 @@ void test_vc_boot_sends_event_and_stays_idle() { vc.Boot(); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(vc.phase())); TEST_ASSERT_EQUAL_INT(1, backend.send_event_count); TEST_ASSERT_EQUAL_STRING("boot", backend.last_event_name.c_str()); TEST_ASSERT_TRUE(ui.render_count > 0); @@ -326,8 +305,7 @@ void test_vc_begin_ptt_transitions_to_recording() { bool ok = vc.BeginPushToTalk(); TEST_ASSERT_TRUE(ok); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), static_cast(vc.phase())); } void test_vc_begin_ptt_fails_when_not_idle() { @@ -341,8 +319,7 @@ void test_vc_begin_ptt_fails_when_not_idle() { bool ok = vc.BeginPushToTalk(); TEST_ASSERT_FALSE(ok); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), static_cast(vc.phase())); } void test_vc_complete_ptt_fails_when_not_recording() { @@ -370,10 +347,8 @@ void test_vc_complete_ptt_empty_audio_fails() { bool ok = vc.CompletePushToTalk(empty); TEST_ASSERT_FALSE(ok); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(vc.phase())); - TEST_ASSERT_EQUAL_STRING("capture audio vide", - vc.last_response().error.c_str()); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(vc.phase())); + TEST_ASSERT_EQUAL_STRING("capture audio vide", vc.last_response().error.c_str()); } void test_vc_full_successful_flow_idle_to_idle() { @@ -391,14 +366,12 @@ void test_vc_full_successful_flow_idle_to_idle() { vc.Boot(); TEST_ASSERT_TRUE(vc.BeginPushToTalk()); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), static_cast(vc.phase())); bool ok = vc.CompletePushToTalk(DummyWav()); TEST_ASSERT_TRUE(ok); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(vc.phase())); TEST_ASSERT_EQUAL_INT(1, backend.submit_voice_count); TEST_ASSERT_EQUAL_INT(1, media.apply_intent_count); TEST_ASSERT_EQUAL_INT(1, media.prepare_count); @@ -424,8 +397,7 @@ void test_vc_successful_flow_with_audio_download() { bool ok = vc.CompletePushToTalk(DummyWav()); TEST_ASSERT_TRUE(ok); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(vc.phase())); TEST_ASSERT_EQUAL_INT(1, backend.download_count); TEST_ASSERT_EQUAL_INT(1, media.play_reply_count); } @@ -445,10 +417,8 @@ void test_vc_backend_error_returns_to_idle() { bool ok = vc.CompletePushToTalk(DummyWav()); TEST_ASSERT_FALSE(ok); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(vc.phase())); - TEST_ASSERT_EQUAL_STRING("server timeout", - vc.last_response().error.c_str()); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(vc.phase())); + TEST_ASSERT_EQUAL_STRING("server timeout", vc.last_response().error.c_str()); TEST_ASSERT_EQUAL_STRING("voice_error", backend.last_event_name.c_str()); } @@ -465,8 +435,7 @@ void test_vc_backend_error_empty_message_uses_default() { vc.BeginPushToTalk(); vc.CompletePushToTalk(DummyWav()); - TEST_ASSERT_EQUAL_STRING("session vocale en echec", - vc.last_response().error.c_str()); + TEST_ASSERT_EQUAL_STRING("session vocale en echec", vc.last_response().error.c_str()); } void test_vc_audio_download_failure_sends_event() { @@ -486,8 +455,7 @@ void test_vc_audio_download_failure_sends_event() { TEST_ASSERT_TRUE(ok); TEST_ASSERT_EQUAL_STRING("playback_failed", backend.last_event_name.c_str()); - TEST_ASSERT_EQUAL_STRING("tts reply download failed", - backend.last_event_detail.c_str()); + TEST_ASSERT_EQUAL_STRING("tts reply download failed", backend.last_event_detail.c_str()); } void test_vc_audio_playback_failure_sends_event() { @@ -509,8 +477,7 @@ void test_vc_audio_playback_failure_sends_event() { TEST_ASSERT_TRUE(ok); TEST_ASSERT_EQUAL_STRING("playback_failed", backend.last_event_name.c_str()); - TEST_ASSERT_EQUAL_STRING("tts reply playback failed", - backend.last_event_detail.c_str()); + TEST_ASSERT_EQUAL_STRING("tts reply playback failed", backend.last_event_detail.c_str()); } void test_vc_restore_called_with_resume_flag() { @@ -561,12 +528,10 @@ void test_vc_can_start_new_session_after_success() { vc.BeginPushToTalk(); vc.CompletePushToTalk(DummyWav()); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(vc.phase())); TEST_ASSERT_TRUE(vc.BeginPushToTalk()); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), static_cast(vc.phase())); } void test_vc_can_start_new_session_after_error() { @@ -582,8 +547,7 @@ void test_vc_can_start_new_session_after_error() { vc.BeginPushToTalk(); vc.CompletePushToTalk(DummyWav()); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(vc.phase())); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(vc.phase())); TEST_ASSERT_TRUE(vc.BeginPushToTalk()); } @@ -594,21 +558,21 @@ void test_vc_can_start_new_session_after_error() { void test_ota_version_equal_means_up_to_date() { std::string current = "1.2.3"; - std::string latest = "1.2.3"; + std::string latest = "1.2.3"; int cmp = FwCompareVersions(current, latest); TEST_ASSERT_TRUE(cmp >= 0); } void test_ota_version_newer_means_update_available() { std::string current = "1.0.0"; - std::string latest = "1.0.1"; + std::string latest = "1.0.1"; int cmp = FwCompareVersions(current, latest); TEST_ASSERT_TRUE(cmp < 0); } void test_ota_version_current_ahead_means_up_to_date() { std::string current = "2.0.0"; - std::string latest = "1.9.9"; + std::string latest = "1.9.9"; int cmp = FwCompareVersions(current, latest); TEST_ASSERT_TRUE(cmp >= 0); } @@ -732,8 +696,7 @@ void test_vc_boot_renders_idle_state() { TEST_ASSERT_EQUAL_STRING("pret", ui.last_headline.c_str()); TEST_ASSERT_FALSE(ui.last_show_ring); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), - static_cast(ui.last_phase)); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kIdle), static_cast(ui.last_phase)); } void test_vc_begin_ptt_renders_recording_state() { @@ -747,15 +710,14 @@ void test_vc_begin_ptt_renders_recording_state() { TEST_ASSERT_EQUAL_STRING("j'ecoute", ui.last_headline.c_str()); TEST_ASSERT_TRUE(ui.last_show_ring); - TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), - static_cast(ui.last_phase)); + TEST_ASSERT_EQUAL(static_cast(VoicePhase::kRecording), static_cast(ui.last_phase)); } // --------------------------------------------------------------------------- // Entry point // --------------------------------------------------------------------------- -int main(int, char**) { +int main(int, char **) { UNITY_BEGIN(); // VoiceController state transitions diff --git a/firmware/test/test_radio_state/test_radio_state.cpp b/firmware/test/test_radio_state/test_radio_state.cpp index 4f54f25..cfc0671 100644 --- a/firmware/test/test_radio_state/test_radio_state.cpp +++ b/firmware/test/test_radio_state/test_radio_state.cpp @@ -5,28 +5,30 @@ /// No real Audio/I2S/LittleFS hardware used — audio_ pointer stays null; /// every method that calls audio_ is already guarded by null / initialized_ checks. -#include +#include #include #include #include +#include #include #include -#include // --------------------------------------------------------------------------- // Minimal Arduino stubs // --------------------------------------------------------------------------- inline void yield() {} inline void delay(unsigned long) {} -inline unsigned long millis() { return 0; } -template inline T constrain(T v, T lo, T hi) { +inline unsigned long millis() { + return 0; +} +template inline T constrain(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); } struct _SerialStub { - void printf(const char*, ...) {} - void println(const char*) {} - void print(const char*) {} + void printf(const char *, ...) {} + void println(const char *) {} + void print(const char *) {} } Serial; // --------------------------------------------------------------------------- @@ -35,28 +37,31 @@ struct _SerialStub { class Audio { public: int last_volume = 0; - const char* last_fs_path = nullptr; + const char *last_fs_path = nullptr; void setPinout(int, int, int) {} void setVolume(int v) { last_volume = v; } - bool connecttohost(const char*) { return true; } + bool connecttohost(const char *) { return true; } // connecttoFS used for both SD MP3 and LittleFS TTS - template - bool connecttoFS(FS&, const char* path) { last_fs_path = path; return true; } + template bool connecttoFS(FS &, const char *path) { + last_fs_path = path; + return true; + } void stopSong() {} bool isRunning() { return false; } void loop() {} }; // Minimal SD stub so radio_player.h compiles (SD not used in native tests). -struct _SDStub {} SD; +struct _SDStub { +} SD; // --------------------------------------------------------------------------- // Pure-C++ headers (no Arduino deps) // Open private members for white-box state testing. // --------------------------------------------------------------------------- #define private public -#include "../../include/voice_controller.h" #include "../../include/radio_player.h" +#include "../../include/voice_controller.h" #undef private // --------------------------------------------------------------------------- @@ -66,7 +71,9 @@ struct _SDStub {} SD; // --------------------------------------------------------------------------- RadioPlayer::RadioPlayer() {} -RadioPlayer::~RadioPlayer() { delete audio_; } +RadioPlayer::~RadioPlayer() { + delete audio_; +} bool RadioPlayer::Begin(int bck, int ws, int dout) { audio_ = new Audio(); @@ -77,7 +84,8 @@ bool RadioPlayer::Begin(int bck, int ws, int dout) { } void RadioPlayer::Loop() { - if (initialized_ && audio_) audio_->loop(); + if (initialized_ && audio_) + audio_->loop(); } MediaSnapshot RadioPlayer::Snapshot() const { @@ -88,31 +96,37 @@ MediaSnapshot RadioPlayer::Snapshot() const { snap.wifi_ssid = wifi_ssid_; snap.wifi_rssi = wifi_rssi_; snap.battery_pct = battery_pct_; - if (!stations_.empty() && current_station_ >= 0 && - current_station_ < (int)stations_.size()) { + if (!stations_.empty() && current_station_ >= 0 && current_station_ < (int)stations_.size()) { snap.station = stations_[current_station_].first; } snap.track = current_title_; - for (const auto& s : stations_) snap.available_stations.push_back(s.first); + for (const auto &s : stations_) + snap.available_stations.push_back(s.first); return snap; } -void RadioPlayer::ApplyIntent(const VoiceIntent& intent) { +void RadioPlayer::ApplyIntent(const VoiceIntent &intent) { if (intent.type == "set_volume") { SetVolume(atoi(intent.value.c_str())); } else if (intent.type == "play") { if (!playing_) { - if (mode_ == MediaMode::kMp3) StartCurrentMp3(); - else StartCurrentStation(); + if (mode_ == MediaMode::kMp3) + StartCurrentMp3(); + else + StartCurrentStation(); } } else if (intent.type == "pause") { Stop(); } else if (intent.type == "next") { - if (mode_ == MediaMode::kMp3) NextMp3(); - else Next(); + if (mode_ == MediaMode::kMp3) + NextMp3(); + else + Next(); } else if (intent.type == "previous") { - if (mode_ == MediaMode::kMp3) PreviousMp3(); - else Previous(); + if (mode_ == MediaMode::kMp3) + PreviousMp3(); + else + Previous(); } else if (intent.type == "next_mp3") { NextMp3(); } else if (intent.type == "previous_mp3") { @@ -135,104 +149,125 @@ void RadioPlayer::PrepareForReply(PlayerAction action) { was_playing_ = playing_; saved_volume_ = volume_; if (action == PlayerAction::kDuck) { - if (audio_) audio_->setVolume((volume_ / 2) * 21 / 100); + if (audio_) + audio_->setVolume((volume_ / 2) * 21 / 100); } else if (action == PlayerAction::kStopResume) { - if (playing_ && audio_) { audio_->stopSong(); playing_ = false; } + if (playing_ && audio_) { + audio_->stopSong(); + playing_ = false; + } } } void RadioPlayer::RestoreAfterReply(bool resume) { if (saved_volume_ >= 0) { - if (audio_) audio_->setVolume(saved_volume_ * 21 / 100); + if (audio_) + audio_->setVolume(saved_volume_ * 21 / 100); saved_volume_ = -1; } - if (resume && was_playing_ && !playing_) StartCurrentStation(); + if (resume && was_playing_ && !playing_) + StartCurrentStation(); was_playing_ = false; } -bool RadioPlayer::PlayReplyAudio(const std::vector&) { - return false; // stub — not tested +bool RadioPlayer::PlayReplyAudio(const std::vector &) { + return false; // stub — not tested } -void RadioPlayer::SetStations( - const std::vector>& list) { +void RadioPlayer::SetStations(const std::vector> &list) { stations_ = list; - if (current_station_ >= (int)stations_.size()) current_station_ = 0; + if (current_station_ >= (int)stations_.size()) + current_station_ = 0; } void RadioPlayer::PlayStation(int index) { - if (stations_.empty()) return; + if (stations_.empty()) + return; current_station_ = index % (int)stations_.size(); StartCurrentStation(); } -void RadioPlayer::PlayStation(const std::string& name) { +void RadioPlayer::PlayStation(const std::string &name) { for (int i = 0; i < (int)stations_.size(); i++) { - if (stations_[i].first == name) { PlayStation(i); return; } + if (stations_[i].first == name) { + PlayStation(i); + return; + } } } void RadioPlayer::Next() { - if (stations_.empty()) return; + if (stations_.empty()) + return; current_station_ = (current_station_ + 1) % (int)stations_.size(); StartCurrentStation(); } void RadioPlayer::Previous() { - if (stations_.empty()) return; - current_station_ = (current_station_ - 1 + (int)stations_.size()) % - (int)stations_.size(); + if (stations_.empty()) + return; + current_station_ = (current_station_ - 1 + (int)stations_.size()) % (int)stations_.size(); StartCurrentStation(); } void RadioPlayer::Stop() { - if (audio_) audio_->stopSong(); + if (audio_) + audio_->stopSong(); playing_ = false; } void RadioPlayer::SetVolume(int vol) { volume_ = constrain(vol, 0, 100); - if (audio_) audio_->setVolume(volume_ * 21 / 100); + if (audio_) + audio_->setVolume(volume_ * 21 / 100); } -bool RadioPlayer::IsPlaying() const { return playing_; } +bool RadioPlayer::IsPlaying() const { + return playing_; +} -void RadioPlayer::SetMp3Files(const std::vector& files) { +void RadioPlayer::SetMp3Files(const std::vector &files) { mp3_files_ = files; - if (current_mp3_ >= (int)mp3_files_.size()) current_mp3_ = 0; + if (current_mp3_ >= (int)mp3_files_.size()) + current_mp3_ = 0; } void RadioPlayer::NextMp3() { - if (mp3_files_.empty()) return; + if (mp3_files_.empty()) + return; current_mp3_ = (current_mp3_ + 1) % (int)mp3_files_.size(); StartCurrentMp3(); } void RadioPlayer::PreviousMp3() { - if (mp3_files_.empty()) return; - current_mp3_ = (current_mp3_ - 1 + (int)mp3_files_.size()) % - (int)mp3_files_.size(); + if (mp3_files_.empty()) + return; + current_mp3_ = (current_mp3_ - 1 + (int)mp3_files_.size()) % (int)mp3_files_.size(); StartCurrentMp3(); } void RadioPlayer::StartCurrentMp3() { - if (!initialized_ || !audio_ || mp3_files_.empty()) return; - const std::string& path = mp3_files_[current_mp3_]; + if (!initialized_ || !audio_ || mp3_files_.empty()) + return; + const std::string &path = mp3_files_[current_mp3_]; audio_->stopSong(); playing_ = audio_->connecttoFS(SD, path.c_str()); } void RadioPlayer::StartCurrentStation() { - if (!initialized_ || !audio_ || stations_.empty()) return; - const auto& [name, url] = stations_[current_station_]; + if (!initialized_ || !audio_ || stations_.empty()) + return; + const auto &[name, url] = stations_[current_station_]; audio_->stopSong(); playing_ = audio_->connecttohost(url.c_str()); } -void RadioPlayer::OnInfo(const char*) {} -void RadioPlayer::OnTitle(const char* title) { current_title_ = title ? title : ""; } +void RadioPlayer::OnInfo(const char *) {} +void RadioPlayer::OnTitle(const char *title) { + current_title_ = title ? title : ""; +} -void updatePlayerWifi(RadioPlayer& p, const std::string& ssid, int rssi) { +void updatePlayerWifi(RadioPlayer &p, const std::string &ssid, int rssi) { p.wifi_ssid_ = ssid; p.wifi_rssi_ = rssi; } @@ -241,8 +276,9 @@ void updatePlayerWifi(RadioPlayer& p, const std::string& ssid, int rssi) { // Helpers // =========================================================================== -static std::vector> three_stations() { - return {{"FIP", "http://fip.fr/stream"}, {"NovaPlanet", "http://nova.fr/stream"}, +static std::vector> three_stations() { + return {{"FIP", "http://fip.fr/stream"}, + {"NovaPlanet", "http://nova.fr/stream"}, {"France Inter", "http://inter.fr/stream"}}; } @@ -260,7 +296,7 @@ void test_snapshot_defaults() { auto s = p.Snapshot(); TEST_ASSERT_EQUAL(MediaMode::kRadio, s.mode); TEST_ASSERT_FALSE(s.playing); - TEST_ASSERT_EQUAL_INT(40, s.volume); // default volume + TEST_ASSERT_EQUAL_INT(40, s.volume); // default volume TEST_ASSERT_TRUE(s.station.empty()); TEST_ASSERT_EQUAL_INT(-1, s.battery_pct); } @@ -301,7 +337,7 @@ void test_playstation_int_sets_index() { void test_playstation_int_wraps() { RadioPlayer p; p.SetStations(three_stations()); - p.PlayStation(5); // 5 % 3 == 2 + p.PlayStation(5); // 5 % 3 == 2 TEST_ASSERT_EQUAL_STRING("France Inter", p.Snapshot().station.c_str()); } @@ -322,15 +358,15 @@ void test_playstation_unknown_name_no_change() { void test_next_advances() { RadioPlayer p; p.SetStations(three_stations()); - p.Next(); // 0 → 1 + p.Next(); // 0 → 1 TEST_ASSERT_EQUAL_STRING("NovaPlanet", p.Snapshot().station.c_str()); } void test_next_wraps_at_end() { RadioPlayer p; p.SetStations(three_stations()); - p.PlayStation(2); // France Inter (last) - p.Next(); // wraps → 0 (FIP) + p.PlayStation(2); // France Inter (last) + p.Next(); // wraps → 0 (FIP) TEST_ASSERT_EQUAL_STRING("FIP", p.Snapshot().station.c_str()); } @@ -375,7 +411,7 @@ void test_intent_set_volume() { void test_intent_pause_clears_playing() { RadioPlayer p; - p.playing_ = true; // simulate playing state + p.playing_ = true; // simulate playing state VoiceIntent intent; intent.type = "pause"; p.ApplyIntent(intent); @@ -431,8 +467,8 @@ void test_prepare_saves_state() { // saved_volume_ = 80, was_playing_ = true // After restore with resume=false, saved_volume_ reset p.RestoreAfterReply(false); - TEST_ASSERT_EQUAL_INT(80, p.Snapshot().volume); // volume restored - TEST_ASSERT_TRUE(p.IsPlaying()); // kDuck never stops playback + TEST_ASSERT_EQUAL_INT(80, p.Snapshot().volume); // volume restored + TEST_ASSERT_TRUE(p.IsPlaying()); // kDuck never stops playback } void test_prepare_stop_resume_action() { @@ -441,11 +477,11 @@ void test_prepare_stop_resume_action() { // (guard: if (playing_ && audio_) — audio is null, so no-op) p.playing_ = true; p.PrepareForReply(PlayerAction::kStopResume); - TEST_ASSERT_TRUE(p.IsPlaying()); // audio_ is null → no change + TEST_ASSERT_TRUE(p.IsPlaying()); // audio_ is null → no change // resume=true, was_playing_=true, but !playing_=false → StartCurrentStation not called. // playing_ never stopped (no audio_) → remains true. p.RestoreAfterReply(true); - TEST_ASSERT_TRUE(p.IsPlaying()); // never stopped; was_playing_ reset internally + TEST_ASSERT_TRUE(p.IsPlaying()); // never stopped; was_playing_ reset internally } void test_restore_resets_saved_volume() { @@ -512,14 +548,16 @@ void test_nextmp3_advances() { void test_nextmp3_wraps() { RadioPlayer p; p.SetMp3Files(three_mp3s()); - p.NextMp3(); p.NextMp3(); p.NextMp3(); // 0→1→2→0 + p.NextMp3(); + p.NextMp3(); + p.NextMp3(); // 0→1→2→0 TEST_ASSERT_EQUAL_INT(0, p.CurrentMp3Index()); } void test_previousmp3_wraps_at_start() { RadioPlayer p; p.SetMp3Files(three_mp3s()); - p.PreviousMp3(); // (0-1+3)%3 == 2 + p.PreviousMp3(); // (0-1+3)%3 == 2 TEST_ASSERT_EQUAL_INT(2, p.CurrentMp3Index()); } diff --git a/firmware/test/test_wifi_state/test_wifi_state.cpp b/firmware/test/test_wifi_state/test_wifi_state.cpp index 75c00a1..e4ece0e 100644 --- a/firmware/test/test_wifi_state/test_wifi_state.cpp +++ b/firmware/test/test_wifi_state/test_wifi_state.cpp @@ -5,10 +5,10 @@ /// No WiFi/Arduino/Preferences hardware used — only the inline header methods /// and the two pure state methods inlined below. -#include -#include -#include #include +#include +#include +#include // --------------------------------------------------------------------------- // Include the pure-C++ header (no Arduino deps) @@ -25,15 +25,15 @@ // apPassword(), backendUrl() are already inline in the header.) // --------------------------------------------------------------------------- -void WifiManager::SetApCredentials(const std::string& ssid, - const std::string& password) { +void WifiManager::SetApCredentials(const std::string &ssid, const std::string &password) { ap_ssid_ = ssid; ap_password_ = password; } -void WifiManager::SetState(State s, const std::string& info) { +void WifiManager::SetState(State s, const std::string &info) { state_ = s; - if (on_state_change_) on_state_change_(s, info); + if (on_state_change_) + on_state_change_(s, info); } // Hardware-dependent methods — not called in these tests; provide empty stubs @@ -41,15 +41,23 @@ void WifiManager::SetState(State s, const std::string& info) { void WifiManager::Begin() {} void WifiManager::Loop() {} void WifiManager::StartApMode() {} -std::string WifiManager::ip() const { return ""; } -int WifiManager::rssi() const { return 0; } -std::string WifiManager::apIp() const { return ""; } -std::vector WifiManager::Scan() { return {}; } +std::string WifiManager::ip() const { + return ""; +} +int WifiManager::rssi() const { + return 0; +} +std::string WifiManager::apIp() const { + return ""; +} +std::vector WifiManager::Scan() { + return {}; +} void WifiManager::LoadCredentials() {} -void WifiManager::SaveCredentials(const std::string&, const std::string&, - const std::string&) {} -bool WifiManager::TryConnect(const std::string&, const std::string&, - uint32_t) { return false; } +void WifiManager::SaveCredentials(const std::string &, const std::string &, const std::string &) {} +bool WifiManager::TryConnect(const std::string &, const std::string &, uint32_t) { + return false; +} void WifiManager::SetupApWebServer() {} void WifiManager::StopApWebServer() {} @@ -119,7 +127,7 @@ void test_callback_fires_on_state_change() { WifiManager::State last_state = WifiManager::State::kIdle; std::string last_info; - mgr.SetOnStateChange([&](WifiManager::State s, const std::string& info) { + mgr.SetOnStateChange([&](WifiManager::State s, const std::string &info) { ++call_count; last_state = s; last_info = info; @@ -164,7 +172,7 @@ void test_callback_not_called_without_registration() { void test_multiple_state_transitions() { WifiManager mgr; int count = 0; - mgr.SetOnStateChange([&](WifiManager::State, const std::string&) { ++count; }); + mgr.SetOnStateChange([&](WifiManager::State, const std::string &) { ++count; }); mgr.SetState(WifiManager::State::kConnecting, ""); mgr.SetState(WifiManager::State::kFailed, "");