pub mod actions;
pub mod advertise;
pub mod payload;
use anyhow::Result;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tracing::info;
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::protocol::Event;
use crate::state::app_state::AppState;
use crate::{console_error, console_info, console_warn};
use actions::{BluetoothBleBeaconProtocol, BEACON_STARTED_EVENT};
use advertise::BeaconAdvertiser;
use payload::BeaconFrame;
pub struct BeaconServer {
advertiser: Mutex<BeaconAdvertiser>,
status_tx: mpsc::UnboundedSender<String>,
}
impl BeaconServer {
pub async fn start_beacon(&self, frame: BeaconFrame) -> Result<String> {
let description = frame.describe();
let mut advertiser = self.advertiser.lock().await;
advertiser.start(frame).await?;
let adapter = advertiser.adapter_name().to_string();
drop(advertiser);
console_info!(
self.status_tx,
"Beacon advertising on {}: {}",
adapter,
description
);
Ok(description)
}
pub async fn stop_beacon(&self) -> Option<String> {
let mut advertiser = self.advertiser.lock().await;
let previous = advertiser.current().map(BeaconFrame::describe);
advertiser.stop().await;
drop(advertiser);
match &previous {
Some(what) => console_info!(self.status_tx, "Beacon stopped advertising: {}", what),
None => console_info!(self.status_tx, "Beacon was not advertising"),
}
previous
}
pub async fn current(&self) -> Option<BeaconFrame> {
self.advertiser.lock().await.current().cloned()
}
pub async fn adapter_name(&self) -> String {
self.advertiser.lock().await.adapter_name().to_string()
}
}
pub fn beacon_configuration_failure(
device_name: &str,
adapter: &str,
err: &anyhow::Error,
) -> String {
let (decision, overloaded) = if crate::llm::is_overload_error(err) {
(
"decision=fail_closed_llm_overloaded",
" The LLM backend was saturated rather than broken, so starting the server again may \
succeed.",
)
} else {
("decision=fail_closed_llm_error", "")
};
format!(
"BLE beacon '{device_name}' on adapter {adapter} could not be configured \
({decision}): the handler failed ({err}). NOTHING is being advertised and the server \
is not running - a beacon is its advertising payload, so there is no useful default \
to fall back to and inventing one would put an unattributable frame on the \
air.{overloaded}"
)
}
pub struct BluetoothBleBeacon;
impl BluetoothBleBeacon {
pub async fn spawn_with_llm_actions(
device_name: String,
adapter: Option<String>,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
instruction: String,
) -> Result<std::net::SocketAddr> {
info!("Starting BLE beacon server");
let advertiser = match BeaconAdvertiser::open(device_name.clone(), adapter).await {
Ok(advertiser) => advertiser,
Err(e) => {
console_error!(
status_tx,
"BLE beacon '{}' cannot start (decision=refused_adapter_unavailable): {}. \
NOTHING is being advertised and the server is not running.",
device_name,
e
);
return Err(e);
}
};
let adapter_name = advertiser.adapter_name().to_string();
console_info!(
status_tx,
"BLE beacon ready on adapter {} (device name '{}')",
adapter_name,
device_name
);
let server = Arc::new(BeaconServer {
advertiser: Mutex::new(advertiser),
status_tx: status_tx.clone(),
});
app_state
.register_server_handle(server_id, server.clone())
.await;
let protocol = BluetoothBleBeaconProtocol::new();
let started_event = Event::new(
&BEACON_STARTED_EVENT,
serde_json::json!({
"device_name": device_name,
"adapter": adapter_name,
"instruction": instruction,
}),
);
let result = match call_llm(
&llm_client,
&app_state,
server_id,
None, &started_event,
&protocol,
)
.await
{
Ok(result) => result,
Err(e) => {
server.stop_beacon().await;
let reason = beacon_configuration_failure(&device_name, &adapter_name, &e);
console_error!(status_tx, "{}", reason);
return Err(e.context(reason));
}
};
let refused: Vec<String> = result
.failures
.iter()
.map(|f| format!("{}: {}", f.action, f.error))
.collect();
let asked_to_stop = result
.raw_actions
.iter()
.any(|a| a.get("type").and_then(|v| v.as_str()) == Some("stop_beacon"));
let action_count = result.raw_actions.len();
match server.current().await {
Some(frame) => {
console_info!(
status_tx,
"BLE beacon '{}' on adapter {} is advertising (decision=model_answer): {}",
device_name,
adapter_name,
frame.describe()
);
}
None if !refused.is_empty() => {
console_error!(
status_tx,
"BLE beacon '{}' on adapter {} is idle \
(decision=fail_closed_bad_action): the handler answered, but no action it \
produced could be executed ({}). NOTHING is being advertised.",
device_name,
adapter_name,
refused.join("; ")
);
}
None if asked_to_stop => {
console_info!(
status_tx,
"BLE beacon '{}' on adapter {} is idle (decision=model_reject): the handler \
answered with stop_beacon, so nothing is advertised on purpose.",
device_name,
adapter_name
);
}
None => {
console_warn!(
status_tx,
"BLE beacon '{}' on adapter {} is idle (decision=model_silent): no \
start_ibeacon / start_eddystone_uid / start_eddystone_url action was \
produced ({} action(s) returned). NOTHING is being advertised; use one of \
those actions to begin broadcasting.",
device_name,
adapter_name,
action_count
);
}
}
Ok(std::net::SocketAddr::from((
std::net::Ipv4Addr::UNSPECIFIED,
0,
)))
}
}