1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! BluFi (box-level): provision ESP32 devices onto WiFi over BLE, using the
//! box's Bluetooth adapter and Espressif's BluFi protocol.
//!
//! Every action except `scan` connects to the target by its advertised BLE
//! name and negotiates BluFi security first, so budgets are wide;
//! provisioning end-to-end (BLE connect + credential transfer + the target
//! joining WiFi) routinely takes 30 s+.
use super::box_handle;
pub use crate::wire::{BleDevice, BlufiDeviceInfo, BlufiNetwork, BlufiProvisionResult, BlufiStatus};
pub(crate) mod ops {
use std::time::Duration;
use serde_json::json;
use crate::error::Result;
use crate::wire::{
box_command, value_as, value_list_field, BleDevice, BlufiDeviceInfo, BlufiNetwork,
BlufiProvisionResult, BlufiStatus, CommandResponse, Op, Timeout,
};
const PATH: &str = "/blufi/command";
/// Box-side BLE connect timeout (s), matching the CLI default.
const CONNECT_TIMEOUT_S: f64 = 20.0;
fn parse_devices(resp: CommandResponse) -> Result<Vec<BleDevice>> {
value_list_field(resp, "devices")
}
fn parse_networks(resp: CommandResponse) -> Result<Vec<BlufiNetwork>> {
value_list_field(resp, "networks")
}
fn budget(box_side: f64) -> Timeout {
Timeout::After(Duration::from_secs_f64(box_side + 40.0))
}
pub(crate) fn scan(timeout: f64) -> Op<Vec<BleDevice>> {
Op {
req: box_command(PATH, "scan", json!({ "timeout": timeout }), budget(timeout)),
parse: parse_devices,
}
}
pub(crate) fn connect(device_name: &str) -> Op<BlufiDeviceInfo> {
Op {
req: box_command(
PATH,
"connect",
json!({ "device_name": device_name, "timeout": CONNECT_TIMEOUT_S }),
budget(CONNECT_TIMEOUT_S),
),
parse: value_as::<BlufiDeviceInfo>,
}
}
pub(crate) fn provision(
device_name: &str,
ssid: &str,
password: &str,
) -> Op<BlufiProvisionResult> {
Op {
req: box_command(
PATH,
"provision",
json!({
"device_name": device_name,
"ssid": ssid,
"password": password,
"timeout": CONNECT_TIMEOUT_S,
}),
// Connect + security + credential transfer + the target
// joining WiFi + status polling.
budget(CONNECT_TIMEOUT_S + 60.0),
),
parse: value_as::<BlufiProvisionResult>,
}
}
pub(crate) fn wifi_scan(device_name: &str) -> Op<Vec<BlufiNetwork>> {
// The target runs its own WiFi scan (box default 15s) after the BLE
// connect completes.
Op {
req: box_command(
PATH,
"wifi_scan",
json!({ "device_name": device_name, "timeout": CONNECT_TIMEOUT_S }),
budget(CONNECT_TIMEOUT_S + 30.0),
),
parse: parse_networks,
}
}
pub(crate) fn status(device_name: &str) -> Op<BlufiStatus> {
Op {
req: box_command(
PATH,
"status",
json!({ "device_name": device_name, "timeout": CONNECT_TIMEOUT_S }),
budget(CONNECT_TIMEOUT_S),
),
parse: value_as::<BlufiStatus>,
}
}
pub(crate) fn version(device_name: &str) -> Op<Option<String>> {
fn parse_version(resp: CommandResponse) -> Result<Option<String>> {
#[derive(serde::Deserialize)]
struct V {
#[serde(default)]
version: Option<String>,
}
let v: V = value_as(resp)?;
Ok(v.version)
}
Op {
req: box_command(
PATH,
"version",
json!({ "device_name": device_name, "timeout": CONNECT_TIMEOUT_S }),
budget(CONNECT_TIMEOUT_S),
),
parse: parse_version,
}
}
}
box_handle! {
/// Handle for BluFi provisioning over the box's Bluetooth adapter
/// (from [`crate::LagerBox::blufi`]).
sync: Blufi,
async: AsyncBlufi,
methods: {
/// Scan for BluFi-capable BLE devices for `timeout` seconds.
fn scan(timeout: f64) -> Vec<BleDevice> = ops::scan;
/// Connect to a target by its advertised BLE name; returns its
/// firmware version and WiFi state.
fn connect(device_name: &str) -> BlufiDeviceInfo = ops::connect;
/// Provision a target onto a WiFi network. Fails (as an
/// [`crate::Error::Box`]) when the target does not reach the
/// connected state.
fn provision(device_name: &str, ssid: &str, password: &str)
-> BlufiProvisionResult = ops::provision;
/// Ask the target to scan for WiFi networks it can see.
fn wifi_scan(device_name: &str) -> Vec<BlufiNetwork> = ops::wifi_scan;
/// Read the target's current WiFi state.
fn status(device_name: &str) -> BlufiStatus = ops::status;
/// Read the target's BluFi firmware version.
fn version(device_name: &str) -> Option<String> = ops::version;
}
}