feat: core tests, bug fixes, API docs rewrite, HTTP compat routes

- Fix state_machine reset_state_progress: reset sequence index before
  validation to prevent out-of-bounds error on state transitions
- Fix video transformer test: use ±1 tolerance for OpenCV interpolation
- Add core integration tests (service_manager, dependencies, messages)
- Add HTTP compat routes (/index.html, POST /api/wifi/scan, hotspot aliases)
- Rewrite clients/docs/API.md to match actual implementation
- Fix BLE unused imports warning
- CEO task planning for next round (ConfigReload, playlist snapshot)

cargo check: 0 warnings, cargo test: 22/22 passed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
showen
2026-03-12 12:40:17 +08:00
parent 60488311d3
commit 5af7fc18a5
10 changed files with 924 additions and 306 deletions

View File

@@ -38,6 +38,7 @@ struct PendingWifiResponse {
pub(crate) struct HttpState {
wifi_response: Mutex<PendingWifiResponse>,
wifi_response_cv: Condvar,
last_wifi_result: Mutex<Option<String>>,
config: Mutex<Arc<AppConfig>>,
player_status: Mutex<crate::core::message::PlayerStatusData>,
ble_ready: AtomicBool,
@@ -62,6 +63,7 @@ impl HttpState {
payload: None,
}),
wifi_response_cv: Condvar::new(),
last_wifi_result: Mutex::new(None),
config: Mutex::new(config),
player_status: Mutex::new(player_status),
ble_ready: AtomicBool::new(false),
@@ -72,9 +74,22 @@ impl HttpState {
fn publish_wifi_result(&self, payload: String) {
if let Ok(mut state) = self.wifi_response.lock() {
state.version += 1;
state.payload = Some(payload);
state.payload = Some(payload.clone());
self.wifi_response_cv.notify_all();
}
if let Ok(mut last_wifi_result) = self.last_wifi_result.lock() {
*last_wifi_result = Some(payload.clone());
}
let ws_payload = match serde_json::from_str::<serde_json::Value>(&payload) {
Ok(value) => encode_ws_event("wifi_update", value),
Err(_) => encode_ws_event("wifi_update", serde_json::json!({ "raw": payload })),
};
if let Some(ws_payload) = ws_payload {
self.publish_ws(ws_payload);
}
}
pub(crate) fn config(&self) -> Arc<AppConfig> {
@@ -137,6 +152,19 @@ impl HttpState {
snapshots.push(payload);
}
if let Ok(last_wifi_result) = self.last_wifi_result.lock() {
if let Some(raw) = last_wifi_result.as_ref() {
let payload = match serde_json::from_str::<serde_json::Value>(raw) {
Ok(value) => encode_ws_event("wifi_update", value),
Err(_) => encode_ws_event("wifi_update", serde_json::json!({ "raw": raw })),
};
if let Some(payload) = payload {
snapshots.push(payload);
}
}
}
snapshots
}

View File

@@ -97,16 +97,24 @@ fn root_route() -> impl Filter<Extract = impl Reply, Error = warp::Rejection> +
warp::path::end()
.and(warp::get())
.map(|| warp::reply::html(WEB_UI_HTML))
.or(
warp::path("index.html")
.and(warp::path::end())
.and(warp::get())
.map(|| warp::reply::html(WEB_UI_HTML)),
)
}
fn ws_route(
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path("ws").and(warp::ws()).and(with_state(state)).map(
|ws: warp::ws::Ws, state: Arc<HttpState>| {
warp::path("ws")
.and(warp::path::end())
.and(warp::ws())
.and(with_state(state))
.map(|ws: warp::ws::Ws, state: Arc<HttpState>| {
ws.on_upgrade(move |socket| websocket_session(socket, state))
},
)
})
}
fn status_route(
@@ -343,11 +351,21 @@ fn wifi_scan_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
let tx_for_post = tx.clone();
let state_for_post = Arc::clone(&state);
warp::path!("api" / "wifi" / "scan")
.and(warp::get())
.and(with_tx(tx))
.and(with_state(state))
.and(with_state(Arc::clone(&state)))
.and_then(handle_wifi_scan)
.or(
warp::path!("api" / "wifi" / "scan")
.and(warp::post())
.and(with_tx(tx_for_post))
.and(with_state(state_for_post))
.and_then(handle_wifi_scan),
)
}
fn wifi_connect_route(
@@ -383,11 +401,14 @@ fn wifi_ap_start_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
let tx_for_hotspot = tx.clone();
let state_for_hotspot = Arc::clone(&state);
warp::path!("api" / "wifi" / "ap" / "start")
.and(warp::post())
.and(warp::body::bytes())
.and(with_tx(tx))
.and(with_state(state))
.and(with_state(Arc::clone(&state)))
.and_then(|body: bytes::Bytes, tx, state| async move {
let req: WifiApStartRequest = match parse_optional_json(&body) {
Ok(req) => req,
@@ -404,22 +425,60 @@ fn wifi_ap_start_route(
)
.await
})
.or(
warp::path!("api" / "wifi" / "hotspot" / "start")
.and(warp::post())
.and(warp::body::bytes())
.and(with_tx(tx_for_hotspot))
.and(with_state(state_for_hotspot))
.and_then(|body: bytes::Bytes, tx, state| async move {
let req: WifiApStartRequest = match parse_optional_json(&body) {
Ok(req) => req,
Err(reply) => return Ok::<_, Infallible>(*reply),
};
let ssid = req.ssid.unwrap_or_else(|| "showen".to_string());
let password = req.password.unwrap_or_else(|| "12345678".to_string());
let success_ssid = ssid.clone();
wifi_action_reply(
tx,
state,
WifiCommand::ApStart { ssid, password },
move |_| format!("AP 热点已启动: SSID={success_ssid}"),
)
.await
}),
)
}
fn wifi_ap_stop_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
let tx_for_hotspot = tx.clone();
let state_for_hotspot = Arc::clone(&state);
warp::path!("api" / "wifi" / "ap" / "stop")
.and(warp::post())
.and(with_tx(tx))
.and(with_state(state))
.and(with_state(Arc::clone(&state)))
.and_then(|tx, state| async move {
wifi_action_reply(tx, state, WifiCommand::ApStop, |_| {
"AP 热点已关闭".to_string()
})
.await
})
.or(
warp::path!("api" / "wifi" / "hotspot" / "stop")
.and(warp::post())
.and(with_tx(tx_for_hotspot))
.and(with_state(state_for_hotspot))
.and_then(|tx, state| async move {
wifi_action_reply(tx, state, WifiCommand::ApStop, |_| {
"AP 热点已关闭".to_string()
})
.await
}),
)
}
fn ble_start_route(
@@ -784,6 +843,7 @@ async fn wifi_request(
async fn websocket_session(ws: warp::ws::WebSocket, state: Arc<HttpState>) {
let (mut sender, mut receiver) = ws.split();
let mut events = state.ws_subscribe();
for payload in state.ws_snapshots() {
if sender.send(warp::ws::Message::text(payload)).await.is_err() {
@@ -791,8 +851,6 @@ async fn websocket_session(ws: warp::ws::WebSocket, state: Arc<HttpState>) {
}
}
let mut events = state.ws_subscribe();
loop {
tokio::select! {
event = events.recv() => {