### Validate, compile, and run the XIAO ESP32-C5 example Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/README.md Commands to validate, compile, and run the complete XIAO ESP32-C5 example. Requires examples/secrets.yaml with wifi_ssid and wifi_password. ```sh esphome config examples/xiao-esp32c5.yaml esphome compile examples/xiao-esp32c5.yaml esphome run examples/xiao-esp32c5.yaml ``` -------------------------------- ### k80_controller_configure Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md One-time installation of the controller queue configuration. Validates the queue state, endpoints, and profiles, then installs the configuration and initializes all positions. ```APIDOC ## k80_controller_configure ### Description One-time installation of the controller queue configuration. Validates the queue state, endpoints, and profiles, then installs the configuration and initializes all positions. ### Signature `static inline int k80_controller_configure(k80_controller_queue *q, k80_controller_pending *storage, const k80_controller_endpoint *endpoints, size_t count)` ### Parameters - **q** (k80_controller_queue *) - Pointer to the controller queue. - **storage** (k80_controller_pending *) - Pointer to storage for pending configuration. - **endpoints** (const k80_controller_endpoint *) - Array of endpoints to configure. - **count** (size_t) - Number of endpoints. ### Returns - **int** - 1 on success, 0 on failure. ### Behavior Rejects if `q` is null, `q->pending` or `q->armed` is already set, `storage` or `endpoints` is null, `count` is 0 or exceeds K80_CONTROLLER_ENDPOINT_LIMIT, any endpoint address is invalid for its profile, any profile has no fields, or any two endpoints share the same slot and group. On success, copies the endpoints into storage, sets defaults (attempts and spacing), and initializes each position as enabled with `k80_default_controls()`. ### Source `k80_controller_scheduler.h:95-121` ``` -------------------------------- ### Initialize and configure A7105 probe device Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/md7105-probe-protocol.md Shows the full probe protocol setup: include the header, define a custom SPI transfer callback, create a bus struct, then reset the device, configure 4-wire mode, write a device ID, and configure the observed FIFO at offset 2. Each call returns A7105_PROBE_OK on success; a non-OK result indicates a transport error and the example returns early. ```c #include "md7105_probe_protocol.h" // bus backed by a custom SPI driver static int my_transfer(void *ctx, const uint8_t *tx, uint8_t *rx, size_t len) { return my_spi_transfer((struct MyBus *)ctx, tx, rx, len) ? A7105_PROBE_TRANSPORT_ERROR : A7105_PROBE_OK; } struct MyBus bus_storage; // contains 'my_transfer' context a7105_probe_bus_t bus = {&bus_storage, my_transfer}; if (a7105_probe_reset_device(&bus) != A7105_PROBE_OK || a7105_probe_configure_4wire(&bus) != A7105_PROBE_OK) return; // transport error uint8_t id[4] = {0xA5, 0x5A, 0xB9, 0x46}; if (a7105_probe_write_id(&bus, id) != A7105_PROBE_OK) return; if (a7105_probe_configure_observed_fifo_at(&bus, 2) != A7105_PROBE_OK) return; ``` -------------------------------- ### k80_controller_configure - One-time installation Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Installs the endpoint configuration into the queue. Rejects if the queue is already pending or armed, if any address is invalid, if any profile has no fields, or if endpoints share slot+group. On success, copies endpoints to storage, sets defaults, and initializes all positions enabled. ```cpp static inline int k80_controller_configure(k80_controller_queue *q, k80_controller_pending *storage, const k80_controller_endpoint *endpoints, size_t count) { if (!q || q->pending || q->armed || !storage || !endpoints || count == 0 || count > K80_CONTROLLER_ENDPOINT_LIMIT) return 0; for (size_t i = 0; i < count; ++i) { if (!k80_controller_profile_address_valid(endpoints[i].profile, endpoints[i].slot, endpoints[i].group) || k80_controller_profile_fields(endpoints[i].profile) == 0) return 0; for (size_t j = 0; j < i; ++j) if (endpoints[i].slot == endpoints[j].slot && endpoints[i].group == endpoints[j].group) return 0; } for (size_t i = 0; i < count; ++i) { storage[i].slot = (uint8_t)endpoints[i].slot; storage[i].group = (uint8_t)endpoints[i].group; storage[i].profile = (uint8_t)endpoints[i].profile; storage[i].remaining = 0; storage[i].enabled = 1; storage[i].state = k80_default_controls(); } q->pending = storage; q->count = count; q->attempts = K80_CONTROLLER_DEFAULT_ATTEMPTS; q->spacing_ms = K80_CONTROLLER_DEFAULT_SPACING_MS; return 1; } ``` -------------------------------- ### k80_controller_started: Mark TX start time Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Marks the most recently selected slot's TX start time, driving the per-channel spacing clock. ```cpp static inline void k80_controller_started(k80_controller_queue *q, uint64_t started) { q->slot_started_us[q->selected_slot] = started; q->started_slots |= UINT64_C(1) << q->selected_slot; } ``` -------------------------------- ### k80_controller_started Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Marks the most recently selected slot's TX start time, driving the per-channel spacing clock. ```APIDOC ## k80_controller_started ### Description Marks the most recently selected slot's TX start time, driving the per-channel spacing clock. ### Function Signature ```cpp static inline void k80_controller_started(k80_controller_queue *q, uint64_t started) ``` ### Parameters - `q` (k80_controller_queue*) - Required - Pointer to the controller queue. - `started` (uint64_t) - Required - The start time in microseconds. ### Return Value - `void` - No return value. ### Source `k80_controller_scheduler.h:208-210` ``` -------------------------------- ### Usage example for ct_to_state Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/state-conversion.md Shows usage of ct_to_state with error handling. First converts RGB to a wire state, then converts a CCT value (5600 K) to a wire index. Logs warnings and returns early on invalid input, preserving the light state. ```cpp #include "yiscaxia_state_conversion.h" k80_control_values next = k80_default_controls(); // RGB -> HSI wire state float r = 1.0f, g = 0.2f, b = 0.1f; if (!rgb_to_state(r, g, b, /*intensity=*/0.8f, next, &next)) { ESP_LOGW("app", "invalid RGB — state rejected"); return; // light state kept; no TX } // CCT -> wire index with error handling float mireds = 1000000.0f / 5600.0f; // 5600 K if (!ct_to_state(mireds, 1.0f, next, &next)) { ESP_LOGW("app", "CCT outside 2600..10000 K — rejected"); return; } // next.mode == K80_MODE_CCT; next.ct_index == 30 ``` -------------------------------- ### YiscaxiaController basic wiring Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-controller.md Creates and configures a YiscaxiaController instance as done by the Python component's to_code() hook. Must call setup() and loop() later from the ESPHome scheduler. ```cpp auto *controller = new yiscaxia::YiscaxiaController(); controller->set_position_capacity(12); controller->set_initial_pairs("1A,1B,1C,1D,1E,1F"); controller->set_transport(transport); // Md7105Transport instance // later: controller->setup(), controller->loop() from the ESPHome scheduler ``` -------------------------------- ### k80_controller_begin_drain Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Starts the drain lifecycle by setting the draining flag. Returns 0 if the queue is already armed or draining, or if the queue is uninitialized. ```APIDOC ## k80_controller_begin_drain ### Description Starts the drain lifecycle by setting the draining flag. ### Signature `int k80_controller_begin_drain(k80_controller_queue *q)` ### Parameters - **q** (k80_controller_queue *) - Pointer to the controller queue. ### Returns - **int** - 1 on success, 0 if already armed/draining or uninitialized. ### Behavior Sets `draining = 1`. ### Source `k80_controller_scheduler.h:123-126` ``` -------------------------------- ### Minimal YAML configuration from xiao-esp32c5.yaml Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/configuration.md No description ```yaml external_components: - source: type: local path: ../components components: [yiscaxia] spi: id: radio_spi clk_pin: GPIO8 mosi_pin: GPIO10 miso_pin: GPIO9 yiscaxia: id: pixel_controller transport: type: md7105 spi_id: radio_spi cs_pin: GPIO1 name_prefix: Pixel K80 position_capacity: 12 initial_pairs: 1A,1B,1C,1D,1E,1F available_pairs: id: pixel_pairs name: Available channel and group pairs transmission_attempts: id: pixel_attempts name: Transmission attempts channel_spacing: id: pixel_spacing name: Channel transmission spacing rainbow_degrees_per_step: id: pixel_rainbow_step name: Custom Rainbow degrees per step rainbow_step_spacing: id: pixel_rainbow_spacing name: Custom Rainbow step spacing ``` -------------------------------- ### Prepare and apply positions with error handling Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-config.md Includes the header and demonstrates the controller-style flow: validate positions with prepare_positions, then apply them with apply_positions. On validation failure, logs the error and returns. ```cpp #include "k80_controller_config.h" // controller-style flow: validate first, then apply Positions proposed; auto result = prepare_positions(&queue, "1A,2B,2D", &proposed); if (!result) { ESP_LOGE("app", "rejected: %s", result.error.c_str()); return; } result = apply_positions(&queue, proposed, [](size_t i) { /* cancel deferred output for remapped position i */ }); ``` -------------------------------- ### Invalid initial_pairs validation failure example Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/esphome-config-schema.md Example that triggers a validation failure at `esphome config` time via `validate_pairs`. The `initial_pairs: "1a"` value is invalid because valid pairs use channel 1–48 and group A–F, raising cv.Invalid. ```yaml yiscaxia: id: c transport: {type: md7105, cs_pin: GPIO1} initial_pairs: "1a" # -> cv.Invalid: use channel 1–48 + group A–F available_pairs: {id: p} transmission_attempts: {id: t} channel_spacing: {id: s} ``` -------------------------------- ### Manual wiring of YiscaxiaController and YiscaxiaLightOutput Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-light.md Minimal manual wiring of the Yiscaxia light classes, normally created by ESPHome codegen. Creates a controller from an id, wraps it in a YiscaxiaLightOutput for position 1, then wraps that in an sdk_test::LightState and calls setup_state. Optionally activates the Custom Rainbow effect via CUSTOM_RAINBOW_EFFECT_INDEX. ```cpp auto *controller = static_cast(id(pixel_controller)); auto *output = new yiscaxia::YiscaxiaLightOutput(controller, 0); // position 1 // sdk_test-style: state wraps output auto *state = new sdk_test::LightState(output, 0x1234); output->setup_state(state); // If RGB with an active Custom Rainbow effect: state->effect(yiscaxia::YiscaxiaLightOutput::CUSTOM_RAINBOW_EFFECT_INDEX); ``` -------------------------------- ### YiscaxiaLightOutput class declaration Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-light.md Class declaration for the K80 light output adapter. The constructor takes the owning controller and a zero-based endpoint index; setup_state registers the light and adds ten effects (SOS, Lightning 1/2, TV Screen, Police, Ambulance, Fire Engine, RGB Circle 1/2, and the custom rainbow). ```cpp class YiscaxiaLightOutput final : public light::LightOutput { public: explicit YiscaxiaLightOutput(YiscaxiaController *parent, int endpoint) : parent_(parent), endpoint_(endpoint), rainbow_(this) {} void update_state(light::LightState *state) override; void setup_state(light::LightState *state) override; light::LightTraits get_traits() override; void write_state(light::LightState *state) override; protected: friend class YiscaxiaCustomRainbowEffect; static constexpr uint32_t CUSTOM_RAINBOW_EFFECT_INDEX = K80_NATIVE_EFFECT_MAX + 1; YiscaxiaController *parent_; const int endpoint_; int last_non_fls_mode_{K80_MODE_CCT}; bool effects_registered_{false}; bool initial_restore_write_{false}; bool automatic_phase_{false}; bool generating_phase_{false}; bool manual_pending_{false}; k80_control_values desired_{k80_default_controls()}; YiscaxiaNativeEffect sos_{"SOS"}, lightning_1_{"Lightning 1"}, lightning_2_{"Lightning 2"}, tv_{"TV Screen"}, police_{"Police"}, ambulance_{"Ambulance"}, fire_{"Fire Engine"}, circle_1_{"RGB Circle 1"}, circle_2_{"RGB Circle 2"}; YiscaxiaCustomRainbowEffect rainbow_; }; ``` -------------------------------- ### YiscaxiaCustomRainbowEffect class declaration Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-light.md Defines the YiscaxiaCustomRainbowEffect class, a host-side rainbow effect that advances hue and pushes synthetic RGB light calls without transmitting a native lamp effect. The class is declared in yiscaxia_light.h:18-39 and includes methods start(), apply(), hue(), initialize_hue_if_pending(), and set_value(). ```cpp class YiscaxiaCustomRainbowEffect final : public light::LightEffect { public: explicit YiscaxiaCustomRainbowEffect(YiscaxiaLightOutput *output) : LightEffect("Custom Rainbow"), output_(output) {} void start() override; void apply() override; uint16_t hue() const { return this->hue_; } void initialize_hue_if_pending(int hue) { if (this->hue_initialization_pending_) { this->hue_ = static_cast(hue % RAINBOW_HUE_PERIOD); this->hue_initialization_pending_ = false; } } void set_value(float value) { this->value_ = value; } protected: bool hue_initialization_pending_{false}; YiscaxiaLightOutput *output_; uint32_t last_phase_check_ms_{0}; uint16_t hue_{0}; float value_{1.0f}; }; ``` -------------------------------- ### YiscaxiaLightState class definition Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-light.md Defines a light state that flags the initial restore update around the base setup() and provides methods to consume that update and cancel pending output for remapping. Generated as _position_ with RESTORE_DEFAULT_OFF restore mode and 0s default transition length. ```cpp class YiscaxiaLightState : public light::LightState { public: explicit YiscaxiaLightState(light::LightOutput *output) : LightState(output) {} void setup() override { this->initial_restore_update_pending_ = true; light::LightState::setup(); this->initial_restore_update_pending_ = false; } bool consume_initial_restore_update() { // Initial-state callbacks may issue ordinary commands before native restore. if (!this->initial_restore_update_pending_ || this->initial_state_callback_) return false; this->initial_restore_update_pending_ = false; return true; } protected: friend class YiscaxiaController; void cancel_pending_output_for_remap() { if (!this->transformer_ && !this->next_write_) return; const auto target = this->transformer_ ? this->transformer_->get_target_values() : this->current_values; this->set_immediately_(target, false); this->next_write_ = false; this->output_->write_state(this); this->disable_loop_if_idle_(); } bool initial_restore_update_pending_{false}; }; ``` -------------------------------- ### Format C/C++ with astyle Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/README.md Formats project-owned C/C++ files with Artistic Style 3.1 using the checked-in .astylerc options. Run from the repository root. ```sh git ls-files -z '*.h' '*.cpp' '*.c' | xargs -0 astyle --options=none --project=.astylerc ``` -------------------------------- ### YiscaxiaTransport abstract class definition Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-transport.md Defines the abstract transport interface with pure virtual methods ready, now_us, and transmit. Backends must also implement the Component lifecycle methods setup(), dump_config(), and get_setup_priority() because TRANSPORT_SCHEMA extends cv.COMPONENT_SCHEMA and codegen calls cg.register_component(transport, ...). ```cpp class YiscaxiaTransport : public Component { public: virtual bool ready() const = 0; virtual uint64_t now_us() const = 0; virtual yiscaxia_tx_result transmit(const yiscaxia_tx_packet &packet, int slot) = 0; }; ``` -------------------------------- ### Prepare and apply pair-table edit Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-controller.md Handles a rejected pair-table edit. Shows that prepare_pairs returns a result with an error string when a duplicate address is detected, and that apply_pairs can still reject the proposed configuration at apply time. ```cpp k80_controller_config::Positions proposed; auto result = controller->prepare_pairs("1A,1A", &proposed); // duplicate pair if (!result) { // result.error == "Position 2: duplicate address already used by position 1" return; } if (!controller->apply_pairs(proposed)) { /* rejected at apply time */ } ``` -------------------------------- ### k80_controller_take_state Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Picks the next transmittable state: round-robin over slots, skipping slots whose spacing clock is still running (only slots already started). Within a slot, round-robin over positions with pending attempts. On success, decrements remaining, advances cursors, records selected_slot, and returns the endpoint plus a copy of the state. ```cpp static inline int k80_controller_take_state(k80_controller_queue *q, uint64_t now, int *endpoint, k80_control_values *state) { if (!q || !endpoint || !state || !q->pending || !q->armed) return 0; for (size_t i = 0; i < K80_CONTROLLER_RF_SLOT_COUNT; ++i) { const size_t slot = (q->next_slot + i) % K80_CONTROLLER_RF_SLOT_COUNT; if ((q->started_slots & (UINT64_C(1) << slot)) && now - q->slot_started_us[slot] < (uint64_t)q->spacing_ms * 1000U) continue; for (size_t position = 0; position < q->count; ++position) { const size_t e = (q->next_position[slot] + position) % q->count; if (!q->pending[e].remaining || q->pending[e].slot != slot) continue; *endpoint = (int)e; *state = q->pending[e].state; --q->pending[e].remaining; q->next_position[slot] = (e + 1) % q->count; q->next_slot = (slot + 1) % K80_CONTROLLER_RF_SLOT_COUNT; q->selected_slot = (uint8_t)slot; return 1; } } return 0; } ``` -------------------------------- ### YiscaxiaNumber class declaration Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-entities.md Defines the YiscaxiaNumber component that manages a numeric setting. The setup method publishes the controller's current value, but marks the component failed with status "Unknown configuration setting" if configuration_setting_max returns 0. Uses record version 1 and distinct preference salts per setting (0x59504304 attempts, 0x59504303 spacing, 0x59504305 degrees, 0x59504306 step spacing). ```cpp class YiscaxiaNumber final : public number::Number, public Component { public: explicit YiscaxiaNumber(YiscaxiaController *parent, YiscaxiaSetting setting) : parent_(parent), setting_(setting) {} float get_setup_priority() const override { return 799.4f; } const std::string &configuration_status() const { return this->status_; } void setup() override { if (configuration_setting_max(this->setting_) == 0) { this->status_ = "Unknown configuration setting"; this->mark_failed(); return; } if (this->setting_ == YiscaxiaSetting::RAINBOW_STEP_SPACING) this->setup_record<5>(); else this->setup_record<3>(); this->publish_state(this->parent_->configuration_setting(this->setting_)); } protected: static constexpr uint8_t RECORD_VERSION = 1; template struct Record { uint8_t bytes[Size] {}; }; static_assert(sizeof(Record<3>) == 3); static_assert(sizeof(Record<5>) == 5); YiscaxiaController *parent_; YiscaxiaSetting setting_; bool known_saved_{false}; ESPPreferenceObject preference_; std::string status_{"Ready"}; uint32_t preference_salt() const; template void setup_record(); bool valid(float value) const; std::string invalid_reason(float value) const; template static uint32_t decode(const Record &record); template static Record encode(uint32_t value); bool save_value(uint32_t value); void control(float value) override; }; ``` -------------------------------- ### Configure and use k80_controller_queue with k80_controller_configure, begin_drain, request_state, take_state Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Shows the full lifecycle of the k80_controller_queue: configure with 6 endpoints, drain to apply the initial table, request a state change, and take states for transmission. Fair pacing is automatic via per-slot round-robin cursors and spacing_ms blocks further TX on that channel. ```c #include "k80_controller_scheduler.h" k80_controller_pending storage[6]; k80_controller_endpoint endpoints[6]; for (int i = 0; i < 6; ++i) endpoints[i] = (k80_controller_endpoint){2, i, K80_CONTROLLER_PROFILE_NATIVE_RAW_FREQUENCY}; k80_controller_queue queue = {0}; if (!k80_controller_configure(&queue, storage, endpoints, 6)) { /* invalid config */ return; } k80_controller_begin_drain(&queue); /* ... initial table applied ... */ k80_controller_end_drain(&queue); k80_control_values on = k80_default_controls(); on.mode = K80_MODE_HSI; on.level = 80; on.hue = 200; on.saturation = 100; if (!k80_controller_request_state(&queue, 0, &on)) { /* rejected: disabled/invalid */ return; } int endpoint; k80_control_values out; while (k80_controller_take_state(&queue, now_us(), &endpoint, &out)) { /* build and transmit for endpoint; then: */ k80_controller_started(&queue, started_us); } ``` -------------------------------- ### a7105_probe_initialize_rx Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/md7105-probe-protocol.md Initializes receive-only baseline profile. ```APIDOC ## a7105_probe_initialize_rx ### Description Initializes receive-only baseline using the 500k profile on channel slot 0, used before calibration. ### Signature ```cpp int a7105_probe_initialize_rx(const a7105_probe_bus_t *bus); ``` ### Parameters - **bus** (const a7105_probe_bus_t*) - Required - Pointer to the bus configuration. ### Returns - (int) - Status code (0 on success, non-zero on error). ### Source `md7105_probe_protocol.h:453-461` ``` -------------------------------- ### Implementing a custom transport Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-transport.md Shows how to subclass yiscaxia::YiscaxiaTransport and override the required methods: ready(), now_us(), and transmit(). ```APIDOC ## Implementing a custom transport ### Description Subclass `yiscaxia::YiscaxiaTransport` and override the pure virtual methods to provide a custom radio transport. ### Methods #### `bool ready() const` - **Return** (bool) - Whether the transport is ready. #### `uint64_t now_us() const` - **Return** (uint64_t) - Current time in microseconds. #### `yiscaxia_tx_result transmit(const yiscaxia_tx_packet &packet, int slot)` - **Parameters** - `packet` (const yiscaxia_tx_packet&) - The packet to transmit. - `slot` (int) - The slot number. - **Return** (yiscaxia_tx_result) - The result of the transmission. ### Example ```cpp class MyTransport : public yiscaxia::YiscaxiaTransport { public: bool ready() const override { return ready_; } uint64_t now_us() const override { return micros(); } yiscaxia_tx_result transmit(const yiscaxia_tx_packet &packet, int slot) override { yiscaxia_tx_result r{}; if (!radio_write(packet.bytes, slot)) { r.error = YISCAXIA_TX_FAILED; return r; } r.trigger_attempted = 1; r.completed = 1; r.restored = 1; r.started_us = now_us(); r.error = YISCAXIA_TX_OK; return r; } bool ready_{false}; }; ``` ``` -------------------------------- ### k80_controller_build_profile_packet Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-protocol.md Builds a profile packet for BRIGHTNESS or CCT_2700. Returns -1 for invalid arguments or unsupported native profiles. For CCT_2700, writes a fixed nine-byte seed with the level in byte 3. ```cpp static inline int k80_controller_build_profile_packet(int profile, int slot, int group, int level, uint8_t output[K80_FRAME_SIZE]) { if (!output || level < 0 || level > K80_LEVEL_MAX || !k80_controller_profile_address_valid(profile, slot, group)) return -1; if (profile == K80_CONTROLLER_PROFILE_BRIGHTNESS) return k80_controller_build_packet(group, level, output); if (profile != K80_CONTROLLER_PROFILE_CCT_2700) return -1; static const uint8_t cct_2700[9] = {0x36, 0, 0, 0, 1, 0xB4, 0, 0x64, 8}; memcpy(output, cct_2700, sizeof(cct_2700)); output[3] = (uint8_t)level; k80_finalize_frame(output); return 0; } ``` -------------------------------- ### C++ programmatic edit of YiscaxiaPairs with status handling Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-entities.md Shows how to cast the pairs entity and call make_call().set_state() to set a state, then log the configuration status. Use this for programmatic edits within lambdas. ```cpp auto *pairs = static_cast(id(pixel_pairs)); pairs->make_call().set_state("1A,2B,2D").perform(); ESP_LOGI("app", "pairs status: %s", pairs->configuration_status().c_str()); ``` -------------------------------- ### Turn on position light with SOS effect on boot Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/README.md Turns on the first position light with the SOS effect at boot. The light ID follows the pattern _position_<1-based position>. ```yaml esphome: on_boot: then: - light.turn_on: id: pixel_controller_position_1 effect: SOS ``` -------------------------------- ### k80_controller_build_state_packet Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-protocol.md Builds a runtime state packet used by YiscaxiaController::loop. Layout: byte0 0x36, byte1 group, byte2 mode when level != 0, byte3 level, byte4 ct_index (CCT) or 0 (HSI/FLS), bytes5-6 hue little-endian (HSI), byte7 saturation (HSI), byte8 effect (FLS) or 1 (HSI), bytes9-11 sum+CRC via k80_finalize_frame. OFF always uses the CCT2700 zero format with the requested group. ```cpp static inline int k80_controller_build_state_packet(int profile, int slot, int group, const k80_control_values *state, uint8_t output[K80_FRAME_SIZE]) { if (!output || !k80_controller_state_valid(profile, slot, group, state)) return -1; uint8_t body[K80_FRAME_SIZE] = {0x36, 0, 0, 0, 1, 0xB4, 0, 100, 8, 0, 0, 0}; body[1] = (uint8_t)group; body[3] = (uint8_t)state->level; if (state->level != 0) { body[2] = (uint8_t)state->mode; if (state->mode == K80_MODE_CCT) body[4] = (uint8_t)state->ct_index; else { body[4] = 0; body[5] = body[6] = 0; body[8] = (uint8_t)state->effect; if (state->mode == K80_MODE_HSI) { body[5] = (uint8_t)state->hue; body[6] = (uint8_t)(state->hue >> 8); body[7] = (uint8_t)state->saturation; body[8] = 1; } } } k80_finalize_frame(body); memcpy(output, body, sizeof(body)); return 0; } ``` -------------------------------- ### k80_controller_profile_address_valid function Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-protocol.md Live admission per profile: CCT_2700 and NATIVE admit only raw slot/group 0/0; NATIVE_MEASURED_FREQUENCY admits slots 0..3; NATIVE_RAW_FREQUENCY admits all 48 slots; BRIGHTNESS admits any raw address. Rejected addresses fail packet building and state requests. ```cpp static inline K80_CONTROLLER_CONSTEXPR int k80_controller_profile_address_valid(int profile, int slot, int group) { if (!k80_controller_raw_address_valid(slot, group)) return 0; if (profile == K80_CONTROLLER_PROFILE_CCT_2700) return slot == 0 && group == 0; if (profile == K80_CONTROLLER_PROFILE_NATIVE) return slot == 0 && group == 0; if (profile == K80_CONTROLLER_PROFILE_NATIVE_MEASURED_FREQUENCY) return slot <= K80_CONTROLLER_MEASURED_RF_SLOT_MAX; if (profile == K80_CONTROLLER_PROFILE_NATIVE_RAW_FREQUENCY) return 1; return profile == K80_CONTROLLER_PROFILE_BRIGHTNESS; } ``` -------------------------------- ### Run MD7105 TX packet protocol with a7105_tx_packet_run Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/md7105-tx-packet-protocol.md Shows how to call a7105_tx_packet_run with a packet built by k80_controller_build_state_packet and handle the returned result. The error codes YISCAXIA_TX_STANDBY_FAILED and YISCAXIA_TX_RESTORE_FAILED indicate the radio state is unknown or restore failed, so radio service should be stopped; YISCAXIA_TX_DEADLINE_EXCEEDED allows retry; YISCAXIA_TX_OK with restored means success and RX is listening again on slot 2. ```cpp #include "md7105_tx_packet_protocol.h" yiscaxia_tx_packet packet; // filled by k80_controller_build_state_packet auto result = a7105_tx_packet_run(&bus, &packet, slot, now, delay, nullptr); if (result.trigger_attempted) { // schedule the per-channel spacing clock using result.started_us } if (result.error == YISCAXIA_TX_STANDBY_FAILED || result.error == YISCAXIA_TX_RESTORE_FAILED) { // radio state is unknown/restorable-failed: stop all radio service radio_dead = true; } else if (result.error == YISCAXIA_TX_DEADLINE_EXCEEDED) { // retry is possible but the channel pacing clock already accounts for the attempt } else if (result.error == YISCAXIA_TX_OK && result.restored) { // success; RX listening again on slot 2 } ``` -------------------------------- ### k80_controller_profile_fields - Capability helper Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Returns a bitmask of supported fields for a given profile. For native profiles, returns all fields; for BRIGHTNESS and CCT_2700, returns only BRIGHTNESS; otherwise returns NONE. ```cpp static inline uint32_t k80_controller_profile_fields(int profile) { if (k80_controller_profile_is_native(profile)) return K80_CONTROLLER_FIELD_BRIGHTNESS | K80_CONTROLLER_FIELD_RGB | K80_CONTROLLER_FIELD_COLOR_TEMPERATURE | K80_CONTROLLER_FIELD_EFFECT | K80_CONTROLLER_FIELD_MODE; switch (profile) { case K80_CONTROLLER_PROFILE_BRIGHTNESS: case K80_CONTROLLER_PROFILE_CCT_2700: return K80_CONTROLLER_FIELD_BRIGHTNESS; default: return K80_CONTROLLER_FIELD_NONE; } } ``` -------------------------------- ### k80_controller_take Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Brightness-only variant of take_state that retrieves the current level for the selected endpoint. ```APIDOC ## k80_controller_take ### Description Brightness-only variant of `take_state` that retrieves the current level for the selected endpoint. ### Function Signature ```cpp static inline int k80_controller_take(k80_controller_queue *q, uint64_t now, int *endpoint, int *level) ``` ### Parameters - `q` (k80_controller_queue*) - Required - Pointer to the controller queue. - `now` (uint64_t) - Required - Current time in microseconds. - `endpoint` (int*) - Required - Output parameter for the selected endpoint. - `level` (int*) - Required - Output parameter for the brightness level. ### Return Value - `int` - Returns 1 on success, 0 on failure (e.g., if `level` is NULL or no state available). ### Source `k80_controller_scheduler.h:200-206` ``` -------------------------------- ### rgb_to_state: convert RGB to HSI control values Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/state-conversion.md Converts raw normalized RGB channels and an intensity (state×brightness product) into an HSI k80_control_values. Preserves non-color fields from previous except level; sets mode to K80_MODE_HSI, computes saturation (0..100 rounded) and hue (0..360) only when delta != 0. Returns false if output is null, any RGB channel is invalid, or intensity is invalid. ```cpp // Use raw normalized RGB; intensity is already the state/brightness product. inline bool rgb_to_state(float red, float green, float blue, float intensity, const k80_control_values &previous, k80_control_values *output) { if (!output || !unit_value_valid(red) || !unit_value_valid(green) || !unit_value_valid(blue)) return false; k80_control_values next = previous; if (!brightness_to_level(intensity, &next.level)) return false; const float maximum = std::max({red, green, blue}); const float delta = maximum - std::min({red, green, blue}); next.mode = K80_MODE_HSI; next.saturation = maximum == 0 ? 0 : static_cast(std::floor( delta / maximum * static_cast(K80_SATURATION_MAX) + .5f)); if (delta != 0) { float hue; if (maximum == red) hue = 60 * (green - blue) / delta; else if (maximum == green) hue = 60 * (blue - red) / delta + 120; else hue = 60 * (red - green) / delta + 240; if (hue < 0) hue += 360; next.hue = static_cast(std::floor(hue + .5f)); } *output = next; return true; } ``` -------------------------------- ### Define configuration constants and built-in effect names Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/esphome-config-schema.md Defines configuration key constants and the tuple of built-in effect names. The effect names are attached to every generated position light as `{"yiscaxia_builtin": {CONF_NAME: name}}` effect entries, making them resolvable in `light.turn_on` actions and API calls. ```python CONF_TRANSPORT = "transport" CONF_NAME_PREFIX = "name_prefix" CONF_POSITION_CAPACITY = "position_capacity" CONF_INITIAL_PAIRS = "initial_pairs" CONF_AVAILABLE_PAIRS = "available_pairs" CONF_TRANSMISSION_ATTEMPTS = "transmission_attempts" CONF_CHANNEL_SPACING = "channel_spacing" CONF_RAINBOW_DEGREES_PER_STEP = "rainbow_degrees_per_step" CONF_RAINBOW_STEP_SPACING = "rainbow_step_spacing" CONF_POSITIONS = "positions" BUILTIN_EFFECT_NAMES: tuple[str, ...] = ( "SOS", "Lightning 1", "Lightning 2", "TV Screen", "Police", "Ambulance", "Fire Engine", "RGB Circle 1", "RGB Circle 2", "Custom Rainbow", ) ``` -------------------------------- ### k80_controller_request_brightness Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Brightness-only request for profiles that support K80_CONTROLLER_FIELD_BRIGHTNESS. Clamps the value to 0..1, quantizes it, and for native profiles forks the full state with the new level (preserving color/effect selections). Non-native profiles keep the budget when the pending level is identical; non-finite values are rejected before clamping. ```cpp static inline int k80_controller_request_brightness(k80_controller_queue *q, int endpoint, float value) { if (!q || (!q->armed && !q->draining) || endpoint < 0 || (size_t)endpoint >= q->count || !q->pending || !q->pending[endpoint].enabled || !k80_controller_profile_supports_field(q->pending[endpoint].profile, K80_CONTROLLER_FIELD_BRIGHTNESS) || !isfinite(value)) return 0; if (value < 0) value = 0; if (value > 1) value = 1; const int level = k80_quantize_brightness(value); k80_controller_pending *pending = &q->pending[endpoint]; if (k80_controller_profile_is_native(pending->profile)) { k80_control_values state = pending->state; state.level = level; return k80_controller_request_state(q, endpoint, &state); } if (pending->remaining && pending->state.level == level) return 1; pending->state.level = level; pending->remaining = q->attempts; return 1; } ``` -------------------------------- ### k80_controller_build_packet function Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-protocol.md Builds only the FIFO body (no preamble, RF ID, or hardware CRC) for the BRIGHTNESS profile: per-group captured seeds with level written into byte 3, then k80_finalize_frame. Returns 0 on success, -1 on invalid input without modifying output. ```cpp static inline int k80_controller_build_packet( int group, int level, uint8_t output[K80_FRAME_SIZE]) { // Captured zero-level prefixes, sequences 14, 20, 25, 48, 4, 9 in // k80-raw-frames-20260919T1817/index.jsonl (2026-09-19). static const uint8_t seeds[K80_GROUP_COUNT][9] = { {0x36, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x00, 0x64, 0x08}, {0x36, 0x01, 0x00, 0x00, 0x00, 0x04, 0x01, 0x64, 0x08}, {0x36, 0x02, 0x00, 0x00, 0x00, 0x37, 0x01, 0x64, 0x08}, {0x36, 0x03, 0x00, 0x00, 0x00, 0x78, 0x00, 0x64, 0x01}, {0x36, 0x04, 0x00, 0x00, 0x00, 0x78, 0x00, 0x64, 0x01}, {0x36, 0x05, 0x00, 0x00, 0x00, 0x78, 0x00, 0x64, 0x01}, }; if (output == NULL || group < 0 || group >= K80_GROUP_COUNT || level < 0 || level > K80_LEVEL_MAX) return -1; memcpy(output, seeds[group], sizeof(seeds[group])); output[3] = (uint8_t)level; k80_finalize_frame(output); return 0; } ``` -------------------------------- ### Request state: manual vs rainbow phase Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/yiscaxia-controller.md Shows how to request a one-shot state change versus a budgeted Rainbow phase. The manual call uses the attempt budget (default 3) while the automatic Rainbow phase makes exactly one attempt with no retries; failure indicates an endpoint disabled, queue disarmed, or invalid controls. ```cpp k80_control_values on = k80_default_controls(); on.mode = K80_MODE_HSI; on.level = 50; on.hue = 90; on.saturation = 100; // manual change: uses the configured attempt budget (default 3) bool accepted = controller->request_state(0, on, false); // automatic Rainbow phase: exactly one attempt, no retries bool phase = controller->request_state(0, on, true); if (!accepted) { // endpoint disabled, queue disarmed, or invalid controls — see errors.md } ``` -------------------------------- ### brightness_to_level: quantize brightness to wire level Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/state-conversion.md Converts a normalized brightness (0..1) into a quantized wire level (0..100) via k80_quantize_brightness. Returns false when level is null or value is non-finite/out of range; otherwise sets *level and returns true. ```cpp inline bool brightness_to_level(float value, int *level) { if (!level || !unit_value_valid(value)) return false; *level = k80_quantize_brightness(value); return true; } ``` -------------------------------- ### k80_controller_setting enum Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/k80-controller-scheduler.md Enumeration of configurable scheduler settings: attempt budget and channel spacing. ```cpp typedef enum { K80_CONTROLLER_SETTING_ATTEMPTS, K80_CONTROLLER_SETTING_CHANNEL_SPACING, } k80_controller_setting; ``` -------------------------------- ### a7105_probe_configure_rx_profile Source: https://github.com/xaionaro/esphome-yiscaxia/blob/main/_autodocs/api-reference/md7105-probe-protocol.md Configures a full receive profile for the A7105. ```APIDOC ## a7105_probe_configure_rx_profile ### Description Configures a full 18-register receive profile: 4-wire, MODE_CONTROL FIFO, easy FEP, RX/CODE/CLOCK/DATA_RATE/PLL/DELAY/RX_GAIN/RX_DEM. ### Signature ```cpp int a7105_probe_configure_rx_profile(const a7105_probe_bus_t *bus, uint8_t channel, const a7105_scan_profile_t *profile); ``` ### Parameters - **bus** (const a7105_probe_bus_t*) - Required - Pointer to the bus configuration. - **channel** (uint8_t) - Required - PLL channel to use. - **profile** (const a7105_scan_profile_t*) - Required - Pointer to the scan profile. ### Returns - (int) - Status code (0 on success, non-zero on error). ### Source `md7105_probe_protocol.h:410-452` ```