mod app_data_files;
mod token_storage;
use anyhow::{Context, Result};
use arora_bridge::{Bridge, DeviceInfo};
use arora_simple_data_store::SimpleDataStore;
use arora_studio_bridge_client::firestore_support::options::{
FirebaseEmulatorOptions, FirebaseOptions,
};
use arora_studio_bridge_client::zenoh::ZenohDeviceClient;
use log::{info, warn};
use crate::operator::Operator;
use app_data_files::ensure_app_data_dir;
const OWNER_LABEL: &str =
"Semio Studio owner UID(s), comma-separated — leave empty to skip (disables Studio support)";
const NAME_LABEL: &str = "Device name — leave empty for a generated name";
const MODEL_FAMILY_LABEL: &str = "Model family — optional, leave empty to skip";
pub(crate) async fn run_with_hal(hal: Box<dyn arora_hal::Hal>) -> Result<()> {
let frontend = crate::run::select_frontend();
let client = if frontend.interactive {
connect_with_operator(&*frontend.operator).await?
} else {
Some(connect().await?)
};
match client {
Some(client) => {
crate::run_with_frontend(hal, client, Box::new(SimpleDataStore::new()), frontend).await
}
None => {
info!("studio-bridge: skipped by operator (no owner) — running without Studio");
let bridge = crate::run::local_ws_bridge().await?;
crate::run_with_frontend(hal, bridge, Box::new(SimpleDataStore::new()), frontend).await
}
}
}
pub async fn connect_with_operator(operator: &dyn Operator) -> Result<Option<Box<dyn Bridge>>> {
let owners = resolve_owners(operator).await;
if owners.is_empty() {
return Ok(None);
}
let name = resolve_field(operator, "DEVICE_NAME", NAME_LABEL)
.await
.unwrap_or_else(generated_device_name);
let model_family = resolve_field(operator, "MODEL_FAMILY", MODEL_FAMILY_LABEL).await;
let info = DeviceInfo {
name: Some(name),
description: env_nonempty("DEVICE_DESCRIPTION"),
model_family,
hardware_version: env_nonempty("HARDWARE_VERSION"),
software_version: env_nonempty("SOFTWARE_VERSION"),
owners,
};
let client = build_studio_bridge().await?;
client
.update_device_info(Some(info))
.await
.context("failed to register device info with Studio")?;
info!("Registered device info with Studio");
Ok(Some(client))
}
pub async fn connect() -> Result<Box<dyn Bridge>> {
let client = build_studio_bridge().await?;
if let Some(info) = device_info_from_env() {
client
.update_device_info(Some(info))
.await
.context("failed to register device info with Studio")?;
info!("Registered device info with Studio");
}
Ok(client)
}
async fn build_studio_bridge() -> Result<Box<dyn Bridge>> {
let firebase_options = FirebaseOptions::from_env();
let firebase_emulator_options = FirebaseEmulatorOptions::from_env();
let endpoint_override = std::env::var("STUDIO_BRIDGE_ENDPOINT")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let app_data_dir = ensure_app_data_dir().context("could not create app data directory")?;
let key_path = app_data_dir.join("key");
let token_path = match std::env::var("IDENTITY_FILE") {
Ok(identity_file) => std::path::PathBuf::from(identity_file),
Err(_) => app_data_dir.join("refresh_token"),
};
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
let refresh_token = token_storage::load_token(&key_path, &token_path)
.ok()
.flatten();
if refresh_token.is_some() {
info!("Refresh token found");
} else {
warn!("No refresh token found");
}
let save_cb: Box<dyn FnMut(String) + Send + Sync> = {
let key_path = key_path.clone();
let token_path = token_path.clone();
Box::new(move |token: String| {
if let Err(e) = token_storage::save_token(&key_path, &token_path, &token) {
warn!("Failed to save refresh token to {:?}: {:?}", token_path, e);
}
})
};
let client = match endpoint_override {
Some(endpoint) => {
info!("Connecting to Semio Studio via Zenoh (endpoint: {endpoint})");
ZenohDeviceClient::new_endpoint(
&firebase_options,
Some(&firebase_emulator_options),
refresh_token,
Some(save_cb),
endpoint,
)
.await
}
None => {
info!("Connecting to Semio Studio via the baked-in bridge endpoint");
ZenohDeviceClient::new(
&firebase_options,
Some(&firebase_emulator_options),
refresh_token,
Some(save_cb),
)
.await
}
}
.map_err(|e| anyhow::anyhow!("failed to connect to Semio Studio via Zenoh: {e:?}"))?;
let client: Box<dyn Bridge> = Box::new(client);
Ok(client)
}
fn device_info_from_env() -> Option<arora_bridge::DeviceInfo> {
let owners: Vec<String> = std::env::var("DEVICE_OWNERS")
.ok()
.map(|s| {
s.split(',')
.map(|o| o.trim().to_string())
.filter(|o| !o.is_empty())
.collect()
})
.unwrap_or_default();
let info = arora_bridge::DeviceInfo {
name: std::env::var("DEVICE_NAME").ok(),
description: std::env::var("DEVICE_DESCRIPTION").ok(),
model_family: std::env::var("MODEL_FAMILY").ok(),
hardware_version: std::env::var("HARDWARE_VERSION").ok(),
software_version: std::env::var("SOFTWARE_VERSION").ok(),
owners,
};
let configured = info.name.is_some()
|| info.description.is_some()
|| info.model_family.is_some()
|| info.hardware_version.is_some()
|| info.software_version.is_some()
|| !info.owners.is_empty();
configured.then_some(info)
}
async fn resolve_owners(operator: &dyn Operator) -> Vec<String> {
let raw = match env_nonempty("DEVICE_OWNERS") {
Some(value) => Some(value),
None => operator.ask_text(OWNER_LABEL, false).await,
};
raw.map(|s| {
s.split(',')
.map(|o| o.trim().to_string())
.filter(|o| !o.is_empty())
.collect()
})
.unwrap_or_default()
}
async fn resolve_field(operator: &dyn Operator, env_key: &str, label: &str) -> Option<String> {
if let Some(value) = env_nonempty(env_key) {
return Some(value);
}
operator
.ask_text(label, false)
.await
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
fn env_nonempty(key: &str) -> Option<String> {
std::env::var(key)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
fn generated_device_name() -> String {
format!("arora-device-{}", random_device_suffix())
}
fn random_device_suffix() -> String {
use std::hash::{BuildHasher, Hasher};
let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
hasher.write_u64(std::process::id() as u64);
let mut value = hasher.finish();
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
let mut suffix = String::with_capacity(8);
for _ in 0..8 {
suffix.push(ALPHABET[(value % 36) as usize] as char);
value /= 36;
}
suffix
}