mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
295 lines
9.8 KiB
C++
295 lines
9.8 KiB
C++
#include "sendspin_hub.h"
|
|
|
|
#ifdef USE_ESP32
|
|
|
|
#include "esphome/components/network/util.h"
|
|
#ifdef USE_ETHERNET
|
|
#include "esphome/components/ethernet/ethernet_component.h"
|
|
#endif
|
|
#ifdef USE_WIFI
|
|
#include "esphome/components/wifi/wifi_component.h"
|
|
#endif
|
|
|
|
#include "esphome/core/application.h"
|
|
#include "esphome/core/helpers.h"
|
|
#include "esphome/core/log.h"
|
|
#include "esphome/core/version.h"
|
|
|
|
#include <esp_log.h>
|
|
|
|
namespace esphome::sendspin_ {
|
|
|
|
static const char *const TAG = "sendspin.hub";
|
|
|
|
#ifdef USE_SENDSPIN_ARTWORK
|
|
// Indexed by the library enums, which start at zero and are contiguous.
|
|
static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"};
|
|
static const char *const IMAGE_FORMAT_NAMES[] = {"JPEG", "PNG", "BMP"};
|
|
#endif
|
|
|
|
void SendspinHub::setup() {
|
|
auto config = this->build_client_config_();
|
|
this->client_ = std::make_unique<sendspin::SendspinClient>(std::move(config));
|
|
|
|
// Set up persistence (preferences must be initialized before providers are added to the client)
|
|
this->last_played_server_pref_ =
|
|
global_preferences->make_preference<LastPlayedServerPref>(fnv1a_hash("sendspin_last_played"));
|
|
#ifdef USE_SENDSPIN_PLAYER
|
|
this->static_delay_pref_ = global_preferences->make_preference<StaticDelayPref>(fnv1a_hash("sendspin_static_delay"));
|
|
#endif
|
|
|
|
// Wire providers and client listener
|
|
this->client_->set_listener(this);
|
|
this->client_->set_network_provider(this);
|
|
this->client_->set_persistence_provider(this);
|
|
|
|
#ifdef USE_SENDSPIN_ARTWORK
|
|
this->artwork_role_ = &this->client_->add_artwork(this->artwork_config_);
|
|
this->artwork_role_->set_listener(this);
|
|
#endif
|
|
|
|
#ifdef USE_SENDSPIN_CONTROLLER
|
|
this->controller_role_ = &this->client_->add_controller();
|
|
this->controller_role_->set_listener(this);
|
|
#endif
|
|
|
|
#ifdef USE_SENDSPIN_METADATA
|
|
this->metadata_role_ = &this->client_->add_metadata();
|
|
this->metadata_role_->set_listener(this);
|
|
#endif
|
|
|
|
#ifdef USE_SENDSPIN_PLAYER
|
|
this->client_->add_player(this->player_config_).set_listener(this->player_listener_);
|
|
#endif
|
|
|
|
if (!this->client_->start_server()) {
|
|
ESP_LOGE(TAG, "Failed to start Sendspin server");
|
|
this->mark_failed();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void SendspinHub::loop() { this->client_->loop(); }
|
|
|
|
void SendspinHub::dump_config() {
|
|
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
|
ESP_LOGCONFIG(TAG,
|
|
"Sendspin Hub:\n"
|
|
" Client ID: %s\n"
|
|
" Task stack in PSRAM: %s",
|
|
get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_));
|
|
|
|
#ifdef USE_SENDSPIN_ARTWORK
|
|
// Slot indices come from the order the image platform entries were declared, so the log is the
|
|
// only place the mapping from a slot to the artwork it asked for can be read back.
|
|
uint8_t slot = 0;
|
|
for (const auto &preference : this->artwork_config_.preferred_formats) {
|
|
ESP_LOGCONFIG(TAG, " Artwork slot %u: %s as %s, %ux%u, display offset %" PRId32 " ms", slot++,
|
|
IMAGE_SOURCE_NAMES[static_cast<uint8_t>(preference.source)],
|
|
IMAGE_FORMAT_NAMES[static_cast<uint8_t>(preference.format)], preference.width, preference.height,
|
|
preference.display_offset_ms);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// --- Delegating methods ---
|
|
|
|
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
|
void SendspinHub::connect_to_server(const std::string &url) {
|
|
if (this->is_ready()) {
|
|
this->client_->connect_to(url);
|
|
}
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
|
void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason) {
|
|
if (this->is_ready()) {
|
|
this->client_->disconnect(reason);
|
|
}
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
|
void SendspinHub::update_state(sendspin::SendspinClientState state) {
|
|
if (this->is_ready()) {
|
|
this->client_->update_state(state);
|
|
}
|
|
}
|
|
|
|
const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
|
// The server matches client_id against the L2 source MAC of the device's multicast traffic.
|
|
// ESP-IDF derives the ethernet MAC as base+3 by default on ESP32-S3, so we cannot use the
|
|
// eFuse base MAC when ethernet is the active interface.
|
|
#ifdef USE_ETHERNET
|
|
if (ethernet::global_eth_component != nullptr) {
|
|
return ethernet::global_eth_component->get_eth_mac_address_pretty_into_buffer(buf);
|
|
}
|
|
#endif
|
|
return get_mac_address_pretty_into_buffer(buf);
|
|
}
|
|
|
|
sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
|
|
sendspin::SendspinClientConfig config;
|
|
|
|
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
|
config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
|
|
config.name = App.get_friendly_name();
|
|
config.product_name = App.get_name();
|
|
config.manufacturer = "ESPHome";
|
|
config.software_version = ESPHOME_VERSION;
|
|
config.httpd_psram_stack = this->task_stack_in_psram_;
|
|
|
|
return config;
|
|
}
|
|
|
|
// --- SendspinClientListener overrides ---
|
|
// THREAD CONTEXT: Main loop (fired from client_->loop())
|
|
|
|
void SendspinHub::on_group_update(const sendspin::GroupUpdateObject &group) {
|
|
this->group_update_callbacks_.call(group);
|
|
}
|
|
|
|
void SendspinHub::on_request_high_performance() {
|
|
#ifdef USE_WIFI
|
|
if (wifi::global_wifi_component != nullptr) {
|
|
wifi::global_wifi_component->request_high_performance();
|
|
wifi::global_wifi_component->request_roaming_suppression();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void SendspinHub::on_release_high_performance() {
|
|
#ifdef USE_WIFI
|
|
if (wifi::global_wifi_component != nullptr) {
|
|
wifi::global_wifi_component->release_high_performance();
|
|
wifi::global_wifi_component->release_roaming_suppression();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// --- SendspinNetworkProvider override ---
|
|
|
|
// THREAD CONTEXT: Main loop (polled by client_->loop())
|
|
bool SendspinHub::is_network_ready() { return network::is_connected(); }
|
|
|
|
// --- SendspinPersistenceProvider overrides ---
|
|
|
|
// THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events)
|
|
bool SendspinHub::save_last_server_hash(uint32_t hash) {
|
|
LastPlayedServerPref pref{.server_id_hash = hash};
|
|
bool ok = this->last_played_server_pref_.save(&pref);
|
|
if (ok) {
|
|
ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash);
|
|
} else {
|
|
ESP_LOGW(TAG, "Failed to persist last played server hash");
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events)
|
|
std::optional<uint32_t> SendspinHub::load_last_server_hash() {
|
|
LastPlayedServerPref pref{};
|
|
if (this->last_played_server_pref_.load(&pref)) {
|
|
ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash);
|
|
return pref.server_id_hash;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// --- Sendspin role specific methods/overrides ---
|
|
|
|
#ifdef USE_SENDSPIN_ARTWORK
|
|
// THREAD CONTEXT: Dedicated artwork decode thread; downstream callbacks run here too
|
|
void SendspinHub::on_image_decode(uint8_t slot, const uint8_t *data, size_t length,
|
|
sendspin::SendspinImageFormat format) {
|
|
this->artwork_image_decode_callbacks_.call(slot, data, length, format);
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (fired from client_->loop() once the slot's offset-shifted display
|
|
// deadline is reached; lateness_ms reports how far past the deadline the display slipped)
|
|
void SendspinHub::on_image_display(uint8_t slot, uint32_t lateness_ms) {
|
|
this->artwork_image_display_callbacks_.call(slot, lateness_ms);
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (fired from client_->loop())
|
|
void SendspinHub::on_image_clear(uint8_t slot) { this->artwork_image_clear_callbacks_.call(slot); }
|
|
|
|
// THREAD CONTEXT: Main loop (invoked from SendspinImageSlot once a delivery is fully presented)
|
|
void SendspinHub::artwork_frame_done(uint8_t slot) {
|
|
if (this->artwork_role_ != nullptr) {
|
|
this->artwork_role_->frame_done(slot);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_SENDSPIN_CONTROLLER
|
|
// THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components)
|
|
void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume,
|
|
std::optional<bool> mute) {
|
|
if (this->is_ready()) {
|
|
sendspin::ClientCommandControllerObject obj = {
|
|
.command = command,
|
|
.volume = volume,
|
|
.muted = mute,
|
|
};
|
|
this->controller_role_->send_command(obj);
|
|
}
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop())
|
|
void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) {
|
|
this->controller_state_callbacks_.call(state);
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_SENDSPIN_METADATA
|
|
// THREAD CONTEXT: Main loop (MetadataRoleListener override, fired from client_->loop())
|
|
void SendspinHub::on_metadata(const sendspin::ServerMetadataStateObject &metadata) {
|
|
this->metadata_update_callbacks_.call(metadata);
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
|
uint32_t SendspinHub::get_track_progress_ms() const {
|
|
if (this->is_ready()) {
|
|
return this->metadata_role_->get_track_progress_ms();
|
|
}
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_SENDSPIN_PLAYER
|
|
// THREAD CONTEXT: Main loop, called from child component setup() after player role is created and configured
|
|
sendspin::PlayerRole *SendspinHub::get_player_role() {
|
|
if (this->is_ready()) {
|
|
return this->client_->player();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (SendspinPersistenceProvider override)
|
|
bool SendspinHub::save_static_delay(uint16_t delay_ms) {
|
|
StaticDelayPref pref{.delay_ms = delay_ms};
|
|
bool ok = this->static_delay_pref_.save(&pref);
|
|
if (ok) {
|
|
ESP_LOGD(TAG, "Persisted static delay: %u ms", delay_ms);
|
|
} else {
|
|
ESP_LOGW(TAG, "Failed to persist static delay");
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
// THREAD CONTEXT: Main loop (SendspinPersistenceProvider override)
|
|
std::optional<uint16_t> SendspinHub::load_static_delay() {
|
|
StaticDelayPref pref{};
|
|
if (this->static_delay_pref_.load(&pref)) {
|
|
ESP_LOGI(TAG, "Loaded static delay: %u ms", pref.delay_ms);
|
|
return pref.delay_ms;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
#endif
|
|
|
|
} // namespace esphome::sendspin_
|
|
|
|
#endif // USE_ESP32
|