Skip to main content

Tafy Studio HAL Specification

Overview

The Hardware Abstraction Layer (HAL) provides a uniform interface for all hardware in Tafy Studio. This specification defines message formats, naming conventions, and behavior contracts that all drivers must implement.

Core Principles

  1. Self-Describing: Messages include schema version and capabilities
  2. Forward Compatible: Unknown fields are ignored, not errors
  3. Units in Names: No ambiguity (meters_per_sec not speed)
  4. Graceful Degradation: Missing optional features don't break core functionality
  5. Idempotent Commands: Sending the same command twice is safe

Message Envelope

Every HAL message is wrapped in a standard envelope:

{
"hal_major": 1,
"hal_minor": 0,
"schema": "tafylabs/hal/motor/differential/1.0",
"device_id": "esp32-a4cf12",
"caps": ["motor.differential:v1.0", "sensor.range.tof:v1.0"],
"ts": "2024-03-14T10:30:00.000Z",
"payload": { ... }
}

Fields:

  • hal_major: Major version of HAL (breaking changes)
  • hal_minor: Minor version of HAL (additions only)
  • schema: Full schema identifier for payload
  • device_id: Unique device identifier
  • caps: Array of capability strings with versions
  • ts: ISO 8601 timestamp
  • payload: The actual message data

Subject Naming

NATS subjects follow a hierarchical pattern:

hal.v{major}.{domain}.{type}.{action}

Examples:

  • hal.v1.motor.differential.cmd - Differential motor commands
  • hal.v1.sensor.range.data - Range sensor readings
  • hal.v1.camera.config.set - Camera configuration

Standard Domains

Motor Control (motor)

Differential Drive

Commands to hal.v1.motor.differential.cmd:

{
"linear_vel_m_per_s": 0.5,
"angular_vel_rad_per_s": 0.2,
"duration_ms": 1000,
"stop_action": "brake"
}

State on hal.v1.motor.differential.state:

{
"left_vel_m_per_s": 0.48,
"right_vel_m_per_s": 0.52,
"linear_vel_m_per_s": 0.5,
"angular_vel_rad_per_s": 0.2,
"battery_volts": 12.1,
"current_amps": 2.3
}

Servo Control

Commands to hal.v1.motor.servo.cmd:

{
"channel": 0,
"position_deg": 90.0,
"speed_deg_per_s": 60.0
}

Sensors (sensor)

Range Sensors

Data on hal.v1.sensor.range.data:

{
"distance_m": 1.23,
"min_range_m": 0.02,
"max_range_m": 4.0,
"fov_deg": 25.0,
"confidence": 0.95
}

IMU

Data on hal.v1.sensor.imu.data:

{
"accel_m_per_s2": {"x": 0.1, "y": 0.0, "z": 9.81},
"gyro_rad_per_s": {"x": 0.0, "y": 0.0, "z": 0.01},
"mag_tesla": {"x": 0.00002, "y": 0.00001, "z": 0.00004},
"orientation_quaternion": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0},
"temperature_celsius": 25.3
}

Vision (vision)

Camera Frames

Request on hal.v1.camera.frame.req:

{
"format": "jpeg",
"width": 640,
"height": 480,
"quality": 85
}

Response on hal.v1.camera.frame.resp:

{
"format": "jpeg",
"width": 640,
"height": 480,
"data": "base64_encoded_image_data",
"timestamp_ms": 1678901234567
}

System (system)

Health Check

Request on hal.v1.system.health.req:

{
"include_diagnostics": true
}

Response on hal.v1.system.health.resp:

{
"status": "healthy",
"uptime_s": 3600,
"free_memory_bytes": 1048576,
"cpu_percent": 23.5,
"temperature_celsius": 45.0,
"diagnostics": {
"voltage": 12.1,
"errors": []
}
}

Capability Declaration

Devices declare capabilities in their mDNS TXT records and registration:

{
"caps": [
"motor.differential:v1.0:priority=high",
"sensor.range.tof:v1.0:count=3",
"system.health:v1.0",
"system.ota:v1.0"
]
}

Format: {domain}.{type}:v{version}[:metadata]

Command Patterns

Request/Reply

For commands expecting responses:

Client: NATS.Request("hal.v1.motor.info.get", timeout: 5s)
Driver: NATS.Reply(with device info)

Fire and Forget

For commands without confirmation:

Client: NATS.Publish("hal.v1.motor.differential.cmd", command)

Streaming

For continuous data:

Driver: NATS.Publish("hal.v1.sensor.imu.data", data) // 100Hz
Clients: NATS.Subscribe("hal.v1.sensor.imu.data")

Error Handling

Errors are returned in a standard format:

{
"error": {
"code": "RANGE_EXCEEDED",
"message": "Requested speed 10 m/s exceeds maximum 2 m/s",
"details": {
"requested": 10,
"maximum": 2
}
}
}

Standard error codes:

  • INVALID_PARAMETER - Parameter validation failed
  • RANGE_EXCEEDED - Value outside acceptable range
  • NOT_IMPLEMENTED - Feature not available
  • HARDWARE_ERROR - Physical device problem
  • TIMEOUT - Operation timed out

Timing and Quality of Service

Latency Classes

  • Real-time: <1ms (encoder feedback)
  • Low-latency: <10ms (motor commands)
  • Interactive: <100ms (camera frames)
  • Background: >100ms (firmware updates)

Message Priority

Set via NATS headers:

  • Priority: high - Safety-critical (e-stop, collision)
  • Priority: normal - Standard operations
  • Priority: low - Telemetry, logging

Versioning and Migration

Version Rules

  • Major version change = breaking change
  • Minor version change = additions only
  • Patch version change = clarifications

Migration Support

During transitions, drivers may:

  1. Publish to both old and new subjects
  2. Accept commands on both versions
  3. Include adapters for translation

Example migration:

# v1 -> v2 migration (6 months)
hal.v1.motor.cmd -> hal.v2.motor.differential.cmd
Driver publishes to both, gradually deprecate v1

Best Practices

For Driver Authors

  1. Validate all inputs, provide clear errors
  2. Include units in every numeric field name
  3. Publish state changes immediately
  4. Support capability queries
  5. Implement health checks

For Client Authors

  1. Subscribe before sending commands
  2. Handle missing devices gracefully
  3. Respect device capabilities
  4. Use appropriate timeouts
  5. Log errors with context

Performance Guidelines

  • State updates: 10-100Hz depending on dynamics
  • Command latency: <10ms processing time
  • Startup time: <1s from power to capability announcement
  • Memory usage: <10MB for simple drivers

Example: Building a Motor Driver

# 1. Announce capabilities
mdns.register("_tafynode._tcp", {
"caps": ["motor.differential:v1.0", "system.health:v1.0"],
"device_id": device_id
})

# 2. Subscribe to commands
async def handle_motor_cmd(msg):
cmd = json.loads(msg.data)
# Validate
if abs(cmd["linear_vel_m_per_s"]) > MAX_SPEED:
return error("RANGE_EXCEEDED", f"Speed exceeds {MAX_SPEED}")

# Execute
set_motor_speeds(
left=calculate_left_speed(cmd),
right=calculate_right_speed(cmd)
)

# Update state
publish_state()

nats.subscribe("hal.v1.motor.differential.cmd", cb=handle_motor_cmd)

# 3. Publish state
async def publish_state():
state = {
"left_vel_m_per_s": get_left_speed(),
"right_vel_m_per_s": get_right_speed(),
"battery_volts": read_battery(),
"timestamp_ms": time.time() * 1000
}

envelope = create_envelope("motor.differential.state", state)
await nats.publish("hal.v1.motor.differential.state", envelope)

# 4. Start state publisher
asyncio.create_task(publish_state_loop(rate_hz=20))

Testing Compliance

Use the HAL compliance test suite:

# Test a motor driver
tafy test hal --device esp32-a4cf12 --capability motor.differential

# Test all devices
tafy test hal --all

Tests verify:

  • Message format compliance
  • Timing requirements
  • Error handling
  • Capability accuracy
  • State consistency