ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
sendspin_hub.cpp
Go to the documentation of this file.
1#include "sendspin_hub.h"
2
3#ifdef USE_ESP32
4
6#ifdef USE_ETHERNET
8#endif
9#ifdef USE_WIFI
11#endif
12
15#include "esphome/core/log.h"
17
18#include <esp_log.h>
19
20namespace esphome::sendspin_ {
21
22static const char *const TAG = "sendspin.hub";
23
24#ifdef USE_SENDSPIN_ARTWORK
25// Indexed by the library enums, which start at zero and are contiguous.
26static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"};
27static const char *const IMAGE_FORMAT_NAMES[] = {"JPEG", "PNG", "BMP"};
28#endif
29
31 auto config = this->build_client_config_();
32 this->client_ = std::make_unique<sendspin::SendspinClient>(std::move(config));
33
34 // Set up persistence (preferences must be initialized before providers are added to the client)
37#ifdef USE_SENDSPIN_PLAYER
39#endif
40
41 // Wire providers and client listener
42 this->client_->set_listener(this);
43 this->client_->set_network_provider(this);
44 this->client_->set_persistence_provider(this);
45
46#ifdef USE_SENDSPIN_ARTWORK
47 this->artwork_role_ = &this->client_->add_artwork(this->artwork_config_);
48 this->artwork_role_->set_listener(this);
49#endif
50
51#ifdef USE_SENDSPIN_CONTROLLER
52 this->controller_role_ = &this->client_->add_controller();
53 this->controller_role_->set_listener(this);
54#endif
55
56#ifdef USE_SENDSPIN_METADATA
57 this->metadata_role_ = &this->client_->add_metadata();
58 this->metadata_role_->set_listener(this);
59#endif
60
61#ifdef USE_SENDSPIN_PLAYER
62 this->client_->add_player(this->player_config_).set_listener(this->player_listener_);
63#endif
64
65 if (!this->client_->start_server()) {
66 ESP_LOGE(TAG, "Failed to start Sendspin server");
67 this->mark_failed();
68 return;
69 }
70}
71
72void SendspinHub::loop() { this->client_->loop(); }
73
75 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
76 ESP_LOGCONFIG(TAG,
77 "Sendspin Hub:\n"
78 " Client ID: %s\n"
79 " Task stack in PSRAM: %s",
80 get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_));
81
82#ifdef USE_SENDSPIN_ARTWORK
83 // Slot indices come from the order the image platform entries were declared, so the log is the
84 // only place the mapping from a slot to the artwork it asked for can be read back.
85 uint8_t slot = 0;
86 for (const auto &preference : this->artwork_config_.preferred_formats) {
87 ESP_LOGCONFIG(TAG, " Artwork slot %u: %s as %s, %ux%u, display offset %" PRId32 " ms", slot++,
88 IMAGE_SOURCE_NAMES[static_cast<uint8_t>(preference.source)],
89 IMAGE_FORMAT_NAMES[static_cast<uint8_t>(preference.format)], preference.width, preference.height,
90 preference.display_offset_ms);
91 }
92#endif
93}
94
95// --- Delegating methods ---
96
97// THREAD CONTEXT: Main loop (invoked from Sendspin components)
98void SendspinHub::connect_to_server(const std::string &url) {
99 if (this->is_ready()) {
100 this->client_->connect_to(url);
101 }
102}
103
104// THREAD CONTEXT: Main loop (invoked from Sendspin components)
105void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason) {
106 if (this->is_ready()) {
107 this->client_->disconnect(reason);
108 }
109}
110
111// THREAD CONTEXT: Main loop (invoked from Sendspin components)
112void SendspinHub::update_state(sendspin::SendspinClientState state) {
113 if (this->is_ready()) {
114 this->client_->update_state(state);
115 }
116}
117
118const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
119 // The server matches client_id against the L2 source MAC of the device's multicast traffic.
120 // ESP-IDF derives the ethernet MAC as base+3 by default on ESP32-S3, so we cannot use the
121 // eFuse base MAC when ethernet is the active interface.
122#ifdef USE_ETHERNET
123 if (ethernet::global_eth_component != nullptr) {
125 }
126#endif
128}
129
130sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
131 sendspin::SendspinClientConfig config;
132
133 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
134 config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
135 config.name = App.get_friendly_name();
136 config.product_name = App.get_name();
137 config.manufacturer = "ESPHome";
138 config.software_version = ESPHOME_VERSION;
139 config.httpd_psram_stack = this->task_stack_in_psram_;
140
141 return config;
142}
143
144// --- SendspinClientListener overrides ---
145// THREAD CONTEXT: Main loop (fired from client_->loop())
146
147void SendspinHub::on_group_update(const sendspin::GroupUpdateObject &group) {
148 this->group_update_callbacks_.call(group);
149}
150
159
168
169// --- SendspinNetworkProvider override ---
170
171// THREAD CONTEXT: Main loop (polled by client_->loop())
173
174// --- SendspinPersistenceProvider overrides ---
175
176// THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events)
178 LastPlayedServerPref pref{.server_id_hash = hash};
179 bool ok = this->last_played_server_pref_.save(&pref);
180 if (ok) {
181 ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash);
182 } else {
183 ESP_LOGW(TAG, "Failed to persist last played server hash");
184 }
185 return ok;
186}
187
188// THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events)
189std::optional<uint32_t> SendspinHub::load_last_server_hash() {
191 if (this->last_played_server_pref_.load(&pref)) {
192 ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash);
193 return pref.server_id_hash;
194 }
195 return std::nullopt;
196}
197
198// --- Sendspin role specific methods/overrides ---
199
200#ifdef USE_SENDSPIN_ARTWORK
201// THREAD CONTEXT: Dedicated artwork decode thread; downstream callbacks run here too
202void SendspinHub::on_image_decode(uint8_t slot, const uint8_t *data, size_t length,
203 sendspin::SendspinImageFormat format) {
204 this->artwork_image_decode_callbacks_.call(slot, data, length, format);
205}
206
207// THREAD CONTEXT: Main loop (fired from client_->loop() once the slot's offset-shifted display
208// deadline is reached; lateness_ms reports how far past the deadline the display slipped)
209void SendspinHub::on_image_display(uint8_t slot, uint32_t lateness_ms) {
210 this->artwork_image_display_callbacks_.call(slot, lateness_ms);
211}
212
213// THREAD CONTEXT: Main loop (fired from client_->loop())
214void SendspinHub::on_image_clear(uint8_t slot) { this->artwork_image_clear_callbacks_.call(slot); }
215
216// THREAD CONTEXT: Main loop (invoked from SendspinImageSlot once a delivery is fully presented)
218 if (this->artwork_role_ != nullptr) {
219 this->artwork_role_->frame_done(slot);
220 }
221}
222#endif
223
224#ifdef USE_SENDSPIN_CONTROLLER
225// THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components)
226void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume,
227 std::optional<bool> mute) {
228 if (this->is_ready()) {
229 sendspin::ClientCommandControllerObject obj = {
230 .command = command,
231 .volume = volume,
232 .muted = mute,
233 };
234 this->controller_role_->send_command(obj);
235 }
236}
237
238// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop())
239void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) {
240 this->controller_state_callbacks_.call(state);
241}
242
243// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop())
244// Unlike metadata, this cannot be fanned out as a default-constructed state object: volume and muted are plain values
245// rather than optionals, so children would read a real-looking 0% volume where we mean no value at all. A separate
246// callback lets each child clear only what it can represent.
248#endif
249
250#ifdef USE_SENDSPIN_METADATA
251// THREAD CONTEXT: Main loop (MetadataRoleListener override, fired from client_->loop())
252void SendspinHub::on_metadata(const sendspin::ServerMetadataStateObject &metadata) {
253 this->metadata_update_callbacks_.call(metadata);
254}
255
256// THREAD CONTEXT: Main loop (MetadataRoleListener override, fired from client_->loop())
257// The cached metadata was dropped because the connection to the server was lost, so what the children now mirror is
258// the empty state. Fanning that out as a default-constructed state object rather than through a separate callback
259// keeps one code path in the children: every field is nullopt, which they already publish as empty/unknown.
260void SendspinHub::on_metadata_clear() { this->metadata_update_callbacks_.call(sendspin::ServerMetadataStateObject{}); }
261
262// THREAD CONTEXT: Main loop (invoked from Sendspin components)
264 if (this->is_ready()) {
265 return this->metadata_role_->get_track_progress_ms();
266 }
267 return 0;
268}
269#endif
270
271#ifdef USE_SENDSPIN_PLAYER
272// THREAD CONTEXT: Main loop, called from child component setup() after player role is created and configured
273sendspin::PlayerRole *SendspinHub::get_player_role() {
274 if (this->is_ready()) {
275 return this->client_->player();
276 }
277 return nullptr;
278}
279
280// THREAD CONTEXT: Main loop (SendspinPersistenceProvider override)
281bool SendspinHub::save_static_delay(uint16_t delay_ms) {
282 StaticDelayPref pref{.delay_ms = delay_ms};
283 bool ok = this->static_delay_pref_.save(&pref);
284 if (ok) {
285 ESP_LOGD(TAG, "Persisted static delay: %u ms", delay_ms);
286 } else {
287 ESP_LOGW(TAG, "Failed to persist static delay");
288 }
289 return ok;
290}
291
292// THREAD CONTEXT: Main loop (SendspinPersistenceProvider override)
293std::optional<uint16_t> SendspinHub::load_static_delay() {
294 StaticDelayPref pref{};
295 if (this->static_delay_pref_.load(&pref)) {
296 ESP_LOGI(TAG, "Loaded static delay: %u ms", pref.delay_ms);
297 return pref.delay_ms;
298 }
299 return std::nullopt;
300}
301
302#endif
303
304} // namespace esphome::sendspin_
305
306#endif // USE_ESP32
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
const StringRef & get_friendly_name() const
Get the friendly name of this Application set by pre_setup().
void mark_failed()
Mark this component as failed.
bool is_ready() const
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") std const char * get_eth_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
CallbackManager< void(uint8_t, const uint8_t *, size_t, sendspin::SendspinImageFormat)> artwork_image_decode_callbacks_
CallbackManager< void(uint8_t, uint32_t)> artwork_image_display_callbacks_
sendspin::PlayerRoleListener * player_listener_
void update_state(sendspin::SendspinClientState state)
Updates the client's reported playback state on the server.
bool save_last_server_hash(uint32_t hash) override
bool save_static_delay(uint16_t delay_ms) override
void on_controller_state_clear() override
CallbackManager< void(const sendspin::ServerMetadataStateObject &)> metadata_update_callbacks_
LazyCallbackManager< void(const sendspin::ServerStateControllerObject &)> controller_state_callbacks_
static const char * get_client_id_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Writes the active network interface's MAC into buf and returns its data pointer.
void on_release_high_performance() override
sendspin::MetadataRole * metadata_role_
CallbackManager< void(const sendspin::GroupUpdateObject &)> group_update_callbacks_
sendspin::PlayerRoleConfig player_config_
void on_metadata(const sendspin::ServerMetadataStateObject &metadata) override
sendspin::PlayerRole * get_player_role()
Child components call this to get the PlayerRole instance after setup, so they can push updates to it...
void on_image_clear(uint8_t slot) override
void disconnect_from_server(sendspin::SendspinGoodbyeReason reason)
Disconnects the underlying client from the current server.
std::optional< uint16_t > load_static_delay() override
std::unique_ptr< sendspin::SendspinClient > client_
sendspin::ArtworkRoleConfig artwork_config_
sendspin::SendspinClientConfig build_client_config_()
Builds the SendspinClientConfig from ESPHome configuration and platform info.
sendspin::ControllerRole * controller_role_
ESPPreferenceObject last_played_server_pref_
uint32_t get_track_progress_ms() const
Returns the interpolated track progress in milliseconds, or 0 if the hub is not yet ready.
void send_client_command(sendspin::SendspinControllerCommand command, std::optional< uint8_t > volume=std::nullopt, std::optional< bool > mute=std::nullopt)
LazyCallbackManager< void()> controller_state_clear_callbacks_
ESPPreferenceObject static_delay_pref_
CallbackManager< void(uint8_t)> artwork_image_clear_callbacks_
sendspin::ArtworkRole * artwork_role_
void on_request_high_performance() override
void on_image_display(uint8_t slot, uint32_t lateness_ms) override
void on_image_decode(uint8_t slot, const uint8_t *data, size_t length, sendspin::SendspinImageFormat format) override
std::optional< uint32_t > load_last_server_hash() override
void on_controller_state(const sendspin::ServerStateControllerObject &state) override
void connect_to_server(const std::string &url)
Connects the underlying client to the given Sendspin server.
void on_group_update(const sendspin::GroupUpdateObject &group) override
void artwork_frame_done(uint8_t slot)
Acknowledges the most recent artwork delivery (display or clear) for a slot.
bool request_high_performance()
Request high-performance mode (no power saving) for improved WiFi latency.
void release_roaming_suppression()
Release a roaming suppression request.
void request_roaming_suppression()
Request that post-connect roaming scans be suppressed.
bool release_high_performance()
Release a high-performance mode request.
bool state
Definition fan.h:2
EthernetComponent * global_eth_component
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:28
WiFiComponent * global_wifi_component
const char int const __FlashStringHelper * format
Definition log.h:74
ESPPreferences * global_preferences
const char * get_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Get the device MAC address into the given buffer, in colon-separated uppercase hex notation.
Definition helpers.cpp:816
Application App
Global storage of Application pointer - only one Application can exist.
constexpr uint32_t fnv1a_hash(const char *str)
Calculate a FNV-1a hash of str.
Definition helpers.h:858
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
Persistent storage structure for last played server hash.
Persistent storage structure for player static delay.
uint16_t length
Definition tt21100.cpp:0