# HAMView API Reference

Version: 1.0
Base URL: `/api/v1`
Content-Type: `application/json`

## Table of Contents

1. [Authentication](#authentication)
2. [Error Handling](#error-handling)
3. [Endpoints](#endpoints)
   - [Radios](#radios)
   - [MeshCore](#meshcore)
   - [APRS](#aprs)
4. [Data Models](#data-models)
5. [Examples](#examples)

---

## Authentication

Currently, the API does not require authentication. All endpoints are publicly accessible.

---

## Error Handling

All error responses follow this format:

```json
{
  "error": "Human-readable error message"
}
```

### HTTP Status Codes

| Code | Description |
|------|-------------|
| 200  | Success |
| 400  | Bad Request - Invalid parameters |
| 404  | Not Found - Resource does not exist |
| 500  | Internal Server Error |

---

## Endpoints

### Radios

#### List All Radios

```
GET /api/v1/radios
```

Returns a list of all radio receivers/stations.

**Response:** `200 OK`

```typescript
Radio[]
```

**Example:**

```bash
curl http://localhost:8073/api/v1/radios
```

```json
[
  {
    "id": 1,
    "name": "Station-Alpha",
    "is_online": true,
    "manufacturer": "Heltec",
    "protocol": "meshcore",
    "frequency": 868.1
  }
]
```

---

#### List Radios by Protocol

```
GET /api/v1/radios/:protocol
```

Returns radios filtered by protocol.

**Parameters:**

| Name     | Type   | In   | Description |
|----------|--------|------|-------------|
| protocol | string | path | Protocol name (e.g., "meshcore", "aprs") |

**Response:** `200 OK`

```typescript
Radio[]
```

**Example:**

```bash
curl http://localhost:8073/api/v1/radios/meshcore
```

---

### MeshCore

#### Get MeshCore Statistics

```
GET /api/v1/meshcore
```

Returns aggregated network statistics.

**Response:** `200 OK`

```typescript
{
  messages: number;
  nodes: number;
  receivers: number;
  packets: {
    timestamps: number[];
    packets: number[];
  };
}
```

**Example:**

```bash
curl http://localhost:8073/api/v1/meshcore
```

```json
{
  "messages": 150234,
  "nodes": 127,
  "receivers": 8,
  "packets": {
    "timestamps": [1709650800, 1709654400],
    "packets": [142, 203]
  }
}
```

---

#### List MeshCore Groups

```
GET /api/v1/meshcore/groups
```

Returns public MeshCore groups/channels.

**Response:** `200 OK`

```typescript
{
  id: number;
  name: string;
  secret: string;
}[]
```

**Example:**

```bash
curl http://localhost:8073/api/v1/meshcore/groups
```

```json
[
  {
    "id": 5,
    "name": "General Chat",
    "secret": "0123456789abcdef0123456789abcdef"
  }
]
```

---

#### List MeshCore Nodes

```
GET /api/v1/meshcore/nodes
```

Returns MeshCore network nodes.

**Query Parameters:**

| Name | Type   | Required | Description |
|------|--------|----------|-------------|
| type | string | No       | Node type: "chat", "room", "sensor", "repeater" |

**Response:** `200 OK`

```typescript
{
  id: number;
  packet: MeshCorePacket[];
  name: string;
  type: number;
  prefix: string;
  public_key: string;
  first_heard_at: string;
  last_heard_at: string;
  last_latitude: number | null;
  last_longitude: number | null;
  distance?: number;
}[]
```

**Example:**

```bash
curl http://localhost:8073/api/v1/meshcore/nodes
curl http://localhost:8073/api/v1/meshcore/nodes?type=chat
```

```json
[
  {
    "id": 42,
    "packet": [],
    "name": "NODE-CHARLIE",
    "type": 0,
    "prefix": "mc",
    "public_key": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "first_heard_at": "2026-01-15T08:00:00Z",
    "last_heard_at": "2026-03-05T14:25:00Z",
    "last_latitude": 52.3667,
    "last_longitude": 4.8945
  }
]
```

---

#### Find Nodes Near Location

```
GET /api/v1/meshcore/nodes/close-to/:publickey
```

Returns nodes within a specified radius of a reference node.

**Path Parameters:**

| Name      | Type   | Description |
|-----------|--------|-------------|
| publickey | string | 64-character hex-encoded Ed25519 public key |

**Query Parameters:**

| Name   | Type   | Required | Default | Description |
|--------|--------|----------|---------|-------------|
| radius | number | No       | 25000   | Search radius in meters |

**Response:** `200 OK`

```typescript
{
  node: MeshCoreNode;
  nodes: MeshCoreNode[];  // Sorted by distance, with distance field populated
}
```

**Example:**

```bash
curl "http://localhost:8073/api/v1/meshcore/nodes/close-to/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef?radius=50000"
```

```json
{
  "node": {
    "id": 42,
    "name": "NODE-CHARLIE",
    "public_key": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "last_latitude": 52.3667,
    "last_longitude": 4.8945,
    "distance": 0
  },
  "nodes": [
    {
      "id": 43,
      "name": "NODE-DELTA",
      "last_latitude": 52.3700,
      "last_longitude": 4.9000,
      "distance": 450.5
    }
  ]
}
```

---

#### List MeshCore Packets

```
GET /api/v1/meshcore/packets
```

Returns MeshCore packets based on filter criteria.

**Query Parameters (mutually exclusive):**

| Name         | Type   | Required | Description |
|--------------|--------|----------|-------------|
| hash         | string | No       | 16-character hex hash |
| type         | number | No       | Payload type (0-255) |
| channel_hash | string | No       | 2-character channel hash (requires type) |

**Response:** `200 OK`

```typescript
{
  id: number;
  radio_id: number;
  radio: Radio | null;
  snr: number;
  rssi: number;
  version: number;
  route_type: number;
  payload_type: number;
  hash: string;
  path: string;        // Base64
  payload: string;     // Base64
  raw: string;         // Base64
  parsed: object | null;
  channel_hash: string;
  received_at: string;
}[]
```

**Payload Types:**

| Value | Description |
|-------|-------------|
| 0     | Ping |
| 1     | Node announcement |
| 2     | Direct text message |
| 3     | Group text message |
| 4     | Position update |

**Examples:**

```bash
# Get 100 most recent packets
curl http://localhost:8073/api/v1/meshcore/packets

# Get packets by hash
curl http://localhost:8073/api/v1/meshcore/packets?hash=a1b2c3d4e5f67890

# Get packets by type
curl http://localhost:8073/api/v1/meshcore/packets?type=3

# Get group messages for a channel
curl http://localhost:8073/api/v1/meshcore/packets?type=3&channel_hash=ab
```

```json
[
  {
    "id": 12345,
    "radio_id": 1,
    "snr": 8.5,
    "rssi": -95,
    "version": 1,
    "route_type": 0,
    "payload_type": 3,
    "hash": "a1b2c3d4e5f67890",
    "path": "AQIDBA==",
    "payload": "SGVsbG8gV29ybGQ=",
    "raw": "AQIDBAUGBwg=",
    "parsed": {"text": "Hello World"},
    "channel_hash": "ab",
    "received_at": "2026-03-05T14:30:00Z"
  }
]
```

---

### APRS

#### List APRS Packets

```
GET /api/v1/aprs/packets
```

Returns APRS packets based on filter criteria.

**Query Parameters (evaluated in order):**

| Name  | Type   | Required | Default | Description |
|-------|--------|----------|---------|-------------|
| src   | string | No       | -       | Source callsign (case-insensitive) |
| dst   | string | No       | -       | Destination callsign (case-insensitive) |
| limit | number | No       | 100     | Maximum number of packets when no src/dst filter is used |

**Response:** `200 OK`

```typescript
{
  id: number;
  radio_id: number;
  radio: Radio;
  src: string;
  dst: string;
  path: string;
  comment: string;
  latitude: number | null;
  longitude: number | null;
  symbol: string;
  raw: string;
  received_at: string;
}[]
```

**Examples:**

```bash
# Get 100 most recent packets
curl http://localhost:8073/api/v1/aprs/packets

# Get packets by source callsign
curl http://localhost:8073/api/v1/aprs/packets?src=OE1ABC

# Get packets by destination callsign
curl http://localhost:8073/api/v1/aprs/packets?dst=APRS

# Get recent packets with explicit limit
curl http://localhost:8073/api/v1/aprs/packets?limit=200
```

---

## Data Models

### Radio

```typescript
interface Radio {
  id: number;                      // Unique identifier
  name: string;                    // Station name
  is_online: boolean;              // Online status
  manufacturer: string;            // Hardware manufacturer
  device: string | null;           // Device model
  firmware_version: string | null; // Firmware version
  firmware_date: string | null;    // ISO 8601 timestamp
  antenna: string | null;          // Antenna description
  modulation: string;              // e.g., "LoRa"
  protocol: string;                // e.g., "meshcore", "aprs"
  latitude: number | null;         // Decimal degrees
  longitude: number | null;        // Decimal degrees
  altitude: number | null;         // Meters
  frequency: number;               // Hz
  bandwidth: number;               // Hz
  power: number | null;            // dBm
  gain: number | null;             // dBi
  lora_sf: number | null;          // LoRa spreading factor (7-12)
  lora_cr: number | null;          // LoRa coding rate (5-8)
  extra: object | null;            // Additional metadata
  created_at: string;              // ISO 8601 timestamp
  updated_at: string;              // ISO 8601 timestamp
}
```

### MeshCorePacket

```typescript
interface MeshCorePacket {
  id: number;
  radio_id: number;
  radio: Radio | null;
  snr: number;                     // Signal-to-noise ratio (dB)
  rssi: number;                    // Received signal strength (dBm)
  version: number;
  route_type: number;
  payload_type: number;
  hash: string;                    // 16-char hex
  path: string;                    // Base64-encoded
  payload: string;                 // Base64-encoded
  raw: string;                     // Base64-encoded
  parsed: object | null;           // Depends on payload_type
  channel_hash: string;            // 2-char hex
  received_at: string;             // ISO 8601 timestamp
}
```

### APRSPacket

```typescript
interface APRSPacket {
  id: number;
  radio_id: number;
  radio: Radio;
  src: string;                     // Source callsign
  dst: string;                     // Destination callsign
  path: string;                    // Digipeater path
  comment: string;
  latitude: number | null;
  longitude: number | null;
  symbol: string;                  // APRS symbol table + code
  raw: string;                     // Raw APRS packet
  received_at: string;             // ISO 8601 timestamp
}
```

### MeshCoreNode

```typescript
interface MeshCoreNode {
  id: number;
  packet: MeshCorePacket[];
  name: string;
  type: number;                    // 0=repeater, 1=chat, 2=room, 3=sensor
  prefix: string;                  // 2-char network prefix
  public_key: string;              // 64-char hex Ed25519 public key
  first_heard_at: string;          // ISO 8601 timestamp
  last_heard_at: string;           // ISO 8601 timestamp
  last_latitude: number | null;
  last_longitude: number | null;
  distance?: number;               // Meters (proximity queries only)
}
```

### MeshCoreGroup

```typescript
interface MeshCoreGroup {
  id: number;
  name: string;                    // Max 32 characters
  secret: string;                  // 32-char hex
}
```

### MeshCoreStats

```typescript
interface MeshCoreStats {
  messages: number;
  nodes: number;
  receivers: number;
  packets: {
    timestamps: number[];          // Unix timestamps
    packets: number[];             // Packet counts
  };
}
```

---

## Examples

### JavaScript/TypeScript Client

```typescript
// Using fetch API
const API_BASE = 'http://localhost:8073/api/v1';

// Get all radios
async function getRadios(): Promise<Radio[]> {
  const response = await fetch(`${API_BASE}/radios`);
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}

// Get MeshCore nodes by type
async function getMeshCoreNodes(type?: string): Promise<MeshCoreNode[]> {
  const url = type
    ? `${API_BASE}/meshcore/nodes?type=${type}`
    : `${API_BASE}/meshcore/nodes`;
  const response = await fetch(url);
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}

// Find nearby nodes
async function getNodesNearby(publicKey: string, radius: number = 25000) {
  const response = await fetch(
    `${API_BASE}/meshcore/nodes/close-to/${publicKey}?radius=${radius}`
  );
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}
```

### Python Client

```python
import requests
from typing import List, Optional, Dict, Any

API_BASE = 'http://localhost:8073/api/v1'

def get_radios(protocol: Optional[str] = None) -> List[Dict[str, Any]]:
    """Get all radios or filter by protocol."""
    url = f"{API_BASE}/radios/{protocol}" if protocol else f"{API_BASE}/radios"
    response = requests.get(url)
    response.raise_for_status()
    return response.json()

def get_meshcore_packets(
    hash: Optional[str] = None,
    type: Optional[int] = None,
    channel_hash: Optional[str] = None
) -> List[Dict[str, Any]]:
    """Get MeshCore packets with optional filters."""
    params = {}
    if hash:
        params['hash'] = hash
    if type is not None:
        params['type'] = type
    if channel_hash:
        params['channel_hash'] = channel_hash

    response = requests.get(f"{API_BASE}/meshcore/packets", params=params)
    response.raise_for_status()
    return response.json()

def get_meshcore_stats() -> Dict[str, Any]:
    """Get MeshCore network statistics."""
    response = requests.get(f"{API_BASE}/meshcore")
    response.raise_for_status()
    return response.json()
```

### Go Client

```go
package hamview

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
)

const APIBase = "http://localhost:8073/api/v1"

type Client struct {
    BaseURL string
    HTTP    *http.Client
}

func NewClient() *Client {
    return &Client{
        BaseURL: APIBase,
        HTTP:    &http.Client{},
    }
}

func (c *Client) GetRadios() ([]Radio, error) {
    var radios []Radio
    err := c.get("/radios", &radios)
    return radios, err
}

func (c *Client) GetMeshCoreNodes(nodeType string) ([]MeshCoreNode, error) {
    path := "/meshcore/nodes"
    if nodeType != "" {
        path += "?type=" + url.QueryEscape(nodeType)
    }
    var nodes []MeshCoreNode
    err := c.get(path, &nodes)
    return nodes, err
}

func (c *Client) get(path string, result interface{}) error {
    resp, err := c.HTTP.Get(c.BaseURL + path)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("HTTP %d", resp.StatusCode)
    }

    return json.NewDecoder(resp.Body).Decode(result)
}
```

---

## Rate Limiting

Currently no rate limiting is implemented.

## Versioning

API version is specified in the URL path (`/api/v1`). Breaking changes will increment the version number.

## Support

For issues or questions, please refer to the project documentation at https://git.maze.io/ham/hamview
