30StringRef pair_abort_reason_to_string(sendspin::SendspinPairAbortReason reason) {
31 using sendspin::SendspinPairAbortReason;
33 case SendspinPairAbortReason::ATTEMPT_TIMEOUT:
35 case SendspinPairAbortReason::CONCURRENT_ATTEMPT:
37 case SendspinPairAbortReason::METHOD_NOT_SUPPORTED:
39 case SendspinPairAbortReason::PAIRING_CODE_MISMATCH:
41 case SendspinPairAbortReason::USER_CANCELLED:
43 case SendspinPairAbortReason::UNKNOWN:
50void secure_wipe(
void *data,
size_t len) {
51 volatile auto *p =
static_cast<volatile uint8_t *
>(data);
52 for (
size_t i = 0; i <
len; ++i) {
58ESPPreferenceObject make_blob_pref(
const std::string &name,
size_t size) {
65std::optional<std::vector<uint8_t>> read_blob(ESPPreferenceObject &pref,
size_t size) {
67 if (!pref.load(
out.data(),
out.size())) {
68 secure_wipe(
out.data(),
out.size());
75bool write_blob(ESPPreferenceObject &pref,
const char *key,
size_t size,
const uint8_t *data,
size_t len) {
77 ESP_LOGW(TAG,
"\"%s\" blob of %zu bytes does not match its size of %zu; rejecting write", key,
len,
size);
80 if (!pref.save(data,
len)) {
81 ESP_LOGW(TAG,
"Failed to persist \"%s\" blob (%zu bytes)", key,
len);
88std::optional<size_t> parse_record_slot(
const std::string &key) {
89 const char *
const prefix = sendspin::persistence_keys::RECORD_SLOT_PREFIX;
90 const size_t prefix_len = std::strlen(prefix);
91 if (key.size() <= prefix_len || key.compare(0, prefix_len, prefix) != 0) {
95 for (
size_t i = prefix_len; i < key.size(); i++) {
96 const char c = key[i];
97 if (c <
'0' || c >
'9') {
100 slot = slot * 10 +
static_cast<size_t>(c -
'0');
110#ifdef USE_SENDSPIN_ARTWORK
112static const char *
const IMAGE_SOURCE_NAMES[] = {
"ALBUM",
"ARTIST",
"NONE"};
113static const char *
const IMAGE_FORMAT_NAMES[] = {
"JPEG",
"PNG"};
122 this->
client_ = std::make_unique<sendspin::SendspinClient>(std::move(config));
126 namespace keys = sendspin::persistence_keys;
127 this->
keypair_pref_ = make_blob_pref(
"keypair", keys::KEYPAIR_SIZE);
133 this->
record_slot_prefs_[slot] = make_blob_pref(keys::record_slot_key(slot), keys::RECORD_SLOT_SIZE);
138 this->
client_->set_listener(
this);
139 this->
client_->set_network_provider(
this);
140 this->
client_->set_persistence_provider(
this);
142#ifdef USE_SENDSPIN_ARTWORK
147#ifdef USE_SENDSPIN_CONTROLLER
152#ifdef USE_SENDSPIN_METADATA
157#ifdef USE_SENDSPIN_PLAYER
168 if (this->
enabled_.has_value() && this->unpaired_access_.has_value() &&
169 this->enabled_.value() != this->client_->is_started() && !this->status_has_error()) {
172 }
else if (!this->
client_->start()) {
178#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
195 const char *client_id =
"(unavailable)";
196 if (this->
client_ !=
nullptr && !this->
client_->client_id().empty()) {
197 client_id = this->
client_->client_id().c_str();
199 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
204 " Manufacturer: %s\n"
206 " Firmware version: %s\n"
207 " Task stack in PSRAM: %s\n"
208 " Unpaired access: %s\n"
209 " Pairing code method: %s",
212 YESNO(this->
client_ !=
nullptr && this->
client_->is_unpaired_access_enabled()),
213 this->pairing_code_method_());
215#ifdef USE_SENDSPIN_ARTWORK
219 for (
const auto &preference : this->
artwork_config_.preferred_formats) {
220 ESP_LOGCONFIG(TAG,
" Artwork slot %u: %s as %s, %ux%u, display offset %" PRId32
" ms", slot++,
221 IMAGE_SOURCE_NAMES[
static_cast<uint8_t
>(preference.source)],
222 IMAGE_FORMAT_NAMES[
static_cast<uint8_t
>(preference.format)], preference.width, preference.height,
223 preference.display_offset_ms);
231 ESP_LOGE(TAG,
"Cannot %s: Sendspin failed to start, reboot to retry",
232 enabled ? LOG_STR_LITERAL(
"enable") : LOG_STR_LITERAL(
"disable"));
240 if (this->
client_ ==
nullptr) {
243 return this->
client_->pairing_token();
248 if (this->
client_ ==
nullptr) {
258 if (this->
client_ !=
nullptr) {
259 this->
client_->set_unpaired_access_enabled(enabled);
263#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
270 bool advertise = this->
client_->is_started();
277 ESP_LOGE(TAG,
"Failed to %s mDNS service", advertise ? LOG_STR_LITERAL(
"enable") : LOG_STR_LITERAL(
"disable"));
287 this->
client_->connect_to(url);
294 this->
client_->disconnect(reason);
301 this->
client_->get_group_state().playback_state == sendspin::SendspinPlaybackState::PLAYING) {
309 this->
client_->confirm_pairing_window();
316 this->
client_->cancel_pairing_window();
334 for (
char &c : buf) {
335 if (c >=
'A' && c <=
'F') {
345 return LOG_STR_LITERAL(
"dynamic");
348 return LOG_STR_LITERAL(
"static");
350 return LOG_STR_LITERAL(
"none");
358 sendspin::SendspinClientConfig config;
360 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
373 config.pairing_code_out_channels = {sendspin::SendspinPairingCodeChannel::DISPLAY};
374 config.pairing_code_formats = {sendspin::SendspinPairingCodeFormat::DIGITS};
378 config.static_pairing_code_locations = {
"operator"};
438 namespace keys = sendspin::persistence_keys;
439 if (
auto slot = parse_record_slot(key); slot.has_value()) {
442 if (key == keys::RECORD_ORDER) {
445 if (key == keys::KEYPAIR) {
448 if (key == keys::PAIRING_PSK) {
451 if (key == keys::LAST_PLAYED) {
454 if (key == keys::OUTPUT_DELAY) {
462 if (pref ==
nullptr) {
463 ESP_LOGW(TAG,
"load_blob: unknown key \"%s\"", key.c_str());
466 return read_blob(*pref,
size);
471 if (pref ==
nullptr) {
472 ESP_LOGW(TAG,
"save_blob: unknown key \"%s\"", key.c_str());
475 return write_blob(*pref, key.c_str(),
size, data,
len);
483#ifdef USE_SENDSPIN_ARTWORK
486 sendspin::SendspinImageFormat
format) {
507#ifdef USE_SENDSPIN_CONTROLLER
510 std::optional<bool> mute) {
512 sendspin::ClientCommandControllerObject obj = {
536#ifdef USE_SENDSPIN_METADATA
557#ifdef USE_SENDSPIN_PLAYER
561 return this->
client_->player();
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().
bool status_has_error() const
StringRef is a reference to a string owned by something else.
constexpr const char * c_str() const
static constexpr StringRef from_lit(const CharT(&s)[N])
const char * get_eth_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
bool set_service_enabled(const char *service_type, const char *proto, bool enabled)
Enable or disable a compiled-in service, matched by type and proto (e.g.
Thin adapter over sendspin::SendspinClient.
const char * manufacturer_
ESPPreferenceObject output_delay_pref_
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_
const char * firmware_version_
bool pairing_code_display_supported_
const char * get_product_name_() const
Returns the product name reported to the server: the configured model, or the device name.
std::optional< bool > unpaired_access_
void on_controller_state_clear() override
std::optional< std::vector< uint8_t > > load_blob(const std::string &key) override
void confirm_pairing_window()
Confirms a pairing attempt on the device.
CallbackManager< void(const sendspin::ServerMetadataStateObject &)> metadata_update_callbacks_
ESPPreferenceObject record_order_pref_
std::optional< std::string > get_pairing_token() const
Returns the pairing token a Sendspin server uses to pair with this device.
LazyCallbackManager< void(const std::string &, StringRef)> pairing_failed_callbacks_
void set_unpaired_access_enabled(bool enabled)
Turns unpaired (Sentinel) access on or off.
LazyCallbackManager< void(const sendspin::ServerStateControllerObject &)> controller_state_callbacks_
void on_release_high_performance() override
bool save_blob(const std::string &key, const uint8_t *data, size_t len) override
LazyCallbackManager< void()> clear_pairing_code_callbacks_
bool is_network_ready() override
sendspin::MetadataRole * metadata_role_
void on_display_pairing_code(const std::string &code, sendspin::SendspinPairingCodeFormat format) override
void on_open_pairing_window() override
bool is_client_running() const
Returns whether the Sendspin client is running.
const char * pairing_code_method_() const
CallbackManager< void(const sendspin::GroupUpdateObject &)> group_update_callbacks_
LazyCallbackManager< void(const std::string &)> display_pairing_code_callbacks_
void switch_client()
Sends the SWITCH controller command; exposed as the sendspin.switch action.
void leave_group()
Leaves the current group so another source can use the speaker.
sendspin::PlayerRoleConfig player_config_
void on_metadata(const sendspin::ServerMetadataStateObject &metadata) override
void cancel_pairing_window()
Closes an open pairing window, so a waiting pairing attempt is not confirmed.
ESPPreferenceObject last_played_pref_
void dump_config() override
LazyCallbackManager< void(const std::string &)> pairing_succeeded_callbacks_
const char * static_pairing_code_
void on_shutdown() override
static const char * get_mac_address_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Writes the active network interface's MAC, in lowercase, into buf and returns its data pointer.
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 on_pairing_failed(const std::string &server_id, sendspin::SendspinPairAbortReason reason) override
void on_clear_pairing_code() override
void disconnect_from_server(sendspin::SendspinGoodbyeReason reason)
Disconnects the underlying client from the current server.
std::unique_ptr< sendspin::SendspinClient > client_
sendspin::ArtworkRoleConfig artwork_config_
void update_mdns_service_()
Keeps the _sendspin mDNS service advertised while the client is running.
sendspin::SendspinClientConfig build_client_config_()
Builds the SendspinClientConfig from ESPHome configuration and platform info.
mdns::MDNSComponent * mdns_
sendspin::ControllerRole * controller_role_
void on_pairing_succeeded(const std::string &server_id) override
bool task_stack_in_psram_
uint32_t get_track_progress_ms() const
Returns the interpolated track progress in milliseconds, or 0 if the hub is not yet ready.
std::optional< bool > enabled_
StringRef get_client_id() const
Returns the Sendspin client_id: base64url of the client's static public key.
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_
void on_metadata_clear() override
CallbackManager< void(uint8_t)> artwork_image_clear_callbacks_
ESPPreferenceObject keypair_pref_
void on_close_pairing_window() override
LazyCallbackManager< void()> open_pairing_window_callbacks_
sendspin::ArtworkRole * artwork_role_
void on_request_high_performance() override
std::pair< ESPPreferenceObject *, size_t > pref_for_key_(const std::string &key)
The preference a library key is stored in and its blob size, or {nullptr, 0} for a key with no storag...
void set_enabled(bool enabled)
Requests the Sendspin client, including the server, the roles and the mDNS advertisement,...
std::array< ESPPreferenceObject, SENDSPIN_RECORD_SLOTS > record_slot_prefs_
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
ESPPreferenceObject pairing_psk_pref_
void on_controller_state(const sendspin::ServerStateControllerObject &state) override
LazyCallbackManager< void()> close_pairing_window_callbacks_
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.
std::vector< uint8_t > & out
EthernetComponent * global_eth_component
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
constexpr size_t SENDSPIN_RECORD_SLOTS
Pairing-record slots, handed to the client as max_pairing_records.
SendspinHub * global_sendspin_hub
ESPHOME_LOG_TAG(TAG, "sendspin.image")
WiFiComponent * global_wifi_component
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.
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.
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
bool sync()
Commit pending writes to flash.