Metadata-Version: 2.5
Name: bsln
Version: 0.2.1a1
Summary: Baseline Robotics client: Python SDK and command line to drive remote robot stations
Author: Baseline Robotics
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: Development Status :: 3 - Alpha
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: msgpack>=1.0
Requires-Dist: numpy>=1.26
Requires-Dist: pydantic>=2.7
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Requires-Dist: websockets>=15.0
Provides-Extra: viz
Requires-Dist: rerun-sdk>=0.32.2; extra == 'viz'
Description-Content-Type: text/markdown

# bsln

Python SDK and command line for Baseline Robotics robot stations. Drive a real robot station from your own machine:
stream joint targets from your script, get a report of each session and download the recordings.

This package contains only the client. The server and the station software are not part of it.

This guide shows you how to control a Baseline Robotics robot station from your computer with Python.
You install the `bsln` client, connect with your token and run a short script on a bimanual YAM station.
The script turns one wrist joint by 0.1 rad and then turns it back. 0.1 rad is about 6 degrees.

This guide is for bsln 0.2.1a1, an alpha version. Baseline Robotics also gives you two more guides:
`CLI.md` explains every command, and `SDK.md` explains the Python SDK and how to drive a station from your own policy.

## 1. Install

You install `bsln` with uv, the Python package manager from Astral. uv also installs the correct Python version for you.
If you do not have uv yet, install it:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

For Windows and for the other ways to install uv, see https://docs.astral.sh/uv/getting-started/installation/.

Make a project folder for your scripts, with Python 3.12:

```bash
uv init --python 3.12 bsln-quickstart
```

```bash
cd bsln-quickstart
```

Add `bsln` to the project. bsln is an alpha version, so give its version number:

```bash
uv add bsln==0.2.1a1
```

To watch your sessions live in 3D, also add the viewer extra. It adds the Rerun viewer, which is a large download:

```bash
uv add 'bsln[viz]==0.2.1a1'
```

Activate the project environment. Then the `bsln` command and `python` use this project. Do this in each new terminal, in the project folder:

```bash
source .venv/bin/activate
```

Make sure that you have the correct version:

```bash
uv tree --depth 1
```

**If it works,** the last line is `└── bsln v0.2.1a1`. With the viewer extra, it is `└── bsln[viz] v0.2.1a1`.

**If it fails:**

- `uv: command not found`: open a new terminal, so that it finds uv. If it still fails, install uv again.
- `uv add bsln==0.2.1a1` stops with `No solution found when resolving dependencies`: the project allows a Python older than 3.10.
  Make the project again with `--python 3.12`.
- `bsln: command not found`: the project environment is not active. Go to the project folder and run `source .venv/bin/activate`.

## 2. Get access

Baseline Robotics gives you a bsln token. The token starts with `bsln_`.
Keep the token secret. Do not put it in your code or in a Git repository.

Connect in one of these two ways.

**Way A: log in once.** The command asks for your token. It does not show the token while you type.
It saves the URL and the token in `~/.config/bsln/config.json`. Only your user can read this file.

```bash
bsln login --url https://api.baseline-robotics.com
```

Always give `--url`. Without it, `bsln login` uses `http://localhost:8000`.

**If it works,** you see this line:

```text
logged in as <your name> (control, read, write) - saved to /Users/you/.config/bsln/config.json
```

**Way B: environment variables.** Set them in each new terminal. They win over the values that `bsln login` saved.

```bash
export BSLN_URL=https://api.baseline-robotics.com
```

```bash
export BSLN_TOKEN=<your token>
```

Make sure that the connection works:

```bash
bsln whoami
```

**If it works,** you see your user name and your scopes:

```text
{
  "user_id": "usr_7ca6246ae264090c",
  "name": "customer",
  "scopes": [
    "control",
    "read",
    "write"
  ],
  "is_admin": false
}
```

The scopes decide what you can do. `read` lets you list, watch and download. `write` lets you upload. `control` lets you move a robot.

**If it fails:**

- `error 401: missing or invalid token`: the token is wrong, has expired or is not set. Check `BSLN_TOKEN`, or run `bsln login` again.
- `cannot reach the bsln server at ...`: check the URL and your network. A company VPN or proxy can block the connection.

**Extra tokens.** You can make more tokens for other computers or scripts.
This command makes a token that is valid for 30 days. It shows the token one time only, so keep it safe.

```bash
python -c "from bsln.sdk import Client; print(Client().create_token(['read', 'write', 'control'], name='second-computer', ttl_days=30)['token'])"
```

An extra token cannot have more scopes than your token. It expires before your token or at the same time.
To stop a token before it expires, contact Baseline Robotics.

## 3. Find a station

```bash
bsln stations
```

**If it works,** you see a table with one row for each station:

```text
┏━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ id     ┃ robot_type   ┃ status ┃ fault ┃ cameras          ┃ inventory                      ┃ agent_version ┃
┡━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ yam-01 │ yam_bimanual │ idle   │       │ top, left, right │ ball_tennis, bowl_white,       │ 0.2.0         │
│        │              │        │       │                  │ cube_blue_3cm, cube_red_3cm,   │               │
│        │              │        │       │                  │ tray_black                     │               │
└────────┴──────────────┴────────┴───────┴──────────────────┴────────────────────────────────┴───────────────┘
```

Use the `id` in all other commands. `inventory` lists the objects at the station.

You can start a session only on a station with the status `idle`. The other status values are:

- `busy`: another session or run uses the station.
- `fault`: the station stopped after an e-stop or a safety fault. The `fault` column shows why.
- `offline`: the station is not connected.

## 4. Run your first session

Save this script as `quickstart.py`.

```python
"""bsln quickstart: start a session, turn the left wrist a little and back, then end the session.

    export BSLN_URL=https://api.baseline-robotics.com
    export BSLN_TOKEN=<your bsln token>
    python quickstart.py <station id>
"""

import sys
import time

from bsln.sdk import Client

STATION = sys.argv[1]
WRIST = "left_joint6"  # the left wrist. Unit: rad.
TURN = 0.1  # rad. This is about 6 degrees.

client = Client()  # reads BSLN_URL and BSLN_TOKEN, or the values that `bsln login` saved
station = client.station_description(STATION)
print("joints:", station["joint_order"])
print("control rate:", station["control_hz"], "Hz")
high = next(joint["safe_range"][1] for joint in station["joints"] if joint["name"] == WRIST)

with client.start_session(STATION) as session:
    print("session:", session.id)
    start = session.get_state()["target"][WRIST]  # the station holds the wrist here now
    turn = TURN if start + TURN <= high else -TURN  # stay inside the safe range

    # 1. Send one target. The station keeps it. The safety supervisor limits the speed.
    session.set_joints({WRIST: start + turn})
    time.sleep(2.0)

    # 2. Stream targets at the control rate for 3 s. The wrist turns back to its start position in 2 s.
    #    A streamed target expires after 0.25 s. Then the arm holds its position.
    period = 1.0 / station["control_hz"]
    begin = time.monotonic()
    while (elapsed := time.monotonic() - begin) < 3.0:
        done = min(1.0, elapsed / 2.0)  # 0 at the start, 1 after 2 s
        session.send_targets({WRIST: start + turn * (1.0 - done)})
        time.sleep(period)

    print("wrist now:", round(session.get_joints()[WRIST], 3), "rad")
    usage = session.end()  # end the session. The arms hold their position.

if usage:  # what the session did, as the server counted it
    print("messages:", usage["messages"], "dropped:", usage["dropped"], "deadman holds:", usage["deadman_holds"])
print("report: bsln session report", session.id)
```

Run the script. Replace `<station id>` with an `id` from `bsln stations`.

```bash
python quickstart.py <station id>
```

**If it works,** the left wrist turns by about 6 degrees and back, and the script prints this:

```text
joints: ['left_joint1', 'left_joint2', 'left_joint3', 'left_joint4', 'left_joint5', 'left_joint6', 'left_gripper', 'right_joint1', 'right_joint2', 'right_joint3', 'right_joint4', 'right_joint5', 'right_joint6', 'right_gripper']
control rate: 30.0 Hz
session: ses_21f06bf1d78bb309
wrist now: 0.0 rad
messages: 83 dropped: 0 deadman holds: 0
report: bsln session report ses_21f06bf1d78bb309
```

The script does these steps:

1. It reads the station description: the joint names, the units, the safe range of each joint and the control rate.
2. It starts a session. Only you can control the station during your session.
3. It sends one target with `set_joints`. The station keeps this target until you send a new one.
4. It streams targets with `send_targets` for 3 s. A streamed target expires after 0.25 s.
5. It ends the session. The arms hold their position.

Write down the session ID. You need it for the report and for the data.

**If it fails,** the script stops with a Python traceback. The last line tells you why:

| Last line of the traceback | What it means | What to do |
|---|---|---|
| `BslnError: HTTP 404: station not found` | The station ID is wrong. | Copy the `id` from `bsln stations`. |
| `BslnError: HTTP 409: station is busy` | Another session uses the station. | Wait, or use another station with the status `idle`. |
| `BslnError: HTTP 409: station is locked by a safety fault` | The station stopped after an e-stop or a fault. | Contact Baseline Robotics. |
| `BslnError: HTTP 403: token lacks the 'control' scope` | Your token can read, but it cannot move a robot. | Use a token with the `control` scope. |
| `BslnError: HTTP 401: missing or invalid token` | The token is wrong or has expired. | Check `BSLN_TOKEN`, or run `bsln login` again. |
| `httpx.ConnectError: ...` | Your computer cannot reach the server. | Check `BSLN_URL` and your network. |

When the script stops with an error inside the `with` block, the session ends and the arms hold their position.

## 5. Watch a session live

You can watch a session from a second terminal while your script drives the station.
The watcher only reads. It cannot move the robot, and it does not keep a session alive.

The live view needs the viewer extra from step 1. Find the session ID with `bsln session ls`. Then run:

```bash
bsln session watch <session id> --view
```

**If it works,** a Rerun window opens. It shows the station in 3D, a plot for each joint and the camera images.
The terminal shows the state of the session and its events. The command ends when the session ends:

```text
session ses_67ca333e573f89d7 active
  episode.end
session ended: stopped after 336 steps
the recording: bsln session download ses_67ca333e573f89d7
```

The quickstart script runs for only about 6 s. Your own scripts usually run longer.
A computer without a display can save the stream to a file. Open the file later with `rerun session.rrd`.

```bash
bsln session watch <session id> --save session.rrd
```

**If it fails:**

- `this needs the optional dependency 'rerun'`: add the viewer extra with `uv add 'bsln[viz]==0.2.1a1'`.
- `session ... has ended ..., so there is nothing to watch.`: the session is over. Download its recording, as step 8 shows.

## 6. Stop a session

End the session in your script with `session.end()`.
The `with` block also ends the session when your script leaves it, for example after an error.
From a terminal, use this command:

```bash
bsln session stop <session id>
```

To find the ID of a session, list your sessions:

```bash
bsln session ls
```

**If it works,** you see each session with its state and the reason why it ended:

```text
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ id                   ┃ station_id ┃ state ┃ created_at                 ┃ end_reason                        ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ses_9c73890697daf597 │ yam-01     │ ended │ 2026-10-15T09:11:50.401412 │ operator disconnected for more    │
│                      │            │       │                            │ than 30 s (after 1041 steps)      │
│ ses_21f06bf1d78bb309 │ yam-01     │ ended │ 2026-10-15T09:06:41.110187 │ stopped after 152 steps           │
└──────────────────────┴────────────┴───────┴────────────────────────────┴───────────────────────────────────┘
```

**If your script stops or loses the connection,** the station holds the arms where they are.
The session stays open for 30 s. In this time, your script can connect again with `session.reconnect()`.
After 30 s without a connection, bsln ends the session with the reason `operator disconnected for more than 30 s`.
Until then the station stays `busy`. To free it sooner, stop the session yourself with `bsln session stop`.

## 7. Emergency stop

Use the emergency stop, or e-stop, when the robot does something that you do not expect. You can send it at any time.
In your script, call `session.estop()`. From a terminal, use one of these commands:

```bash
bsln estop <station id>
```

```bash
bsln session estop <session id>
```

**If it works,** `bsln estop` shows a line that starts with `E-STOP sent`, and `bsln session estop` shows `{"sent": true}`.

After an e-stop:

- The arms stop and hold their position. The motors stay on.
- Your session ends with the reason `safety_abort`. Your script gets `SessionError: session ended: safety_abort after ... steps`.
- The station gets the status `fault`. A new session gets `HTTP 409: station is locked by a safety fault`.
- Only Baseline Robotics can clear the fault. Contact us as step 10 explains.

**If it fails:** `E-STOP could not be delivered` means that the station is offline, so the e-stop cannot reach it.
Contact Baseline Robotics at once.

## 8. Get the report and your data

The report is ready after the session ends.
It shows the commands that you sent, the holds, the safety adjustments, the faults, the e-stops and the connection quality.

```bash
bsln session report <session id>
```

**If it works,** you see a JSON document. These fields are the most important:

| Field | What it tells you |
|---|---|
| `end_reason` | Why the session ended: a `code`, who ended it in `by`, and a `detail` text. |
| `commands` | How many messages you sent, and how many bsln dropped or refused. |
| `holds` | How often the deadman held the arms, and for how long. |
| `safety` | How often the safety supervisor changed your targets, for each joint, and why. |
| `faults` and `estops` | The faults and e-stops during the session. |
| `connection` and `rtt_ms` | Disconnects, and the round-trip time between your script and the server. |
| `recording` | The number of episodes, the size and the steps of the recording. |

While the session still runs, the command fails with `error 409: the session is active: its report is ready when it has ended`.

This command downloads the recorded data to the folder `./data`:

```bash
bsln session download <session id> --out ./data
```

**If it works,** you see `saved to data/<session id>`. The folder contains:

```text
data/ses_21f06bf1d78bb309/
├── report.json                          the session report
└── 000_ses_21f06bf1d78bb309_ep000/      one folder for each episode
    ├── ses_21f06bf1d78bb309_ep000.rrd   the Rerun recording. Open it with: rerun <file>
    ├── steps.parquet                    one row for each control step
    ├── cameras/top/000000.jpg ...       the camera images, 10 for each second
    ├── meta.json                        the station, the robot and the joint limits
    └── result.json                      the outcome, the number of steps and the safety counts
```

In `steps.parquet`, `obs.pos.<joint>`, `obs.vel.<joint>` and `obs.eff.<joint>` are the measured position, velocity and effort.
`action.raw.<joint>` is the target that you sent. `action.safe.<joint>` is the target after the safety supervisor.

## 9. Limits

| Item | Value |
|---|---|
| Joints | 14: `left_joint1` to `left_joint6`, `left_gripper`, `right_joint1` to `right_joint6`, `right_gripper` |
| Units | Arm joints: rad. Do not send degrees. Grippers: 0 is closed, 1 is open. |
| Control rate | 30 Hz. The station runs one control step every 1/30 s. |
| Messages | 60 messages per second at most. bsln drops more messages and sends you a notice. |
| Deadman | 0.25 s. If no new streamed target arrives in this time, the arm holds its position. |
| Safety supervisor | It moves each target into the safe range and limits its speed. It never makes a target larger or faster. |
| Session start | Joints 2 and 3 and the grippers can move a little into the safe range: 0.02 rad or 3 % at most. |
| Session length | 1 hour at most. Then the station ends the session with the reason `time_limit`. |
| Lost connection | After 30 s without a connection, bsln ends the session. |
| Faults | A fault stops the station. It stays stopped until Baseline Robotics clears the fault. |
| E-stop | Always permitted. |

The station description gives the exact values for each station: `client.station_description("<station id>")`.

## 10. Get help

Contact Baseline Robotics. Tell us:

- the station ID and the session ID,
- the time of the problem and your time zone,
- the full error message.

Each error message from bsln includes a request ID, for example `error 404: session not found (request ID 1023e596c15f446680304ccff00d8afc)`.
Include this request ID. With it, we can find your request in our logs.

## 11. Next steps

- `CLI.md` explains every command. It also shows `bsln session control`, a local page with sliders that moves the arms.
- `SDK.md` explains the Python SDK. It shows how to drive a station from your own policy with action chunks.

---

Copyright 2026 Baseline Robotics AG. License: Apache-2.0 (see LICENSE and NOTICE).
