1#![deny(missing_docs)]
4
5use std::{
6 borrow::Cow,
7 error::Error,
8 fmt::Display,
9 io::{BufRead, BufReader},
10 path::Path,
11 process::{Child, Command, Stdio},
12 time::Duration,
13};
14
15use reqwest::{Client, ClientBuilder};
16use serde::{Deserialize, Serialize, ser::SerializeSeq};
17
18#[derive(Debug)]
22pub struct SpeculosClient {
23 process: Child,
24 port: u16,
25 client: Client,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum DeviceModel {
31 Nanos,
33 Nanox,
35 Nanosp,
37 Blue,
39 Stax,
41 Flex,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47pub struct AutomationRule<'a> {
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub text: Option<Cow<'a, str>>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub regexp: Option<Cow<'a, str>>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub x: Option<u32>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub y: Option<u32>,
60 pub conditions: &'a [AutomationCondition<'a>],
62 pub actions: &'a [AutomationAction<'a>],
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum AutomationAction<'a> {
69 Button {
71 button: Button,
73 pressed: bool,
75 },
76 Finger {
78 x: u32,
80 y: u32,
82 touched: bool,
84 },
85 Setbool {
87 varname: Cow<'a, str>,
89 value: bool,
91 },
92 Exit,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct AutomationCondition<'a> {
99 pub varname: Cow<'a, str>,
101 pub value: bool,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum Button {
108 Left,
110 Right,
112}
113
114#[derive(Debug)]
116pub enum SpeculosError {
117 IoError(std::io::Error),
119 ReqwestError(reqwest::Error),
121}
122
123#[derive(Serialize)]
124struct PostApduRequest<'a> {
125 #[serde(with = "hex")]
126 data: &'a [u8],
127}
128
129#[derive(Deserialize)]
130struct PostApduResponse {
131 #[serde(with = "hex")]
132 data: Vec<u8>,
133}
134
135#[derive(Serialize)]
136struct PostAutomationRequest<'a> {
137 version: u32,
138 rules: &'a [AutomationRule<'a>],
139}
140
141impl SpeculosClient {
142 pub fn new<P: AsRef<Path>>(
149 model: DeviceModel,
150 port: u16,
151 app: P,
152 ) -> Result<Self, SpeculosError> {
153 Self::new_with_timeout(model, port, app, Duration::from_secs(10))
154 }
155
156 pub fn new_with_timeout<P: AsRef<Path>>(
162 model: DeviceModel,
163 port: u16,
164 app: P,
165 timeout: Duration,
166 ) -> Result<Self, SpeculosError> {
167 let mut process = Command::new("speculos")
168 .args([
169 "--api-port",
170 &port.to_string(),
171 "--apdu-port",
172 "0",
173 "-m",
174 model.slug(),
175 "--display",
176 "headless",
177 &app.as_ref().display().to_string(),
178 ])
179 .stderr(Stdio::piped())
180 .spawn()?;
181
182 if let Some(stderr) = process.stderr.take() {
184 let reader = BufReader::new(stderr);
185 for line in reader.lines().map_while(Result::ok) {
186 if line.contains("launcher: using default app name & version") {
187 break;
188 }
189 }
190 }
191
192 Ok(Self {
193 process,
194 port,
195 client: ClientBuilder::new().timeout(timeout).build().unwrap(),
196 })
197 }
198
199 pub async fn apdu(&self, data: &[u8]) -> Result<Vec<u8>, SpeculosError> {
206 let response = self
207 .client
208 .post(format!("http://localhost:{}/apdu", self.port))
209 .json(&PostApduRequest { data })
210 .send()
211 .await?;
212 let body = response.json::<PostApduResponse>().await.unwrap();
213
214 Ok(body.data)
215 }
216
217 pub async fn automation(&self, rules: &[AutomationRule<'_>]) -> Result<(), SpeculosError> {
219 let response = self
220 .client
221 .post(format!("http://localhost:{}/automation", self.port))
222 .json(&PostAutomationRequest { version: 1, rules })
223 .send()
224 .await?;
225
226 response.error_for_status()?;
227 Ok(())
228 }
229}
230
231impl Drop for SpeculosClient {
232 fn drop(&mut self) {
233 let _ = self.process.kill();
234 }
235}
236
237impl DeviceModel {
238 pub const fn slug(&self) -> &'static str {
240 match self {
241 Self::Nanos => "nanos",
242 Self::Nanox => "nanox",
243 Self::Nanosp => "nanosp",
244 Self::Blue => "blue",
245 Self::Stax => "stax",
246 Self::Flex => "flex",
247 }
248 }
249}
250
251impl<'a> Serialize for AutomationCondition<'a> {
252 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
253 where
254 S: serde::Serializer,
255 {
256 let mut seq = serializer.serialize_seq(Some(2))?;
257 seq.serialize_element(&self.varname)?;
258 seq.serialize_element(&self.value)?;
259 seq.end()
260 }
261}
262
263impl<'a> Serialize for AutomationAction<'a> {
264 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
265 where
266 S: serde::Serializer,
267 {
268 match self {
269 Self::Button { button, pressed } => {
270 let mut seq = serializer.serialize_seq(Some(3))?;
271 seq.serialize_element("button")?;
272 seq.serialize_element(&match button {
273 Button::Left => 1,
274 Button::Right => 2,
275 })?;
276 seq.serialize_element(pressed)?;
277 seq.end()
278 }
279 Self::Finger { x, y, touched } => {
280 let mut seq = serializer.serialize_seq(Some(4))?;
281 seq.serialize_element("finger")?;
282 seq.serialize_element(&x)?;
283 seq.serialize_element(&y)?;
284 seq.serialize_element(touched)?;
285 seq.end()
286 }
287 Self::Setbool { varname, value } => {
288 let mut seq = serializer.serialize_seq(Some(3))?;
289 seq.serialize_element("setbool")?;
290 seq.serialize_element(varname)?;
291 seq.serialize_element(value)?;
292 seq.end()
293 }
294 Self::Exit => {
295 let mut seq = serializer.serialize_seq(Some(1))?;
296 seq.serialize_element("exit")?;
297 seq.end()
298 }
299 }
300 }
301}
302
303impl From<std::io::Error> for SpeculosError {
304 fn from(value: std::io::Error) -> Self {
305 Self::IoError(value)
306 }
307}
308
309impl From<reqwest::Error> for SpeculosError {
310 fn from(value: reqwest::Error) -> Self {
311 Self::ReqwestError(value)
312 }
313}
314
315impl Display for SpeculosError {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 match self {
318 Self::IoError(error) => write!(f, "{}", error),
319 Self::ReqwestError(error) => write!(f, "{}", error),
320 }
321 }
322}
323
324impl Error for SpeculosError {}