# speculos
Rust wrapper for the [Ledger Speculos](https://github.com/LedgerHQ/speculos) device
emulator.
Spawns the `speculos` Python binary as a child process, exposes its REST API for
reading the screen, and drives input (buttons on Nano, touch on Stax/Flex).
## Prerequisites
You must build and install Speculos yourself. Follow the instructions in the
upstream repo:
- https://github.com/LedgerHQ/speculos
After building, the `speculos` command must be available on your `PATH`. This crate
does not bundle, download, or otherwise provide the emulator binary.
You also need an app `.elf` to run inside the emulator (typically built from a
Ledger app project, or downloaded from a Ledger app release).
## Usage
### Launch with auto-assigned ports
```rust
use speculos::{Speculos, Model, Button};
use std::path::Path;
let sim = Speculos::launch(Path::new("/path/to/app.elf"), Model::NanoX)?;
// Send your APDU traffic to sim.apdu_addr() with whatever transport you use.
let apdu = sim.apdu_addr();
// Drive the UI.
sim.press(Button::Right)?;
sim.press(Button::Both)?;
# Ok::<(), speculos::Error>(())
```
### Specify ports, seed, etc.
```rust
use speculos::{Speculos, Model, SpawnOptions};
use std::net::SocketAddr;
use std::path::Path;
let sim = Speculos::launch_with(
Path::new("/path/to/app.elf"),
Model::Stax,
SpawnOptions {
seed: Some("abandon abandon abandon abandon abandon abandon abandon abandon
abandon abandon abandon about".into()),
apdu: Some("127.0.0.1:9999".parse::<SocketAddr>().unwrap()),
api: Some("127.0.0.1:5000".parse::<SocketAddr>().unwrap()),
},
)?;
# Ok::<(), speculos::Error>(())
```
For a raw hex seed, prefix with `hex:` (Speculos's own convention):
```rust
# use speculos::SpawnOptions;
let opts = SpawnOptions {
seed: Some("hex:0123456789abcdef...".into()),
..Default::default()
};
```
### Read screen text
```rust
# use speculos::{Speculos, Model};
# use std::path::Path;
# use std::time::Duration;
# let sim = Speculos::launch(Path::new("/x"), Model::NanoX)?;
use regex::Regex;
let ev = sim.wait_for(&Regex::new("Address").unwrap(), Duration::from_secs(5))?;
println!("found: {}", ev.text);
# Ok::<(), speculos::Error>(())
```
### Touchscreen input (Stax / Flex)
```rust
# use speculos::{Speculos, Model};
# use std::path::Path;
# let sim = Speculos::launch(Path::new("/x"), Model::Stax)?;
sim.tap(200, 600)?;
sim.drag((200, 500), (200, 100))?;
# Ok::<(), speculos::Error>(())
```
### Screenshots
```rust
# use speculos::{Speculos, Model};
# use std::path::Path;
# let sim = Speculos::launch(Path::new("/x"), Model::NanoX)?;
sim.screenshot_to_file(Path::new("screen.png"))?;
# Ok::<(), speculos::Error>(())
```
### Attach to an already-running speculos
```rust
use speculos::{Speculos, Model};
let sim = Speculos::attach(
Model::NanoX,
"127.0.0.1:9999".parse().unwrap(),
"127.0.0.1:5000".parse().unwrap(),
)?;
// Drop will NOT kill the external process.
# Ok::<(), speculos::Error>(())
```
### Configure from environment
`Speculos::from_env()` reads:
| `SPECULOS_ELF` | yes | Path to the app `.elf`. |
| `SPECULOS_MODEL` | yes | `nanox`/`nanosp`/`stax`/`flex` (case-insensitive). |
| `SPECULOS_BIN` | no | Use a non default `speculos` executable |
| `SPECULOS_APDU` | no | `host:port` for APDU socket. Free if unset. |
| `SPECULOS_API` | no | `host:port` for REST API. Free if unset. |
| `SPECULOS_SEED` | no | Forwarded to `--seed` |
`SPECULOS_APDU` and `SPECULOS_API` must both be set or both unset.
### Override the speculos binary
By default the crate spawns `speculos` from `PATH`. To use a different
executable (absolute path, alternate name, wrapper script), set
`SPECULOS_BIN`. This applies to every launch entry point.
## Models
| `Model::NanoX` | buttons | 128x64 |
| `Model::NanoSP` | buttons | 128x64 |
| `Model::Stax` | touchscreen | 400x672 |
| `Model::Flex` | touchscreen | 480x600 |
`press()` is buttons-only; `tap()`, `drag()`, `press_at()`, `release_at()`,
`move_to()` are touchscreen-only. Calling the wrong family returns
`Error::TouchscreenOnly` or `Error::ButtonsOnly`.
## Lifetime
`Speculos::launch*` owns the child process. Dropping the handle kills it.
`Speculos::attach` does not, so dropping leaves the process alive.
If port selection races (another process grabs the port between `pick_free_port` and
Speculos's bind), `launch_with` retries up to three times with fresh ports, as long
as at least one port was auto-picked.