Save ble adapter in state

- start to use state
- move to Tauri windows
- remove old js
main
lambda 1 month ago
parent 17c1aafd5e
commit a498e4edea

@ -1,5 +1,5 @@
use btleplug::api::{Central, Manager as _, Peripheral, ScanFilter, bleuuid::uuid_from_u16};
use btleplug::platform::Manager;
use btleplug::api::{Central, Peripheral, ScanFilter, bleuuid::uuid_from_u16};
use btleplug::platform::Adapter;
use std::time::Duration;
use tokio::time;
use crate::ble::error::BleError;
@ -16,18 +16,15 @@ static TARGET_UUIDS: &[Uuid] = &[
uuid_from_u16(0x1818), // Power meter
];
pub async fn scan_devices(scan_seconds: u64) -> Result<Vec<Device>, BleError> {
let manager = Manager::new().await?;
let adapters = manager.adapters().await?;
let central = adapters.into_iter().next().ok_or(BleError::SystemBleError)?;
pub async fn scan_devices(adapter: &Adapter, scan_seconds: u64) -> Result<Vec<Device>, BleError> {
let target_vec: Vec<Uuid> = TARGET_UUIDS.to_vec();
central.start_scan(ScanFilter { services: target_vec.clone() }).await?;
adapter.start_scan(ScanFilter { services: target_vec.clone() }).await?;
time::sleep(Duration::from_secs(scan_seconds)).await;
let mut devices = Vec::new();
for peripheral in central.peripherals().await? {
for peripheral in adapter.peripherals().await? {
if let Some(props) = peripheral.properties().await? {
let svc_match = props.services.iter().any(|s| TARGET_UUIDS.iter().any(|t| t == s));
if !svc_match { continue; }
@ -75,7 +72,7 @@ pub async fn scan_devices(scan_seconds: u64) -> Result<Vec<Device>, BleError> {
}
}
central.stop_scan().await.ok();
adapter.stop_scan().await.ok();
println!("{:?}", devices);
Ok(devices)
}

@ -1,14 +0,0 @@
#[derive(Debug)]
pub enum RepoError {
Creation,
Empty,
Serialization
}
impl From<std::io::Error> for RepoError {
fn from(_: std::io::Error) -> Self { RepoError::Creation }
}
impl From<serde_json::Error> for RepoError {
fn from(_: serde_json::Error) -> Self { RepoError::Serialization }
}

@ -1,4 +1,3 @@
mod repo;
mod error;
pub use repo::{try_from_file, try_write, repo_from_devices, Repo};
pub use repo::repo_from_devices;

@ -1,6 +1,5 @@
use std::{fs::{read_to_string, write}};
use serde::{Deserialize, Serialize};
use crate::{device::{device::{get_device_types, DeviceType}, Device}, device_repo::error::RepoError};
use crate::{device::{device::{get_device_types, DeviceType}, Device}};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Repo {
@ -22,24 +21,6 @@ impl Repo {
}
}
const STATIC_PATH: &str = "devices.json";
pub fn try_write(repo: &Repo) -> Result<Repo, RepoError> {
let json = serde_json::to_string_pretty(repo)?;
write(STATIC_PATH, json)?;
Ok(repo.clone())
}
pub fn try_from_file() -> Result<Repo, RepoError> {
let contents = match read_to_string(STATIC_PATH) {
Ok(s) if !s.trim().is_empty() => s,
_ => return Err(RepoError::Empty),
};
let repo: Repo = serde_json::from_str(&contents)?;
Ok(repo)
}
pub fn repo_from_devices(devices: Vec<Device>) -> Repo {
let mut repo = Repo::new();

@ -1,44 +1,47 @@
mod device;
mod ble;
mod device_repo;
mod state;
use tauri::{AppHandle, Manager};
use crate::{ble::scan_devices, device_repo::{repo_from_devices, try_from_file, Repo}};
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
async fn read_repo() -> Repo {
match try_from_file() {
Ok(repo) => repo,
Err(_) => Repo::new()
}
}
#[tauri::command]
async fn scan() -> Repo {
println!("kek");
repo_from_devices((scan_devices(5).await).unwrap_or_default())
}
use std::sync::Arc;
use tauri::{AppHandle, Emitter, Manager, async_runtime};
use crate::{ble::scan_devices, device_repo::{repo_from_devices}};
use crate::state::State;
use btleplug::api::{Manager as ApiManager};
use btleplug::platform::Manager as BleManager;
#[tauri::command]
async fn init(app: AppHandle) -> Result<(),()> {
println!("init rideinn app");
let splash = app.get_webview_window("splashscreen").unwrap();
let connect = app.get_webview_window("connect").unwrap();
let repo = repo_from_devices((scan_devices(5).await).unwrap_or_default());
let state = app.state::<State>();
let adapter = state.adapter.clone();
let repo = repo_from_devices((scan_devices(&adapter, 5).await).unwrap_or_default());
splash.close().unwrap();
connect.show().unwrap();
app.emit_to("connect", "devices-loaded", repo).unwrap();
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![init, read_repo, scan])
.setup(|app| {
async_runtime::block_on(async move {
let manager = BleManager::new().await?;
let adapters = manager.adapters().await?;
let adapter = adapters.into_iter().next()
.ok_or_else(|| Box::<dyn std::error::Error>::from("No BLE adapter found"))?;
app.manage(State { adapter: Arc::new(adapter) } );
Ok::<(), Box<dyn std::error::Error>>(())
})
})
.invoke_handler(tauri::generate_handler![init])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

@ -0,0 +1,6 @@
use btleplug::platform::Adapter;
use std::sync::Arc;
pub struct State {
pub adapter: Arc<Adapter>
}

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri App</title>
<script type="module" src="/connect.js" defer></script>
</head>
<body>
<main class="container">
<h1>Connect</h1>
<div id="app">
</div>
</main>
</body>
</html>

@ -0,0 +1,7 @@
const { once } = window.__TAURI__.event;
const { getCurrentWebviewWindow } = window.__TAURI__.webviewWindow;
const appWebview = getCurrentWebviewWindow();
appWebview.once("devices-loaded", (devices) => {
console.log("Devices are loaded", devices);
});

@ -0,0 +1,7 @@
const { invoke } = window.__TAURI__.core;
async function init() {
return await invoke("init");
}
window.addEventListener("DOMContentLoaded", init);

@ -1,53 +1 @@
import { EVENTS } from "./state.js";
import { fsm } from "./ui.js";
const { invoke } = window.__TAURI__.core;
async function readRepo() {
return await invoke("read_repo");
}
async function scan() {
return await invoke("scan");
}
const isArray = (maybeArray) =>
maybeArray?.constructor && maybeArray?.constructor === Array;
const isEmptyArray = (maybeArray) =>
isArray(maybeArray) && maybeArray.length === 0;
const getClosestDevices = (repo) => {
const sortByRssi = (d1, d2) => d1.rssi > d2.rssi;
const cadSensors = repo.cad_sensors.sort(sortByRssi);
const hrSensors = repo.hr_sensors.sort(sortByRssi);
const trainers = repo.trainers.sort(sortByRssi);
return [hrSensors[0], cadSensors[0], trainers[0]];
};
const isRepoEmpty = (repo) =>
(!repo.cad_sensors || isEmptyArray(repo.cad_sensors)) &&
(!repo.hr_sensors || isEmptyArray(repo.hr_sensors)) &&
(!repo.trainers || isEmptyArray(repo.trainers));
window.addEventListener("DOMContentLoaded", async () => {
console.log("hello from main js");
const [savedDevices, scannedDevices] = await Promise.all([
readRepo(),
scan(),
]);
console.log(savedDevices, scannedDevices);
if (isRepoEmpty(savedDevices)) {
const closest = getClosestDevices(scannedDevices);
if (closest.length === 0) fsm.dispatch(EVENTS.TO_ERROR);
} else {
if (isRepoEmpty(scannedDevices)) {
fsm.dispatch(EVENTS.TO_MANUAL_CONNECT);
} else {
fsm.dispatch(EVENTS.TO_READY);
}
}
});
console.log("hello from main.js");

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri App</title>
<script type="module" src="/init.js" defer></script>
</head>
<body>
<main class="container">
<h1>SplashScreen</h1>
<div id="app">
</div>
</main>
</body>
</html>

@ -1,58 +0,0 @@
const STATES = Object.freeze({
INITIAL: "initial",
SEARCH: "search",
MANUAL_CONNECT: "manualConnect",
READY_TO_ROLL: "ready",
ERROR: "error",
});
const EVENTS = Object.freeze({
TO_SEARCH: "toSearch",
TO_MANUAL_CONNECT: "toManualConnect",
TO_READY: "toReady",
TO_ERROR: "toError",
});
const transitions = new Map([
[[STATES.INITIAL, EVENTS.TO_SEARCH], STATES.SEARCH],
[[STATES.INITIAL, EVENTS.TO_ERROR], STATES.ERROR],
[[STATES.SEARCH, EVENTS.TO_MANUAL_CONNECT], STATES.MANUAL_CONNECT],
[[STATES.SEARCH, EVENTS.TO_READY], STATES.READY_TO_ROLL],
[[STATES.MANUAL_CONNECT, EVENTS.TO_READY], STATES.READY_TO_ROLL],
[[STATES.MANUAL_CONNECT, EVENTS.TO_ERROR], STATES.ERROR],
]);
function createFSM(initialState = STATES.INITIAL) {
const fsm = new EventTarget();
let state = initialState;
fsm.getState = () => state;
fsm.dispatch = (eventName) => {
const key = JSON.stringify([state, eventName]);
const next = [...transitions.entries()].find(
([k]) => JSON.stringify(k) === key,
)?.[1];
if (next) {
const prev = state;
state = next;
fsm.dispatchEvent(
new CustomEvent("statechange", {
detail: { prev, next, trigger: eventName },
}),
);
return true;
}
console.warn(`No transition from "${state}" on "${eventName}"`);
return false;
};
return fsm;
}
export { createFSM, STATES, EVENTS };

@ -1,32 +0,0 @@
import { createFSM, STATES } from "./state.js";
const fsm = createFSM();
const container = document.getElementById("app");
const pages = {
[STATES.INITIAL]: `
<h1>INITIAL</h1>
`,
[STATES.SEARCH]: `
<h1>SEARCH</h1>
`,
[STATES.MANUAL_CONNECT]: `
<h1>MANUAL CONNECT</h1>
`,
[STATES.ERROR]: `
<h1>ERROR</h1>
`,
};
function render(state) {
container.innerHTML = pages[state] ?? "<p>Unknown state</p>";
}
fsm.addEventListener("statechange", (e) => {
const { next } = e.detail;
render(next);
});
render(fsm.getState());
export { fsm };
Loading…
Cancel
Save