pub mod cluster;
pub mod command;
pub mod endpoint;
pub mod ffi;
pub mod reporting;
pub mod scene;
pub mod signal;
use huesmith_core::hue::device::{HueDeviceType, PROFILE_HA};
use huesmith_core::hue::identity::DeviceIdentity;
use huesmith_core::light::state::LightState;
use core::cell::RefCell;
pub(crate) struct Config {
pub device_type: HueDeviceType,
pub identity: &'static DeviceIdentity,
pub mac: Option<[u8; 8]>,
pub channel: Option<u8>,
pub endpoint_id: u8,
}
static mut LIGHT_STATE: Option<RefCell<LightState>> = None;
static mut CONFIG: Option<&'static Config> = None;
static mut TRANSITION_ALARM_SCHEDULED: bool = false;
static mut IDENTIFY_ALARM_SCHEDULED: bool = false;
unsafe fn light_state() -> Option<&'static RefCell<LightState>> {
(*core::ptr::addr_of!(LIGHT_STATE)).as_ref()
}
unsafe fn device_config() -> Option<&'static Config> {
*core::ptr::addr_of!(CONFIG)
}
pub(crate) fn run(config: &'static Config, light_state: LightState, nvram_erase: bool) -> ! {
let initial_on = light_state.on;
let initial_level = light_state.brightness;
unsafe {
LIGHT_STATE = Some(RefCell::new(light_state));
CONFIG = Some(config);
}
let mut platform_cfg: ffi::esp_zb_platform_config_t = unsafe { core::mem::zeroed() };
platform_cfg.radio_config.radio_mode = ffi::RADIO_MODE_NATIVE;
platform_cfg.host_config.host_connection_mode = ffi::HOST_CONNECTION_MODE_NONE;
unsafe {
let ret = ffi::esp_zb_platform_config(&platform_cfg);
if ret != 0 {
log::error!("esp_zb_platform_config failed: {ret} — radio may not come up");
}
ffi::esp_zb_nvram_erase_at_start(nvram_erase);
ffi::esp_zb_io_buffer_size_set(254);
ffi::esp_zb_scheduler_queue_size_set(254);
}
let zb_cfg = ffi::esp_zb_cfg_t {
esp_zb_role: ffi::ZB_ROUTER,
install_code_policy: false,
nwk_cfg: ffi::esp_zb_nwk_cfg_t::zczr(10),
};
unsafe {
ffi::esp_zb_init(&zb_cfg);
}
if let Some(mac) = config.mac {
unsafe {
ffi::esp_zb_set_long_address(mac.as_ptr());
}
}
let mut mac_readback = [0u8; 8];
unsafe {
ffi::esp_zb_get_long_address(mac_readback.as_mut_ptr());
}
log::info!(
"IEEE MAC: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
mac_readback[7],
mac_readback[6],
mac_readback[5],
mac_readback[4],
mac_readback[3],
mac_readback[2],
mac_readback[1],
mac_readback[0],
);
log_struct_sizes();
log::info!(
"CONFIG: device={:?} identity={} {} endpoint={} channel={:?} custom_mac={}",
config.device_type,
config.identity.manufacturer_name,
config.identity.model_identifier,
config.endpoint_id,
config.channel,
config.mac.is_some(),
);
let channel_mask = config
.channel
.map(|ch| 1u32 << ch)
.unwrap_or(ffi::ALL_CHANNELS_MASK);
unsafe {
ffi::esp_zb_set_primary_network_channel_set(channel_mask);
ffi::esp_zb_enable_joining_to_distributed(true);
}
let mut tc_key: [u8; 16] = ffi::ZLL_TC_LINK_KEY;
unsafe {
ffi::esp_zb_secur_TC_standard_distributed_key_set(tc_key.as_mut_ptr());
}
let ep_list = unsafe { ffi::esp_zb_ep_list_create() };
endpoint::EndpointBuilder::new(config.endpoint_id, config.device_type, config.identity)
.profile_id(PROFILE_HA)
.initial_on(initial_on)
.initial_level(initial_level)
.register(ep_list);
unsafe {
let ret = ffi::esp_zb_device_register(ep_list);
if ret != 0 {
log::error!("esp_zb_device_register failed: {ret} — device will not pair");
}
}
verify_endpoint(config.endpoint_id, config.device_type);
unsafe {
signal::set_joined_hook(light_joined_hook);
ffi::esp_zb_core_action_handler_register(Some(action_handler_cb));
}
scene::register_privilege_commands(config.endpoint_id);
log::info!("Starting Zigbee stack...");
unsafe {
let ret = ffi::esp_zb_start(false);
if ret != 0 {
panic!("esp_zb_start failed: {}", ret);
}
}
log::info!("Entering Zigbee main loop");
unsafe {
ffi::esp_zb_stack_main_loop();
}
unreachable!()
}
#[no_mangle]
pub unsafe extern "C" fn esp_zb_app_signal_handler(signal_s: *mut ffi::esp_zb_app_signal_t) {
if signal_s.is_null() {
return;
}
let sig = &*signal_s;
if sig.p_app_signal.is_null() {
return;
}
let sig_type = *sig.p_app_signal;
let status = sig.esp_err_status;
let bdb_signal = signal::BdbSignal::from_raw(sig_type, status);
signal::handle_bdb_signal(bdb_signal);
}
unsafe fn light_joined_hook() {
if let Some(config) = device_config() {
reporting::setup_default_reporting(config.endpoint_id, config.device_type);
}
}
fn log_struct_sizes() {
log::info!(
"FFI sizes: cb_info={} set_attr_msg={} zcl_attr={} endpoint_cfg={}",
core::mem::size_of::<ffi::esp_zb_device_cb_common_info_t>(),
core::mem::size_of::<ffi::esp_zb_zcl_set_attr_value_message_t>(),
core::mem::size_of::<ffi::esp_zb_zcl_attribute_t>(),
core::mem::size_of::<ffi::esp_zb_endpoint_config_t>(),
);
}
fn verify_endpoint(ep: u8, device_type: HueDeviceType) {
let required: &[(u16, u16, &str)] = &[
(
ffi::CLUSTER_BASIC,
ffi::ATTR_BASIC_ZCL_VERSION,
"ZCLVersion",
),
(
ffi::CLUSTER_BASIC,
ffi::ATTR_BASIC_MANUFACTURER_NAME,
"ManufacturerName",
),
(
ffi::CLUSTER_BASIC,
ffi::ATTR_BASIC_MODEL_IDENTIFIER,
"ModelIdentifier",
),
(ffi::CLUSTER_ON_OFF, ffi::ATTR_ON_OFF, "OnOff"),
];
let level: &[(u16, u16, &str)] = &[(
ffi::CLUSTER_LEVEL_CONTROL,
ffi::ATTR_LEVEL_CURRENT_LEVEL,
"CurrentLevel",
)];
let color: &[(u16, u16, &str)] = &[
(
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_TEMPERATURE_MIREDS,
"ColorTempMireds",
),
(
ffi::CLUSTER_COLOR_CONTROL,
ffi::ATTR_COLOR_CAPABILITIES,
"ColorCapabilities",
),
];
let level_checks = if device_type.has_level_control() {
level.iter()
} else {
[].iter()
};
let color_checks = if device_type.has_color_control() {
color.iter()
} else {
[].iter()
};
for &(cluster, attr, name) in required.iter().chain(level_checks).chain(color_checks) {
let ptr = unsafe {
ffi::esp_zb_zcl_get_attribute(ep, cluster, ffi::ZB_ZCL_CLUSTER_SERVER_ROLE, attr)
};
if ptr.is_null() {
log::warn!(
"VERIFY EP{}: cluster 0x{:04X} attr 0x{:04X} ({}) = NULL",
ep,
cluster,
attr,
name
);
}
}
}
unsafe fn handle_identify_effect(message: *const core::ffi::c_void) {
if message.is_null() {
return;
}
let msg = &*(message as *const ffi::esp_zb_zcl_identify_effect_message_t);
log::info!(
"TriggerEffect: effect=0x{:02X} variant=0x{:02X}",
msg.effect_id,
msg.effect_variant,
);
let cmd = huesmith_core::light::command::LightCommand::TriggerEffect {
effect_id: msg.effect_id,
variant: msg.effect_variant,
};
if let Some(state) = light_state() {
state.borrow_mut().apply_command(&cmd);
}
}
unsafe extern "C" fn action_handler_cb(callback_id: u32, message: *const core::ffi::c_void) -> i32 {
match callback_id {
ffi::ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID => {
handle_set_attribute(message);
}
ffi::ESP_ZB_CORE_IDENTIFY_EFFECT_CB_ID => {
handle_identify_effect(message);
schedule_identify_if_active();
}
ffi::ESP_ZB_CORE_SCENES_STORE_SCENE_CB_ID => {
let has_color = device_config().is_some_and(|c| c.device_type.has_color_control());
return scene::handle_store_scene(message, light_state(), has_color);
}
ffi::ESP_ZB_CORE_SCENES_RECALL_SCENE_CB_ID => {
let has_color = device_config().is_some_and(|c| c.device_type.has_color_control());
let ret = scene::handle_recall_scene(message, light_state(), has_color);
schedule_transition_if_active();
return ret;
}
ffi::ESP_ZB_CORE_CMD_PRIVILEGE_COMMAND_REQ_CB_ID => {
return scene::handle_privilege_command(message);
}
_ => {
log::debug!("Unhandled Zigbee action: 0x{:04X}", callback_id);
}
}
0
}
unsafe fn schedule_transition_if_active() {
if let Some(state) = light_state() {
let state = state.borrow();
let active = state.transition_active();
let delay_ms = state.transition_step_interval_ms() as u32;
drop(state);
if active {
ensure_transition_step_scheduled(delay_ms);
}
}
}
unsafe fn handle_set_attribute(message: *const core::ffi::c_void) {
if message.is_null() {
log::warn!("Set attribute: NULL message");
return;
}
let msg_ptr = message as *const ffi::esp_zb_zcl_set_attr_value_message_t;
let msg = &*msg_ptr;
let status = msg.info.status;
let cluster = msg.info.cluster;
let dst_ep = msg.info.dst_endpoint;
let attr_id = msg.attribute.id;
let attr_type = { msg.attribute.data.type_ };
let attr_size = { msg.attribute.data.size };
let attr_value = { msg.attribute.data.value };
log::debug!(
"SET_ATTR: ep={} cluster=0x{:04X} attr=0x{:04X} type=0x{:02X} size={} status={}",
dst_ep,
cluster,
attr_id,
attr_type,
attr_size,
status,
);
if status != ffi::ZCL_STATUS_SUCCESS as u32 {
log::warn!("Set attribute failed: status {}", status);
return;
}
if attr_value.is_null() {
log::warn!("Set attribute: NULL value");
return;
}
if let Some(cmd) =
command::parse_set_attribute(cluster, attr_id, attr_size, attr_value as *const _)
{
log::info!("Command: {:?}", cmd);
let is_identify = matches!(
cmd,
huesmith_core::light::command::LightCommand::Identify { .. }
);
if let Some(state) = light_state() {
state.borrow_mut().apply_command(&cmd);
}
if is_identify {
schedule_identify_if_active();
} else {
schedule_transition_if_active();
}
}
}
unsafe extern "C" fn transition_step_cb(_param: u8) {
TRANSITION_ALARM_SCHEDULED = false;
if let Some(state) = light_state() {
let mut state = state.borrow_mut();
let still_active = state.step_transition();
let delay_ms = state.transition_step_interval_ms() as u32;
drop(state);
if still_active {
ensure_transition_step_scheduled(delay_ms);
}
}
}
unsafe fn ensure_transition_step_scheduled(delay_ms: u32) {
if TRANSITION_ALARM_SCHEDULED {
return;
}
TRANSITION_ALARM_SCHEDULED = true;
ffi::esp_zb_scheduler_alarm(transition_step_cb, 0, delay_ms.max(1));
}
unsafe extern "C" fn identify_step_cb(_param: u8) {
IDENTIFY_ALARM_SCHEDULED = false;
if let Some(state) = light_state() {
let mut state = state.borrow_mut();
let still_active = state.step_identify();
drop(state);
if still_active {
schedule_identify_step();
}
}
}
unsafe fn schedule_identify_step() {
if IDENTIFY_ALARM_SCHEDULED {
return;
}
IDENTIFY_ALARM_SCHEDULED = true;
ffi::esp_zb_scheduler_alarm(identify_step_cb, 0, 500); }
unsafe fn schedule_identify_if_active() {
if let Some(state) = light_state() {
let active = state.borrow().identify_active();
if active {
schedule_identify_step();
}
}
}