#include "MultiMatrixService.h" #include "spore/core/ApiServer.h" #include "spore/util/Logging.h" #include #include namespace { constexpr uint8_t DEFAULT_VOLUME = 15; constexpr char API_STATUS_ENDPOINT[] = "/api/audio/status"; constexpr char API_CONTROL_ENDPOINT[] = "/api/audio"; constexpr char EVENT_TOPIC[] = "audio/player"; } MultiMatrixService::MultiMatrixService(NodeContext& ctx, TaskManager& taskManager, uint8_t rxPin, uint8_t txPin, uint8_t potentiometerPin) : m_ctx(ctx), m_taskManager(taskManager), m_serial(std::make_unique(rxPin, txPin)), m_potentiometerPin(potentiometerPin), m_volume(DEFAULT_VOLUME), m_playerReady(false), m_loopEnabled(false) { pinMode(m_potentiometerPin, INPUT); m_serial->begin(9600); // DFPlayer Mini requires time to initialize after power-on delay(1000); if (m_player.begin(*m_serial)) { m_playerReady = true; m_player.setTimeOut(500); m_player.EQ(DFPLAYER_EQ_NORMAL); m_player.volume(m_volume); LOG_INFO("MultiMatrixService", "DFPlayer initialized successfully"); publishEvent("ready"); } else { LOG_ERROR("MultiMatrixService", "Failed to initialize DFPlayer"); } } void MultiMatrixService::registerEndpoints(ApiServer& api) { api.registerEndpoint(API_STATUS_ENDPOINT, HTTP_GET, [this](AsyncWebServerRequest* request) { handleStatusRequest(request); }, std::vector{}); api.registerEndpoint(API_CONTROL_ENDPOINT, HTTP_POST, [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, std::vector{ ParamSpec{String("action"), true, String("body"), String("string"), {String("play"), String("stop"), String("pause"), String("resume"), String("next"), String("previous"), String("volume"), String("loop")}}, ParamSpec{String("volume"), false, String("body"), String("numberRange"), {}, String("15")}, ParamSpec{String("loop"), false, String("body"), String("boolean"), {}} }); } bool MultiMatrixService::isReady() const { return m_playerReady; } uint8_t MultiMatrixService::getVolume() const { return m_volume; } bool MultiMatrixService::isLoopEnabled() const { return m_loopEnabled; } void MultiMatrixService::play() { if (!m_playerReady) { return; } m_player.play(); publishEvent("play"); LOG_INFO("MultiMatrixService", "Playback started"); } void MultiMatrixService::stop() { if (!m_playerReady) { return; } m_player.stop(); publishEvent("stop"); LOG_INFO("MultiMatrixService", "Playback stopped"); } void MultiMatrixService::pause() { if (!m_playerReady) { return; } m_player.pause(); publishEvent("pause"); LOG_INFO("MultiMatrixService", "Playback paused"); } void MultiMatrixService::resume() { if (!m_playerReady) { return; } m_player.start(); publishEvent("resume"); LOG_INFO("MultiMatrixService", "Playback resumed"); } void MultiMatrixService::next() { if (!m_playerReady) { return; } m_player.next(); publishEvent("next"); LOG_INFO("MultiMatrixService", "Next track"); } void MultiMatrixService::previous() { if (!m_playerReady) { return; } m_player.previous(); publishEvent("previous"); LOG_INFO("MultiMatrixService", "Previous track"); } void MultiMatrixService::setVolume(uint8_t volume) { if (!m_playerReady) { return; } const uint8_t clampedVolume = std::min(volume, MAX_VOLUME); if (clampedVolume == m_volume) { return; } applyVolume(clampedVolume); } void MultiMatrixService::setLoop(bool enabled) { if (!m_playerReady) { return; } m_loopEnabled = enabled; if (enabled) { m_player.enableLoop(); } else { m_player.disableLoop(); } publishEvent("loop"); LOG_INFO("MultiMatrixService", String("Loop ") + (enabled ? "enabled" : "disabled")); } void MultiMatrixService::registerTasks(TaskManager& taskManager) { taskManager.registerTask("multimatrix_potentiometer", POTENTIOMETER_SAMPLE_INTERVAL_MS, [this]() { pollPotentiometer(); }); } void MultiMatrixService::pollPotentiometer() { if (!m_playerReady) { return; } const uint16_t rawValue = analogRead(static_cast(m_potentiometerPin)); const uint8_t targetVolume = calculateVolumeFromPotentiometer(rawValue); if (targetVolume > m_volume + POT_VOLUME_EPSILON || targetVolume + POT_VOLUME_EPSILON < m_volume) { applyVolume(targetVolume); } } uint8_t MultiMatrixService::calculateVolumeFromPotentiometer(uint16_t rawValue) const { // Clamp raw value to prevent underflow (analogRead can return 1024) const uint16_t clampedValue = std::min(rawValue, 1023U); // Invert: all down (0) = max volume, all up (1023) = min volume const uint16_t invertedValue = 1023U - clampedValue; const uint8_t scaledVolume = static_cast((static_cast(invertedValue) * MAX_VOLUME) / 1023U); return std::min(scaledVolume, MAX_VOLUME); } void MultiMatrixService::applyVolume(uint8_t targetVolume) { m_volume = targetVolume; m_player.volume(m_volume); publishEvent("volume"); LOG_INFO("MultiMatrixService", String("Volume set to ") + String(m_volume)); } void MultiMatrixService::handleStatusRequest(AsyncWebServerRequest* request) { StaticJsonDocument<192> doc; doc["ready"] = m_playerReady; doc["volume"] = static_cast(m_volume); doc["loop"] = m_loopEnabled; String json; serializeJson(doc, json); request->send(200, "application/json", json); } void MultiMatrixService::handleControlRequest(AsyncWebServerRequest* request) { String action = request->hasParam("action", true) ? request->getParam("action", true)->value() : ""; bool ok = true; if (action.equalsIgnoreCase("play")) { play(); } else if (action.equalsIgnoreCase("stop")) { stop(); } else if (action.equalsIgnoreCase("pause")) { pause(); } else if (action.equalsIgnoreCase("resume")) { resume(); } else if (action.equalsIgnoreCase("next")) { next(); } else if (action.equalsIgnoreCase("previous")) { previous(); } else if (action.equalsIgnoreCase("volume")) { if (request->hasParam("volume", true)) { int volumeValue = request->getParam("volume", true)->value().toInt(); setVolume(static_cast(std::max(0, std::min(static_cast(MAX_VOLUME), volumeValue)))); } else { ok = false; } } else if (action.equalsIgnoreCase("loop")) { if (request->hasParam("loop", true)) { String loopValue = request->getParam("loop", true)->value(); bool enabled = loopValue.equalsIgnoreCase("true") || loopValue == "1"; setLoop(enabled); } else { ok = false; } } else { ok = false; } StaticJsonDocument<256> resp; resp["success"] = ok; resp["ready"] = m_playerReady; resp["volume"] = static_cast(m_volume); resp["loop"] = m_loopEnabled; if (!ok) { resp["message"] = "Invalid action"; } String json; serializeJson(resp, json); request->send(ok ? 200 : 400, "application/json", json); } void MultiMatrixService::publishEvent(const char* action) { StaticJsonDocument<192> doc; doc["action"] = action; doc["volume"] = static_cast(m_volume); doc["loop"] = m_loopEnabled; String payload; serializeJson(doc, payload); m_ctx.fire(EVENT_TOPIC, &payload); }