# 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:

| Variable         | Required | Notes                                              |
| ---------------- | -------- | -------------------------------------------------- |
| `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

| Variant         | Input       | Screen    |
| --------------- | ----------- | --------- |
| `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.
