ascom-alpaca-core 0.2.2

Framework-agnostic ASCOM Alpaca protocol types and traits for Rust — all 10 device types, no HTTP framework required
Documentation
# Architecture

## Overview

`ascom-alpaca-core` is a protocol abstraction layer. It sits between your hardware logic and your HTTP server:

```
Your Hardware Logic
       |
  [Device Trait]     <-- this crate
       |
  [HTTP Server]      <-- you provide (esp-idf, axum, actix, etc.)
       |
  ASCOM Alpaca Client (NINA, SGP, Voyager, etc.)
```

The crate provides:
- **Traits** defining the ASCOM device interfaces
- **Types** for protocol-level serialization (responses, errors, enums)
- **Registry** for managing multiple heterogeneous devices
- **Discovery/Management** types for the Alpaca management API

It does NOT provide:
- HTTP server or routing
- Async runtime
- Network transport
- State persistence

## Module Structure

```
types/              Protocol primitives
  response.rs       AlpacaResponse<T>, MethodResponse (JSON envelopes)
  error.rs          AlpacaError enum (0x400-0xFFF), AlpacaResult<T>
  device_type.rs    DeviceType enum with URL path mapping
  params.rs         Query/form parameter normalization
  guide_direction.rs  GuideDirection enum (shared by Camera + Telescope)

device/             Base device abstraction
  mod.rs            Device trait (common properties), RegisteredDevice enum
  common.rs         DeviceStateItem, shared types

registry/           Device management
  mod.rs            DeviceRegistry (heterogeneous storage + typed lookup)
  transaction.rs    TransactionCounter (atomic server transaction IDs)
  client.rs         ClientTracker (client ID tracking)

discovery/          UDP discovery protocol
  mod.rs            DiscoveryResponse, port constants, multicast addresses

management/         Management API
  mod.rs            ServerDescription, ConfiguredDevice, ApiVersions

{device_type}/      Per-device modules (feature-gated)
  mod.rs            Device trait + default NotImplemented methods
  types.rs          Domain enums (e.g., CameraState, SensorType, DriveRate)

camera/             Camera has extra modules:
  mod.rs            Camera trait (~55 methods)
  types.rs          CameraState, SensorType, GuideDirection
  image.rs          ImageData enum, ImageBytes binary encoding

conformu/           ConformU test infrastructure (feature-gated)
  dispatch.rs       URL path -> trait method router
  management.rs     Management API JSON handlers
  mocks/            Mock implementations for all 10 device types
```

## Key Design Decisions

### Trait-per-device-type

Each ASCOM device type is a separate Rust trait extending `Device`. This enables:
- Feature-gated compilation (only include device types you need)
- Typed registry lookup (`get_camera(0)` returns `&dyn Camera`)
- Default `NotImplemented` for optional methods

### RegisteredDevice enum

The `RegisteredDevice` enum wraps `Box<dyn DeviceTrait>` for each device type. This is the only way to store heterogeneous trait objects in a single `Vec` without unsafe code:

```rust
pub enum RegisteredDevice {
    SafetyMonitor(Box<dyn SafetyMonitor>),
    Camera(Box<dyn Camera>),
    Switch(Box<dyn Switch>),
    // ...
}
```

`From<Box<dyn T>>` impls allow `registry.register(boxed_device)` to work for any device type.

### PascalCase serialization

The ASCOM Alpaca protocol uses PascalCase field names in JSON. All response types use `#[serde(rename_all = "PascalCase")]`. Fields ending in "ID" need explicit `#[serde(rename = "...ID")]` because serde's PascalCase converts to "Id".

### Integer enum serialization

ASCOM enums are integers on the wire (e.g., `CameraState::Exposing = 2`). We use `serde_repr` for this:

```rust
#[derive(Serialize_repr, Deserialize_repr)]
#[repr(i32)]
pub enum CameraState {
    Idle = 0,
    Waiting = 1,
    Exposing = 2,
    Reading = 3,
    Download = 4,
    Error = 5,
}
```

### Error model

ASCOM has two error layers:
1. **HTTP errors** (400, 500) — for malformed requests, server failures
2. **Protocol errors** (in JSON body, HTTP 200) — for ASCOM-specific conditions

`AlpacaError` represents protocol errors. `RegistryError` represents lookup failures (which map to HTTP 400).

### No-std considerations

The crate uses `std` (for `Mutex`, `HashMap`, `Vec`, `String`) but has no networking or I/O dependencies. On ESP32 with esp-idf, `std` is available. For bare-metal `no_std` targets, the crate would need significant rework (replacing `String` with heapless alternatives, etc.).

## ConformU Architecture

The `conformu` feature adds a complete test infrastructure:

```
ConformU (external tool)
    |
  HTTP requests
    |
conformu_harness.rs (tiny_http server)
    |
  dispatch.rs (URL parser + router)
    |
  DeviceRegistry (typed lookup)
    |
  Mock device (trait implementation)
    |
  AlpacaResponse/MethodResponse (JSON serialization)
    |
  HTTP response back to ConformU
```

### Dispatch layer

`dispatch_request()` maps URL paths to trait method calls. It handles:
- Path parsing: `/api/v1/{device_type}/{number}/{method}`
- Parameter extraction from query string + PUT body
- Type conversion (string params → i32, f64, bool)
- Response envelope construction with transaction IDs
- Error mapping (trait errors → JSON error responses)

### Internal macros

The ConformU module uses two internal macros to reduce boilerplate:

- **`impl_mock_device!`** (in `mocks/mod.rs`) — generates the `impl Device for MockX` block with standard connected/disconnect/driver_info plumbing. Accepts string literals for most devices or `name_field:`/`unique_id_field:` for devices with dynamic identity (Camera). The `device_state` parameter takes a closure.
- **`typed_getter!`** (in `registry/mod.rs`) — generates feature-gated `get_<device>(&self, num) -> Result<&dyn Trait, RegistryError>` methods on `DeviceRegistry`. Each invocation maps a method name, feature flag, DeviceType variant, RegisteredDevice variant, and trait path.

These are private — crate users don't interact with them.

### DeviceStateBuilder

`DeviceStateBuilder` (in `device/common.rs`, exported in prelude) is a public API for constructing `Vec<DeviceStateItem>` without manual struct construction:

```rust
DeviceStateBuilder::new()
    .add("CameraState", state as i32)
    .add("Temperature", -10.0)
    .add("ImageReady", true)
    .build()
```

Used internally by all 10 mock `device_state()` implementations and available to crate consumers for their own device implementations.

### Mock devices

Each mock implements the full device trait with configurable capabilities:
- Time-based state transitions (using `Instant::now()`) for async operations
- `Mutex` for interior mutability (traits use `&self`)
- Builder patterns for feature configuration (e.g., `CameraFeatures`)
- Two mutually exclusive gain/offset modes tested (numeric vs named)

## Wire Format Examples

### GET (value-returning)

```
GET /api/v1/safetymonitor/0/issafe?ClientID=1&ClientTransactionID=42

200 OK
{
  "Value": true,
  "ErrorNumber": 0,
  "ErrorMessage": "",
  "ClientTransactionID": 42,
  "ServerTransactionID": 1
}
```

### PUT (void method)

```
PUT /api/v1/telescope/0/tracking
Content-Type: application/x-www-form-urlencoded

Tracking=true&ClientID=1&ClientTransactionID=43

200 OK
{
  "ErrorNumber": 0,
  "ErrorMessage": "",
  "ClientTransactionID": 43,
  "ServerTransactionID": 2
}
```

### Error response

```
GET /api/v1/telescope/0/rightascension

200 OK
{
  "ErrorNumber": 1031,
  "ErrorMessage": "Not connected",
  "ClientTransactionID": 0,
  "ServerTransactionID": 3
}
```