feat: multimatrix example
This commit is contained in:
228
examples/multimatrix/MultiMatrixService.cpp
Normal file
228
examples/multimatrix/MultiMatrixService.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
#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) {
|
||||
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")}},
|
||||
ParamSpec{String("volume"), false, String("body"), String("numberRange"), {}, String("15")}
|
||||
});
|
||||
}
|
||||
|
||||
bool MultiMatrixService::isReady() const {
|
||||
return m_playerReady;
|
||||
}
|
||||
|
||||
uint8_t MultiMatrixService::getVolume() const {
|
||||
return m_volume;
|
||||
}
|
||||
|
||||
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::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);
|
||||
|
||||
// Debug: fire event on significant change
|
||||
if (targetVolume > m_volume + POT_VOLUME_EPSILON || targetVolume + POT_VOLUME_EPSILON < m_volume) {
|
||||
StaticJsonDocument<128> debugDoc;
|
||||
debugDoc["action"] = "pot_debug";
|
||||
debugDoc["raw"] = rawValue;
|
||||
debugDoc["target"] = targetVolume;
|
||||
debugDoc["current"] = m_volume;
|
||||
String debugPayload;
|
||||
serializeJson(debugDoc, debugPayload);
|
||||
m_ctx.fire(EVENT_TOPIC, &debugPayload);
|
||||
|
||||
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<128> doc;
|
||||
doc["ready"] = m_playerReady;
|
||||
doc["volume"] = static_cast<int>(m_volume);
|
||||
|
||||
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 {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
StaticJsonDocument<192> resp;
|
||||
resp["success"] = ok;
|
||||
resp["ready"] = m_playerReady;
|
||||
resp["volume"] = static_cast<int>(m_volume);
|
||||
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<128> doc;
|
||||
doc["action"] = action;
|
||||
doc["volume"] = static_cast<int>(m_volume);
|
||||
String payload;
|
||||
serializeJson(doc, payload);
|
||||
m_ctx.fire(EVENT_TOPIC, &payload);
|
||||
}
|
||||
Reference in New Issue
Block a user