Metadata-Version: 2.4
Name: cognira-robotics
Version: 0.1.0
Summary: Control your robot from cognira.dev — turn Python functions into buttons, sliders and joysticks.
Author: Thomas Conway
License-Expression: MIT
Project-URL: Homepage, https://cognira.dev/robotics
Keywords: robotics,raspberry-pi,remote-control,cognira
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: System :: Hardware
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# cognira Robotics SDK

Turn plain Python functions into buttons, sliders and joysticks, and drive
your robot from **[cognira.dev/robotics](https://cognira.dev/robotics)**,
from anywhere. It is one file of code on your robot and needs no port forwarding
or extra libraries.

```python
from cognira_robotics import Robot

robot = Robot("Rover")

@robot.button("Horn", key="h")
def horn():
    print("beep!")

@robot.joystick("Drive")
def drive(x, y):
    motors.set(left=y + x, right=y - x)

@robot.on_stop
def stop():
    motors.stop()

robot.run()
```

The first time it runs, it prints a code and opens cognira.dev to approve it.
After that the robot shows up under **Robotics** in the chat sidebar whenever
the script is running. The page lists every control with the Python function
behind it.

## Install

```bash
pip install cognira-robotics
```

Python 3.8+ and the standard library only, so it runs on a Raspberry Pi, a
Jetson, a laptop or anything else with Python.

## Controls

Every decorator works bare (`@robot.button`) or with a label
(`@robot.button("Go")`). Bare, the label comes from the function name, so
`turn_left` becomes "Turn left".

| Decorator | Your function gets | On the web |
|---|---|---|
| `@robot.button(label, key=)` | `fn()`, once per press | a button |
| `@robot.hold(label, key=)` | `fn(pressed)`: `True` on press, `False` on release | press-and-hold |
| `@robot.toggle(label, default=False, key=)` | `fn(on)` | a switch |
| `@robot.slider(label, min=0, max=100, step=1, default=, unit=)` | `fn(value)`, an `int` when the steps are whole numbers | a slider |
| `@robot.joystick(label)` | `fn(x, y)`, each from -1 to 1, with up as +y | a joystick |
| `@robot.text(label, placeholder=, max_length=200)` | `fn(text)` | a text box with a Send button |

All of them also take `group="Arm"` to put controls under a heading,
`description="…"` for a hint, and `id=` if two labels would otherwise clash.
`key="w"` binds a keyboard key on the web page. A function that takes no
arguments works with any control.

## While it runs

```python
robot.show("Battery", "87%")    # a live readout on the web
robot.hide("Battery")
robot.log("Picked up the cup")   # printed here and in the web log
robot.set("Headlights", True)    # move a toggle/slider without calling it
robot.value("Speed")             # current toggle/slider value

@robot.every(2)                  # run on a timer
def report():
    robot.show("Temp", read_temp())

@robot.on_start                  # once, when connected
def hello():
    robot.log("Ready")
```

## Safety

Robots move, so these are on by default:

* **Stop.** The web page always has a Stop button, and **Esc** also triggers it.
  Your `@robot.on_stop` functions run **straight away**, even while another
  function is still busy. Anything still waiting in the queue is dropped.
* **Long-running functions can be interrupted.** Use `robot.sleep()` instead of
  `time.sleep()`. It returns `False` as soon as Stop is pressed:

  ```python
  @robot.button
  def patrol():
      while robot.sleep(0.1):
          step_forward()
  ```

* **Dead-man for held controls.** While you hold a joystick or a hold-button,
  the web page re-sends it 4 times a second. If that stops (closed tab, lost
  Wi-Fi), the robot lets go after `deadman_timeout` seconds (default 1.5). Your
  function then gets `False` or `(0, 0)`.
* **Stale commands are thrown away.** The server drops a press it couldn't
  deliver within 10 seconds, and drops joystick and hold commands after 1.5
  seconds. They are never replayed into a robot that reconnects later.
* **Errors don't crash the robot.** An exception in your function is logged, on
  the web too, and the robot keeps running.

Commands run one at a time, in the order they were sent.

## No account? Run it locally

```python
robot.run(local=True)      # http://localhost:8700
```

This serves the same control panel from the robot itself, which is handy
while you write code. It only listens on `127.0.0.1`. You can pass
`host="0.0.0.0"` to reach it from your phone on the same Wi-Fi, but then
anyone on that network can drive the robot.

## Managing links

The token is saved in `~/.cognira/robots.json`, one entry per robot name.

```bash
python -m cognira_robotics link "Rover"     # link now, without running a script
python -m cognira_robotics list
python -m cognira_robotics forget "Rover"
```

Removing a robot on cognira.dev/robotics revokes its token. The next time the
script runs, it asks to be linked again.

Settings for CI, a headless robot or self-hosting:

| Variable | What it does |
|---|---|
| `COGNIRA_ROBOT_TOKEN` | use this token instead of the saved file |
| `COGNIRA_API_URL` | backend URL (default `https://api.cognira.dev`) |
| `COGNIRA_HOME` | where `robots.json` is kept (default `~/.cognira`) |

## How it works

```
your robot ──HTTPS──▶ api.cognira.dev ◀──HTTPS── cognira.dev/robotics
  connect   (its controls)              live events (SSE)
  commands  (long-poll, ~25 s)          button presses
  state     (readouts, log)
```

The robot only makes outbound requests, so NAT and firewalls don't matter.
The server side is `backend/robots.js` and the web page is
`cognira-web/components/robotics.tsx`.

## Examples

* `examples/hello_robot.py`: one button, one switch and a clock
* `examples/rover.py`: a simulated two-wheeled rover using every control type
* `examples/raspberry_pi_rover.py`: the same idea on real motors with `gpiozero`

```bash
python examples/rover.py --local
```

## Tests

```bash
python -m unittest discover -s tests
```
