250 lines
7.6 KiB
C++
250 lines
7.6 KiB
C++
#include "MultiMatrixService.h"
|
|
#include "spore/core/ApiServer.h"
|
|
#include "spore/util/Logging.h"
|
|
#include <ArduinoJson.h>
|
|
#include <SoftwareSerial.h>
|
|
|
|
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<SoftwareSerial>(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");
|
|
}
|
|
registerTasks();
|
|
}
|
|
|
|
void MultiMatrixService::registerEndpoints(ApiServer& api) {
|
|
api.addEndpoint(API_STATUS_ENDPOINT, HTTP_GET,
|
|
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
|
std::vector<ParamSpec>{});
|
|
|
|
api.addEndpoint(API_CONTROL_ENDPOINT, HTTP_POST,
|
|
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
|
|
std::vector<ParamSpec>{
|
|
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<uint8_t>(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() {
|
|
m_taskManager.registerTask("multimatrix_potentiometer", POTENTIOMETER_SAMPLE_INTERVAL_MS,
|
|
[this]() { pollPotentiometer(); });
|
|
}
|
|
|
|
void MultiMatrixService::pollPotentiometer() {
|
|
if (!m_playerReady) {
|
|
return;
|
|
}
|
|
|
|
const uint16_t rawValue = analogRead(static_cast<uint8_t>(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<uint16_t>(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<uint8_t>((static_cast<uint32_t>(invertedValue) * MAX_VOLUME) / 1023U);
|
|
return std::min<uint8_t>(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<int>(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<uint8_t>(std::max(0, std::min(static_cast<int>(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<int>(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<int>(m_volume);
|
|
doc["loop"] = m_loopEnabled;
|
|
String payload;
|
|
serializeJson(doc, payload);
|
|
m_ctx.fire(EVENT_TOPIC, &payload);
|
|
}
|