agroweekpy 1.0.0
ROS-like Python library for game robot control via WebSocket
Requires Python: >=3.8

# agroweekpy

ROS-like Python library for game robot control via WebSocket.

## Overview

`agroweekpy` provides a familiar rospy-style API for controlling robots in a game via WebSocket communication. It supports:

- **Motor Control**: Set velocities for left and right motors
- **Odometry**: Get distance traveled by motors  
- **Yaw/Orientation**: Get rotation angle (Y-axis)

## Installation

```bash
pip install -e .
```

Or install dependencies directly:

```bash
pip install websockets
```

## Quick Start

```python
import agroweekpy
from agroweekpy.msg import MotorCommand

# Initialize node and connect to game
agroweekpy.init_node('my_rover', uri='ws://localhost:8765')

# Create motor publisher
motor_pub = agroweekpy.Publisher('/motor_cmd', MotorCommand)

# Main control loop
rate = agroweekpy.Rate(10)  # 10 Hz
while not agroweekpy.is_shutdown():
    cmd = MotorCommand(left_velocity=0.5, right_velocity=0.5)
    motor_pub.publish(cmd)
    rate.sleep()
```

## API Reference

### Node Management

```python
# Initialize node
agroweekpy.init_node(name, uri='ws://localhost:8765', anonymous=False)

# Check shutdown state
agroweekpy.is_shutdown()  # Returns True/False

# Block until shutdown
agroweekpy.spin()

# Request shutdown
agroweekpy.signal_shutdown(reason='done')

# Register shutdown callback
agroweekpy.on_shutdown(lambda reason: print(f'Shutting down: {reason}'))
```

### Rate Control

```python
rate = agroweekpy.Rate(10)  # 10 Hz

while not agroweekpy.is_shutdown():
    # Do work...
    rate.sleep()  # Sleep to maintain rate
```

### Logging

```python
agroweekpy.logdebug('Debug message')
agroweekpy.loginfo('Info message')
agroweekpy.logwarn('Warning message')
agroweekpy.logerr('Error message')
agroweekpy.logfatal('Fatal message')
```

### Publisher

```python
from agroweekpy import Publisher
from agroweekpy.msg import MotorCommand

# Create publisher
pub = Publisher('/motor_cmd', MotorCommand, queue_size=10)

# Publish message
cmd = MotorCommand(left_velocity=0.5, right_velocity=0.5)
pub.publish(cmd)
```

#### MotorPublisher (Convenience Class)

```python
from agroweekpy import MotorPublisher

motor = MotorPublisher('/motor_cmd')

motor.forward(0.5)      # Move forward
motor.backward(0.5)     # Move backward
motor.turn_left(0.3)    # Turn left in place
motor.turn_right(0.3)   # Turn right in place
motor.stop()            # Stop motors
motor.set_velocity(0.5, 0.3)  # Set left/right velocities
```

### Subscriber

```python
from agroweekpy import Subscriber
from agroweekpy.msg import Odometry

def callback(msg):
    print(f'Distance: {msg.total_distance}')

sub = Subscriber('/odometry', Odometry, callback)
```

#### OdometrySubscriber (Convenience Class)

```python
from agroweekpy import OdometrySubscriber

odom = OdometrySubscriber('/odometry')

odom.get_distance()       # Total distance (average of both motors)
odom.get_left_distance()  # Left motor distance
odom.get_right_distance() # Right motor distance
odom.get_odometry()       # Full Odometry message
```

#### YawSubscriber (Convenience Class)

```python
from agroweekpy import YawSubscriber

yaw = YawSubscriber('/yaw')

yaw.get_angle()           # Current angle in degrees
yaw.get_angle_radians()   # Current angle in radians
yaw.get_normalized_angle() # Angle in -180 to 180 range
yaw.get_angular_velocity() # Angular velocity (deg/s)
yaw.angle_to(90.0)        # Shortest angle to target
```

### Message Types

#### MotorCommand

```python
from agroweekpy.msg import MotorCommand

cmd = MotorCommand(left_velocity=0.5, right_velocity=0.5)
cmd.left_velocity   # -1.0 to 1.0
cmd.right_velocity  # -1.0 to 1.0
cmd.timestamp       # Auto-generated

# Convenience methods
cmd.forward(0.5)
cmd.backward(0.5)
cmd.turn_left(0.3)
cmd.turn_right(0.3)
cmd.stop()
```

#### Odometry

```python
from agroweekpy.msg import Odometry

odom = Odometry(left_distance=10.5, right_distance=10.3)
odom.left_distance    # Left motor distance
odom.right_distance   # Right motor distance
odom.total_distance   # Average distance
odom.left_velocity    # Current left velocity
odom.right_velocity   # Current right velocity
```

#### Yaw

```python
from agroweekpy.msg import Yaw

yaw = Yaw(angle=45.0)
yaw.angle              # Angle in degrees
yaw.radians            # Angle in radians
yaw.normalized_angle   # -180 to 180
yaw.angular_velocity   # deg/s
yaw.angle_to(90.0)     # Shortest turn to target
```

## WebSocket Protocol

### Message Format

All messages are JSON with this structure:

```json
{
    "topic": "/motor_cmd",
    "data": {
        "left_velocity": 0.5,
        "right_velocity": 0.5,
        "timestamp": 1706450400.123
    },
    "timestamp": 1706450400.123
}
```

### Topics

| Topic | Direction | Message Type | Description |
|-------|-----------|--------------|-------------|
| `/motor_cmd` | Client → Game | MotorCommand | Motor velocity commands |
| `/odometry` | Game → Client | Odometry | Motor distances |
| `/yaw` | Game → Client | Yaw | Robot orientation |

### Expected Game Server Messages

**Odometry** (sent by game):
```json
{
    "topic": "/odometry",
    "data": {
        "left_distance": 10.5,
        "right_distance": 10.3,
        "left_velocity": 0.5,
        "right_velocity": 0.5,
        "timestamp": 1706450400.123
    }
}
```

**Yaw** (sent by game):
```json
{
    "topic": "/yaw",
    "data": {
        "angle": 45.0,
        "angular_velocity": 5.0,
        "timestamp": 1706450400.123
    }
}
```

## Configuration

```python
agroweekpy.init_node(
    'my_rover',
    uri='ws://192.168.1.100:8765',  # Game server URI
    anonymous=False,                 # Add timestamp to name
    log_level=logging.INFO,          # Logging level
    reconnect=True,                  # Auto-reconnect
    reconnect_interval=1.0,          # Initial reconnect delay
    max_reconnect_interval=30.0,     # Max reconnect delay
    ping_interval=20.0,              # WebSocket ping interval
)
```

## Examples

See the `examples/` directory for complete examples:

- `basic_publisher.py` - Simple motor control
- `odometry_listener.py` - Reading odometry data
- `yaw_listener.py` - Reading orientation data
- `rover_controller.py` - Complete rover control example
- `mock_game_server.py` - Mock server for testing

## Comparison with rospy

| rospy | agroweekpy | Notes |
|-------|------------|-------|
| `rospy.init_node()` | `agroweekpy.init_node()` | Similar API |
| `rospy.Publisher()` | `agroweekpy.Publisher()` | Same pattern |
| `rospy.Subscriber()` | `agroweekpy.Subscriber()` | Same pattern |
| `rospy.Rate()` | `agroweekpy.Rate()` | Same pattern |
| `rospy.spin()` | `agroweekpy.spin()` | Same behavior |
| `rospy.is_shutdown()` | `agroweekpy.is_shutdown()` | Same behavior |
| `rospy.loginfo()` | `agroweekpy.loginfo()` | Same API |

## License

MIT License