forked from xiaozhi/xiaozhi-esp32
162 lines
5.0 KiB
C++
162 lines
5.0 KiB
C++
|
|
#include "device_state_machine.h"
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <esp_log.h>
|
||
|
|
|
||
|
|
static const char* TAG = "StateMachine";
|
||
|
|
|
||
|
|
// State name strings for logging
|
||
|
|
static const char* const STATE_STRINGS[] = {
|
||
|
|
"unknown",
|
||
|
|
"starting",
|
||
|
|
"wifi_configuring",
|
||
|
|
"idle",
|
||
|
|
"connecting",
|
||
|
|
"listening",
|
||
|
|
"speaking",
|
||
|
|
"upgrading",
|
||
|
|
"activating",
|
||
|
|
"audio_testing",
|
||
|
|
"fatal_error",
|
||
|
|
"invalid_state"
|
||
|
|
};
|
||
|
|
|
||
|
|
DeviceStateMachine::DeviceStateMachine() {
|
||
|
|
}
|
||
|
|
|
||
|
|
const char* DeviceStateMachine::GetStateName(DeviceState state) {
|
||
|
|
if (state >= 0 && state <= kDeviceStateFatalError) {
|
||
|
|
return STATE_STRINGS[state];
|
||
|
|
}
|
||
|
|
return STATE_STRINGS[kDeviceStateFatalError + 1];
|
||
|
|
}
|
||
|
|
|
||
|
|
bool DeviceStateMachine::IsValidTransition(DeviceState from, DeviceState to) const {
|
||
|
|
// Allow transition to the same state (no-op)
|
||
|
|
if (from == to) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Define valid state transitions based on the state diagram
|
||
|
|
switch (from) {
|
||
|
|
case kDeviceStateUnknown:
|
||
|
|
// Can only go to starting
|
||
|
|
return to == kDeviceStateStarting;
|
||
|
|
|
||
|
|
case kDeviceStateStarting:
|
||
|
|
// Can go to wifi configuring or activating
|
||
|
|
return to == kDeviceStateWifiConfiguring ||
|
||
|
|
to == kDeviceStateActivating;
|
||
|
|
|
||
|
|
case kDeviceStateWifiConfiguring:
|
||
|
|
// Can go to activating (after wifi connected) or audio testing
|
||
|
|
return to == kDeviceStateActivating ||
|
||
|
|
to == kDeviceStateAudioTesting;
|
||
|
|
|
||
|
|
case kDeviceStateAudioTesting:
|
||
|
|
// Can go back to wifi configuring
|
||
|
|
return to == kDeviceStateWifiConfiguring;
|
||
|
|
|
||
|
|
case kDeviceStateActivating:
|
||
|
|
// Can go to upgrading, idle, or back to wifi configuring (on error)
|
||
|
|
return to == kDeviceStateUpgrading ||
|
||
|
|
to == kDeviceStateIdle ||
|
||
|
|
to == kDeviceStateWifiConfiguring;
|
||
|
|
|
||
|
|
case kDeviceStateUpgrading:
|
||
|
|
// Can go to idle (upgrade failed) or activating
|
||
|
|
return to == kDeviceStateIdle ||
|
||
|
|
to == kDeviceStateActivating;
|
||
|
|
|
||
|
|
case kDeviceStateIdle:
|
||
|
|
// Can go to connecting, listening (manual mode), speaking, activating, upgrading, or wifi configuring
|
||
|
|
return to == kDeviceStateConnecting ||
|
||
|
|
to == kDeviceStateListening ||
|
||
|
|
to == kDeviceStateSpeaking ||
|
||
|
|
to == kDeviceStateActivating ||
|
||
|
|
to == kDeviceStateUpgrading ||
|
||
|
|
to == kDeviceStateWifiConfiguring;
|
||
|
|
|
||
|
|
case kDeviceStateConnecting:
|
||
|
|
// Can go to idle (failed) or listening (success)
|
||
|
|
return to == kDeviceStateIdle ||
|
||
|
|
to == kDeviceStateListening;
|
||
|
|
|
||
|
|
case kDeviceStateListening:
|
||
|
|
// Can go to speaking or idle
|
||
|
|
return to == kDeviceStateSpeaking ||
|
||
|
|
to == kDeviceStateIdle;
|
||
|
|
|
||
|
|
case kDeviceStateSpeaking:
|
||
|
|
// Can go to listening or idle
|
||
|
|
return to == kDeviceStateListening ||
|
||
|
|
to == kDeviceStateIdle;
|
||
|
|
|
||
|
|
case kDeviceStateFatalError:
|
||
|
|
// Cannot transition out of fatal error
|
||
|
|
return false;
|
||
|
|
|
||
|
|
default:
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool DeviceStateMachine::CanTransitionTo(DeviceState target) const {
|
||
|
|
return IsValidTransition(current_state_.load(), target);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool DeviceStateMachine::TransitionTo(DeviceState new_state) {
|
||
|
|
DeviceState old_state = current_state_.load();
|
||
|
|
|
||
|
|
// No-op if already in the target state
|
||
|
|
if (old_state == new_state) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate transition
|
||
|
|
if (!IsValidTransition(old_state, new_state)) {
|
||
|
|
ESP_LOGW(TAG, "Invalid state transition: %s -> %s",
|
||
|
|
GetStateName(old_state), GetStateName(new_state));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Perform transition
|
||
|
|
current_state_.store(new_state);
|
||
|
|
ESP_LOGI(TAG, "State: %s -> %s",
|
||
|
|
GetStateName(old_state), GetStateName(new_state));
|
||
|
|
|
||
|
|
// Notify callback
|
||
|
|
NotifyStateChange(old_state, new_state);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
int DeviceStateMachine::AddStateChangeListener(StateCallback callback) {
|
||
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
||
|
|
int id = next_listener_id_++;
|
||
|
|
listeners_.emplace_back(id, std::move(callback));
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
void DeviceStateMachine::RemoveStateChangeListener(int listener_id) {
|
||
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
||
|
|
listeners_.erase(
|
||
|
|
std::remove_if(listeners_.begin(), listeners_.end(),
|
||
|
|
[listener_id](const auto& p) { return p.first == listener_id; }),
|
||
|
|
listeners_.end());
|
||
|
|
}
|
||
|
|
|
||
|
|
void DeviceStateMachine::NotifyStateChange(DeviceState old_state, DeviceState new_state) {
|
||
|
|
std::vector<StateCallback> callbacks_copy;
|
||
|
|
{
|
||
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
||
|
|
callbacks_copy.reserve(listeners_.size());
|
||
|
|
for (const auto& [id, cb] : listeners_) {
|
||
|
|
callbacks_copy.push_back(cb);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const auto& cb : callbacks_copy) {
|
||
|
|
cb(old_state, new_state);
|
||
|
|
}
|
||
|
|
}
|