> ## Documentation Index
> Fetch the complete documentation index at: https://innateinc-theo-skills-odometry-state.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Robot State

export const RobotStateAvailableTable = () => {
  const rows = [{
    state: "Camera Image",
    typeEnum: "RobotStateType.LAST_MAIN_CAMERA_IMAGE_B64",
    description: "Latest frame (base64 JPEG)."
  }, {
    state: "Odometry",
    typeEnum: "RobotStateType.LAST_ODOM",
    description: "2D pose (x, y, theta) and velocities."
  }, {
    state: "Map",
    typeEnum: "RobotStateType.LAST_MAP",
    description: "Occupancy grid."
  }, {
    state: "Head Position",
    typeEnum: "RobotStateType.LAST_HEAD_POSITION",
    description: "Head tilt angle."
  }];
  return <div className="interface-methods-table-wrap">
      <table className="interface-methods-table">
        <thead>
          <tr>
            <th>State</th>
            <th>Type Enum</th>
            <th>Description</th>
          </tr>
        </thead>
        <tbody>
          {rows.map(row => <tr key={row.typeEnum}>
              <td>{row.state}</td>
              <td>
                <span className="interface-param-badge">{row.typeEnum}</span>
              </td>
              <td>{row.description}</td>
            </tr>)}
        </tbody>
      </table>
    </div>;
};

The `RobotState` descriptor gives you access to sensor data—camera images, odometry, maps, and more. Declared state is **automatically updated at 50Hz** while your skill runs.

## Declaration

Declare state dependencies as class attributes:

```python theme={null}
from brain_client.skill_types import Skill, RobotState, RobotStateType

class MySkill(Skill):
    image = RobotState(RobotStateType.LAST_MAIN_CAMERA_IMAGE_B64)
    odom = RobotState(RobotStateType.LAST_ODOM)
```

The system injects and updates these values automatically. Always check for `None` on first access.

## Available State

<RobotStateAvailableTable />

## Camera Image

The main camera image as a base64-encoded JPEG:

```python theme={null}
class MySkill(Skill):
    image = RobotState(RobotStateType.LAST_MAIN_CAMERA_IMAGE_B64)

    def execute(self):
        if not self.image:
            return "No image available", SkillResult.FAILURE

        # Decode base64 to bytes
        import base64
        image_bytes = base64.b64decode(self.image)

        # Use with PIL
        from PIL import Image
        import io
        img = Image.open(io.BytesIO(image_bytes))

        # Or send to vision API
        response = vision_api.analyze(self.image)
```

## Odometry

Where the robot is and how it is moving, as an `innate.Odometry` object. MARS
is a differential-drive base on flat ground, so you get a flat 2D pose —
`x`, `y`, and a yaw angle — directly, no quaternion math needed:

```python theme={null}
class MySkill(Skill):
    odom = RobotState(RobotStateType.LAST_ODOM)

    def execute(self):
        if self.odom:
            x, y = self.odom.position          # meters, odom frame
            heading = self.odom.theta_degrees  # yaw, counter-clockwise positive
            speed = self.odom.linear_velocity  # m/s, forward
```

| Attribute          | Type                  | Description                                                        |
| ------------------ | --------------------- | ------------------------------------------------------------------ |
| `x`, `y`           | `float`               | Position in meters, odom frame                                     |
| `position`         | `tuple[float, float]` | `(x, y)` shorthand                                                 |
| `theta`            | `float`               | Yaw in radians, counter-clockwise positive, wrapped to `(-pi, pi]` |
| `theta_degrees`    | `float`               | Yaw in degrees (matches `navigate_to_position`'s `theta_degrees`)  |
| `linear_velocity`  | `float`               | Forward speed in m/s (negative when reversing)                     |
| `angular_velocity` | `float`               | Turn rate in rad/s, counter-clockwise positive                     |
| `stamp`            | `float`               | Sensor timestamp in seconds                                        |
| `raw`              | `dict`                | The full `nav_msgs/Odometry` as plain data                         |

Always check for `None` on first access — the first odometry message is one
publish period away when a skill starts.

### Closing a loop on odometry

Declared state refreshes at 50 Hz while your skill runs, so you can read the
attributes in a loop to close a control loop. This drives forward a fixed
distance by watching `position`:

```python theme={null}
import math
import time
from innate import Interface, InterfaceType, RobotState, RobotStateType, Skill, SkillResult

class DriveMeters(Skill):
    mobility = Interface(InterfaceType.MOBILITY)
    odom = RobotState(RobotStateType.LAST_ODOM)

    @property
    def name(self):
        return "drive_meters"

    def execute(self, distance: float = 0.5):
        if self.odom is None:
            return "No odometry", SkillResult.FAILURE

        start = self.odom.position                      # (x, y) snapshot
        while math.dist(self.odom.position, start) < distance:
            if self._cancelled:
                self.mobility.send_cmd_vel(linear_x=0.0)
                return "Cancelled", SkillResult.CANCELLED
            # duration acts as a deadman: if this loop dies, the base stops
            self.mobility.send_cmd_vel(linear_x=0.15, duration=0.5)
            time.sleep(0.1)

        self.mobility.send_cmd_vel(linear_x=0.0)
        return f"Drove {distance:.2f}m", SkillResult.SUCCESS
```

For heading, `theta_degrees` is already wrapped to `(-180, 180]`. Accumulate
wrapped deltas so a turn across the ±180° seam still counts correctly:

```python theme={null}
last = self.odom.theta_degrees
turned = 0.0
while turned < 90.0:
    self.mobility.send_cmd_vel(angular_z=0.5, duration=0.5)
    time.sleep(0.05)
    now = self.odom.theta_degrees
    turned += (now - last + 180.0) % 360.0 - 180.0      # signed shortest-arc delta
    last = now
```

### Checking freshness

Use `stamp` (seconds) to skip a reading that has gone stale — for example
after a feed hiccup, when the 50 Hz refresh would otherwise hand you a frozen
value:

```python theme={null}
age = time.time() - self.odom.stamp
if age > 0.5:
    return f"Odometry stale ({age:.1f}s old)", SkillResult.FAILURE
```

### Need more than the 2D pose?

`odom.raw` carries the complete odometry message with rosbridge-style keys —
the real quaternion, `z`, covariances, and the full twist — for skills doing
their own filtering or fusion:

```python theme={null}
class FusePose(Skill):
    odom = RobotState(RobotStateType.LAST_ODOM)

    @property
    def name(self):
        return "fuse_pose"

    def execute(self):
        if self.odom is None:
            return "No odometry", SkillResult.FAILURE

        raw = self.odom.raw                              # full nav_msgs/Odometry as a dict
        quat = raw["pose"]["pose"]["orientation"]        # {"x", "y", "z", "w"} — true quaternion
        z = raw["pose"]["pose"]["position"]["z"]         # height (not on the flat API)
        pose_cov = raw["pose"]["covariance"]             # 36 floats, row-major 6x6
        lateral = raw["twist"]["twist"]["linear"]["y"]   # sideways velocity

        # e.g. reject a fix whose position variance is too high
        if pose_cov[0] > 0.25:                           # var(x) > 0.25 m²
            return "Pose too uncertain", SkillResult.FAILURE

        return f"z={z:.3f} lateral={lateral:.3f}", SkillResult.SUCCESS
```

<Note>
  Skills written for **0.3.0 through 0.6.x** read odometry as a raw-message
  dict (`self.odom["theta_degrees"]`, `self.odom["pose"]["pose"]["position"]`).
  Dict-style access is kept as a permanent compatibility layer — those skills
  keep working with no scheduled removal. New skills should use the attributes
  above.
</Note>

## Map

The occupancy grid map:

```python theme={null}
class MySkill(Skill):
    map_data = RobotState(RobotStateType.LAST_MAP)

    def execute(self):
        if self.map_data:
            width = self.map_data.info.width
            height = self.map_data.info.height
            resolution = self.map_data.info.resolution
            data = self.map_data.data  # 1D array of occupancy values
```

## Head Position

Current head tilt angle:

```python theme={null}
class MySkill(Skill):
    head_pos = RobotState(RobotStateType.LAST_HEAD_POSITION)

    def execute(self):
        if self.head_pos:
            current_angle = self.head_pos
            # Returns int: -25 to +15
```

## Example: CaptureImages

A skill that captures images while rotating:

```python theme={null}
from brain_client.skill_types import (
    Skill, SkillResult, Interface, InterfaceType, RobotState, RobotStateType
)
import math

class CaptureImages(Skill):
    mobility = Interface(InterfaceType.MOBILITY)
    image = RobotState(RobotStateType.LAST_MAIN_CAMERA_IMAGE_B64)

    @property
    def name(self):
        return "capture_images"

    def guidelines(self):
        return "Use to capture images from multiple directions."

    def execute(self, num_directions: int = 4):
        images = []
        rotation_step = (2 * math.pi) / num_directions

        for i in range(num_directions):
            if self._cancelled:
                return "Capture cancelled", SkillResult.CANCELLED

            # Capture current frame
            if self.image:
                images.append(self.image)
                self._send_feedback(f"Captured {i+1}/{num_directions}")

            # Rotate to next position
            if i < num_directions - 1:
                self.mobility.rotate(rotation_step)

        return f"Captured {len(images)} images", SkillResult.SUCCESS

    def cancel(self):
        self._cancelled = True
        return "Capture cancelled"
```

## Example: MonitorPosition

A skill that tracks robot movement:

```python theme={null}
from brain_client.skill_types import Skill, SkillResult, RobotState, RobotStateType
import math
import time

class MonitorPosition(Skill):
    odom = RobotState(RobotStateType.LAST_ODOM)

    @property
    def name(self):
        return "monitor_position"

    def guidelines(self):
        return "Use to monitor robot position for a duration."

    def execute(self, duration: float = 5.0):
        if not self.odom:
            return "Odometry not available", SkillResult.FAILURE

        start = self.odom.position
        start_time = time.time()

        while time.time() - start_time < duration:
            if self._cancelled:
                return "Monitoring cancelled", SkillResult.CANCELLED

            distance = math.dist(self.odom.position, start)

            self._send_feedback(f"Moved {distance:.2f}m from start")
            time.sleep(0.5)

        return f"Monitoring complete", SkillResult.SUCCESS

    def cancel(self):
        self._cancelled = True
        return "Monitoring cancelled"
```
