You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2351 lines
51 KiB
2351 lines
51 KiB
# DVM FNE REST API Technical Documentation
|
|
|
|
**Version:** 1.1
|
|
**Date:** December 6, 2025
|
|
**Author:** AI Assistant (based on source code analysis)
|
|
|
|
AI WARNING: This document was mainly generated using AI assistance. As such, there is the possibility of some error or inconsistency. Examples in Section 14 and Appendix C are *strictly* examples only for how the API *could* be used.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#1-overview)
|
|
2. [Authentication](#2-authentication)
|
|
3. [Common Endpoints](#3-common-endpoints)
|
|
4. [Peer Management](#4-peer-management)
|
|
5. [Radio ID (RID) Management](#5-radio-id-rid-management)
|
|
6. [Talkgroup (TGID) Management](#6-talkgroup-tgid-management)
|
|
7. [Peer List Management](#7-peer-list-management)
|
|
8. [Adjacent Site Map Management](#8-adjacent-site-map-management)
|
|
9. [System Operations](#9-system-operations)
|
|
10. [Protocol-Specific Operations](#10-protocol-specific-operations)
|
|
11. [Response Formats](#11-response-formats)
|
|
12. [Error Handling](#12-error-handling)
|
|
13. [Security Considerations](#13-security-considerations)
|
|
14. [Examples](#14-examples)
|
|
|
|
---
|
|
|
|
## 1. Overview
|
|
|
|
The DVM (Digital Voice Modem) FNE (Fixed Network Equipment) REST API provides a comprehensive interface for managing and monitoring FNE nodes in a distributed network. The API supports HTTP and HTTPS protocols and uses JSON for request and response payloads.
|
|
|
|
### 1.1 Base Configuration
|
|
|
|
**Default Ports:**
|
|
- HTTP: User-configurable (typically 9990)
|
|
- HTTPS: User-configurable (typically 9443)
|
|
|
|
**Transport:**
|
|
- Protocol: HTTP/1.1 or HTTPS
|
|
- Content-Type: `application/json`
|
|
- Character Encoding: UTF-8
|
|
|
|
**SSL/TLS Support:**
|
|
- Optional HTTPS with certificate-based security
|
|
- Configurable via `keyFile` and `certFile` parameters
|
|
- Uses OpenSSL when `ENABLE_SSL` is defined
|
|
|
|
### 1.2 API Architecture
|
|
|
|
The REST API is built on:
|
|
- **Request Dispatcher:** Routes HTTP requests to appropriate handlers
|
|
- **HTTP/HTTPS Server:** Handles network connections
|
|
- **Authentication Layer:** Token-based authentication using SHA-256
|
|
- **Lookup Tables:** Radio ID, Talkgroup Rules, Peer List, Adjacent Site Map
|
|
|
|
---
|
|
|
|
## 2. Authentication
|
|
|
|
All API endpoints (except `/auth`) require authentication using a token-based system.
|
|
|
|
### 2.1 Authentication Flow
|
|
|
|
1. Client sends password hash to `/auth` endpoint
|
|
2. Server validates password and returns authentication token
|
|
3. Client includes token in `X-DVM-Auth-Token` header for subsequent requests
|
|
4. Tokens are bound to client IP/host and remain valid for the session
|
|
|
|
### 2.2 Endpoint: PUT /auth
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Authenticate with the FNE REST API and obtain an authentication token.
|
|
|
|
**Request Headers:**
|
|
```
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"auth": "sha256_hash_of_password_in_hex"
|
|
}
|
|
```
|
|
|
|
**Password Hash Format:**
|
|
- Algorithm: SHA-256
|
|
- Encoding: Hexadecimal string (64 characters)
|
|
- Example: `"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"` (hash of "password")
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"token": "12345678901234567890"
|
|
}
|
|
```
|
|
|
|
**Response (Failure):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "invalid password"
|
|
}
|
|
```
|
|
|
|
**Error Conditions:**
|
|
- `400 Bad Request`: Invalid password, malformed auth string, or invalid characters
|
|
- `401 Unauthorized`: Authentication failed
|
|
|
|
**Notes:**
|
|
- Password must be pre-hashed with SHA-256 on client side
|
|
- Token is a 64-bit unsigned integer represented as a string
|
|
- Tokens are invalidated when:
|
|
- Client authenticates again
|
|
- Server explicitly invalidates the token
|
|
- Server restarts
|
|
|
|
**Example (bash with curl):**
|
|
```bash
|
|
# Generate SHA-256 hash of password
|
|
PASSWORD="your_password_here"
|
|
HASH=$(echo -n "$PASSWORD" | sha256sum | cut -d' ' -f1)
|
|
|
|
# Authenticate
|
|
TOKEN=$(curl -X PUT http://fne.example.com:9990/auth \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"auth\":\"$HASH\"}" | jq -r '.token')
|
|
|
|
echo "Token: $TOKEN"
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Common Endpoints
|
|
|
|
### 3.1 Endpoint: GET /version
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Retrieve FNE software version information.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"version": "Digital Voice Modem (DVM) Converged FNE 4.0.0 (built Dec 03 2025 12:00:00)"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Returns program name, version, and build timestamp
|
|
- Useful for compatibility checks and diagnostics
|
|
|
|
---
|
|
|
|
### 3.2 Endpoint: GET /status
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Retrieve current FNE system status and configuration.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"state": 1,
|
|
"dmrEnabled": true,
|
|
"p25Enabled": true,
|
|
"nxdnEnabled": false,
|
|
"peerId": 10001
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
- `state`: Current FNE state (1 = running)
|
|
- `dmrEnabled`: Whether DMR protocol is enabled
|
|
- `p25Enabled`: Whether P25 protocol is enabled
|
|
- `nxdnEnabled`: Whether NXDN protocol is enabled
|
|
- `peerId`: This FNE's peer ID
|
|
|
|
---
|
|
|
|
## 4. Peer Management
|
|
|
|
### 4.1 Endpoint: GET /peer/query
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Query all connected peers and their status.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"peers": [
|
|
{
|
|
"peerId": 10001,
|
|
"address": "192.168.1.100",
|
|
"port": 54321,
|
|
"connected": true,
|
|
"connectionState": 4,
|
|
"pingsReceived": 120,
|
|
"lastPing": 1701619200,
|
|
"controlChannel": 0,
|
|
"config": {
|
|
"identity": "Site 1 Repeater",
|
|
"software": "dvmhost 4.0.0",
|
|
"sysView": false,
|
|
"externalPeer": false,
|
|
"masterPeerId": 0,
|
|
"conventionalPeer": false
|
|
},
|
|
"voiceChannels": [10002, 10003]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
- `peerId`: Unique peer identifier
|
|
- `address`: IP address of peer
|
|
- `port`: Network port of peer
|
|
- `connected`: Connection status (true/false)
|
|
- `connectionState`: Connection state value (0=INVALID, 1=WAITING_LOGIN, 2=WAITING_AUTH, 3=WAITING_CONFIG, 4=RUNNING)
|
|
- `pingsReceived`: Number of pings received from peer
|
|
- `lastPing`: Unix timestamp of last ping received
|
|
- `controlChannel`: Control channel peer ID (0 if this peer is a control channel, or peer ID of associated control channel)
|
|
- `config`: Peer configuration object
|
|
- `identity`: Peer description/name
|
|
- `software`: Peer software version string
|
|
- `sysView`: Whether peer is a SysView monitoring peer
|
|
- `externalPeer`: Whether peer is a downstream neighbor FNE peer
|
|
- `masterPeerId`: Master peer ID (for neighbor FNE peers)
|
|
- `conventionalPeer`: Whether peer is a conventional (non-trunked) peer
|
|
- `voiceChannels`: Array of voice channel peer IDs associated with this control channel (empty if not a control channel)
|
|
|
|
---
|
|
|
|
### 4.2 Endpoint: GET /peer/count
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Get count of connected peers.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"peerCount": 5
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4.3 Endpoint: PUT /peer/reset
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Reset (disconnect and reconnect) a specific peer.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"peerId": 10001
|
|
}
|
|
```
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerId was not a valid integer"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Forces peer disconnect and requires re-authentication
|
|
- Useful for recovering from stuck connections
|
|
- Peer will need to complete RPTL/RPTK/RPTC sequence again
|
|
- Returns 200 OK even if the peer ID does not exist (check server logs for actual result)
|
|
|
|
---
|
|
|
|
### 4.4 Endpoint: PUT /peer/connreset
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Reset the FNE's upstream peer connection (if FNE is operating as a child node).
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"peerId": 10001
|
|
}
|
|
```
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerId was not a valid integer"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Only applicable when FNE is configured as a child node with upstream peer connections
|
|
- Disconnects from specified upstream peer and attempts reconnection
|
|
- Used for recovering from upstream connection issues
|
|
- The `peerId` must match an upstream peer connection configured in the FNE
|
|
|
|
---
|
|
|
|
## 5. Radio ID (RID) Management
|
|
|
|
The Radio ID (RID) management endpoints allow dynamic modification of the radio ID whitelist/blacklist.
|
|
|
|
### 5.1 Endpoint: GET /rid/query
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Query all radio IDs in the lookup table.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"rids": [
|
|
{
|
|
"id": 123456,
|
|
"enabled": true,
|
|
"alias": "Unit 1"
|
|
},
|
|
{
|
|
"id": 789012,
|
|
"enabled": false,
|
|
"alias": "Unit 2"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
- `id`: Radio ID (subscriber ID)
|
|
- `enabled`: Whether radio is enabled (whitelisted)
|
|
- `alias`: Radio alias/name
|
|
|
|
**Notes:**
|
|
- Returns all radio IDs in the lookup table (no filtering available)
|
|
- Empty `alias` field will be returned as empty string if not set
|
|
|
|
---
|
|
|
|
### 5.2 Endpoint: PUT /rid/add
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Add or update a radio ID in the lookup table.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"rid": 123456,
|
|
"enabled": true,
|
|
"alias": "Unit 1"
|
|
}
|
|
```
|
|
|
|
**Request Fields:**
|
|
- `rid` (required): Radio ID (subscriber ID)
|
|
- `enabled` (required): Whether radio is enabled (whitelisted)
|
|
- `alias` (optional): Radio alias/name
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "rid was not a valid integer"
|
|
}
|
|
```
|
|
or
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "enabled was not a valid boolean"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Changes are in-memory only until `/rid/commit` is called
|
|
- If radio ID already exists, it will be updated
|
|
- `enabled: false` effectively blacklists the radio
|
|
- `alias` field is optional and defaults to empty string if not provided
|
|
|
|
---
|
|
|
|
### 5.3 Endpoint: PUT /rid/delete
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Remove a radio ID from the lookup table.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"rid": 123456
|
|
}
|
|
```
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "rid was not a valid integer"
|
|
}
|
|
```
|
|
|
|
**Response (Failure - RID Not Found):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "failed to find specified RID to delete"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Changes are in-memory only until `/rid/commit` is called
|
|
- Returns error if the specified RID does not exist in the lookup table
|
|
|
|
---
|
|
|
|
### 5.4 Endpoint: GET /rid/commit
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Commit all radio ID changes to disk.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Writes current in-memory state to configured RID file
|
|
- Changes persist across FNE restarts after commit
|
|
- Recommended workflow: Add/Delete multiple RIDs, then commit once
|
|
|
|
---
|
|
|
|
## 6. Talkgroup (TGID) Management
|
|
|
|
Talkgroup management endpoints control talkgroup rules, affiliations, and routing.
|
|
|
|
### 6.1 Endpoint: GET /tg/query
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Query all talkgroup rules.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"tgs": [
|
|
{
|
|
"name": "TAC 1",
|
|
"alias": "Tactical 1",
|
|
"invalid": false,
|
|
"source": {
|
|
"tgid": 1,
|
|
"slot": 1
|
|
},
|
|
"config": {
|
|
"active": true,
|
|
"affiliated": false,
|
|
"parrot": false,
|
|
"inclusion": [],
|
|
"exclusion": [],
|
|
"rewrite": [],
|
|
"always": [],
|
|
"preferred": [],
|
|
"permittedRids": []
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
|
|
**Top-Level Fields:**
|
|
- `name`: Talkgroup name/description
|
|
- `alias`: Short alias for talkgroup
|
|
- `invalid`: Whether talkgroup is marked invalid (disabled)
|
|
|
|
**Source Object:**
|
|
- `tgid`: Talkgroup ID number
|
|
- `slot`: TDMA slot (1 or 2 for DMR, typically 1 for P25/NXDN)
|
|
|
|
**Config Object:**
|
|
- `active`: Whether talkgroup is currently active
|
|
- `affiliated`: Requires affiliation before use
|
|
- `parrot`: Echo mode (transmit back to source)
|
|
- `inclusion`: Array of peer IDs that should receive this talkgroup
|
|
- `exclusion`: Array of peer IDs that should NOT receive this talkgroup
|
|
- `rewrite`: Array of rewrite rules (source peer → destination TGID mappings)
|
|
- `always`: Array of peer IDs that always receive this talkgroup
|
|
- `preferred`: Array of preferred peer IDs for this talkgroup
|
|
- `permittedRids`: Array of radio IDs permitted to use this talkgroup
|
|
|
|
**Rewrite Rule Format:**
|
|
```json
|
|
{
|
|
"peerid": 10001,
|
|
"tgid": 2,
|
|
"slot": 1
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Returns all talkgroup rules (no filtering available)
|
|
|
|
---
|
|
|
|
### 6.2 Endpoint: PUT /tg/add
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Add or update a talkgroup rule.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"name": "TAC 1",
|
|
"alias": "Tactical 1",
|
|
"source": {
|
|
"tgid": 1,
|
|
"slot": 1
|
|
},
|
|
"config": {
|
|
"active": true,
|
|
"affiliated": false,
|
|
"parrot": false,
|
|
"inclusion": [10001, 10002],
|
|
"exclusion": [],
|
|
"rewrite": [],
|
|
"always": [],
|
|
"preferred": [],
|
|
"permittedRids": []
|
|
}
|
|
}
|
|
```
|
|
|
|
**Request Fields:**
|
|
- `name` (required): Talkgroup name/description
|
|
- `alias` (required): Short alias for talkgroup
|
|
- `source` (required): Source object containing:
|
|
- `tgid` (required): Talkgroup ID number
|
|
- `slot` (required): TDMA slot
|
|
- `config` (required): Configuration object containing:
|
|
- `active` (required): Whether talkgroup is active
|
|
- `affiliated` (required): Requires affiliation
|
|
- `parrot` (required): Echo mode
|
|
- `inclusion` (required): Array of peer IDs (can be empty)
|
|
- `exclusion` (required): Array of peer IDs (can be empty)
|
|
- `rewrite` (optional): Array of rewrite rules (can be empty)
|
|
- `always` (optional): Array of peer IDs (can be empty)
|
|
- `preferred` (optional): Array of peer IDs (can be empty)
|
|
- `permittedRids` (optional): Array of radio IDs (can be empty)
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "TG \"name\" was not a valid string"
|
|
}
|
|
```
|
|
or other validation error messages such as:
|
|
- `"TG \"alias\" was not a valid string"`
|
|
- `"TG \"source\" was not a valid JSON object"`
|
|
- `"TG source \"tgid\" was not a valid number"`
|
|
- `"TG source \"slot\" was not a valid number"`
|
|
- `"TG \"config\" was not a valid JSON object"`
|
|
- `"TG configuration \"active\" was not a valid boolean"`
|
|
- `"TG configuration \"affiliated\" was not a valid boolean"`
|
|
- `"TG configuration \"parrot\" slot was not a valid boolean"`
|
|
- `"TG configuration \"inclusion\" was not a valid JSON array"`
|
|
- And similar for other config arrays
|
|
|
|
**Notes:**
|
|
- Changes are in-memory only until `/tg/commit` is called
|
|
- If talkgroup already exists (same tgid+slot), it will be updated
|
|
- All fields are validated and errors are returned immediately if validation fails
|
|
|
|
---
|
|
|
|
### 6.3 Endpoint: PUT /tg/delete
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Remove a talkgroup rule.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"tgid": 1,
|
|
"slot": 1
|
|
}
|
|
```
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "tgid was not a valid integer"
|
|
}
|
|
```
|
|
or
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "slot was not a valid char"
|
|
}
|
|
```
|
|
|
|
**Response (Failure - TGID Not Found):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "failed to find specified TGID to delete"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Changes are in-memory only until `/tg/commit` is called
|
|
- Returns error if the specified talkgroup (tgid+slot combination) does not exist
|
|
|
|
---
|
|
|
|
### 6.4 Endpoint: GET /tg/commit
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Commit all talkgroup changes to disk.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Write Error):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "failed to write new TGID file"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Writes current in-memory state to configured talkgroup rules file
|
|
- Changes persist across FNE restarts after commit
|
|
- Returns error if file write operation fails
|
|
|
|
---
|
|
|
|
## 7. Peer List Management
|
|
|
|
Peer list management controls the authorized peer database for spanning tree configuration.
|
|
|
|
### 7.1 Endpoint: GET /peer/list
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Query authorized peer list.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"peers": [
|
|
{
|
|
"peerId": 10001,
|
|
"peerAlias": "Site 1 Repeater",
|
|
"peerPassword": true,
|
|
"peerReplica": false,
|
|
"canRequestKeys": false,
|
|
"canIssueInhibit": false,
|
|
"hasCallPriority": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
- `peerId`: Unique peer identifier
|
|
- `peerAlias`: Peer description/name/alias
|
|
- `peerPassword`: Whether peer has a password configured (true/false)
|
|
- `peerReplica`: Whether peer participates in peer replication
|
|
- `canRequestKeys`: Whether peer can request encryption keys
|
|
- `canIssueInhibit`: Whether peer can issue radio inhibit commands
|
|
- `hasCallPriority`: Whether peer has call priority (can preempt other calls)
|
|
|
|
---
|
|
|
|
### 7.2 Endpoint: PUT /peer/add
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Add or update an authorized peer.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"peerId": 10001,
|
|
"peerAlias": "Site 1 Repeater",
|
|
"peerPassword": "secretpass",
|
|
"peerReplica": false,
|
|
"canRequestKeys": false,
|
|
"canIssueInhibit": false,
|
|
"hasCallPriority": false
|
|
}
|
|
```
|
|
|
|
**Request Fields:**
|
|
- `peerId` (required): Unique peer identifier
|
|
- `peerAlias` (optional): Peer description/name/alias
|
|
- `peerPassword` (optional): Peer authentication password (string, not boolean)
|
|
- `peerReplica` (optional): Whether peer participates in peer replication (default: false)
|
|
- `canRequestKeys` (optional): Whether peer can request encryption keys (default: false)
|
|
- `canIssueInhibit` (optional): Whether peer can issue radio inhibit commands (default: false)
|
|
- `hasCallPriority` (optional): Whether peer has call priority (default: false)
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerId was not a valid integer"
|
|
}
|
|
```
|
|
or
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerAlias was not a valid string"
|
|
}
|
|
```
|
|
or
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerPassword was not a valid string"
|
|
}
|
|
```
|
|
or
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerReplica was not a valid boolean"
|
|
}
|
|
```
|
|
or similar validation errors for `canRequestKeys`, `canIssueInhibit`, or `hasCallPriority`
|
|
|
|
**Notes:**
|
|
- Changes are in-memory only until `/peer/commit` is called
|
|
- `peerPassword` in the request is a string (the actual password), but in the GET response it's a boolean indicating whether a password is set
|
|
- If peer already exists, it will be updated
|
|
|
|
---
|
|
|
|
### 7.3 Endpoint: PUT /peer/delete
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Remove an authorized peer.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"peerId": 10001
|
|
}
|
|
```
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerId was not a valid integer"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Changes are in-memory only until `/peer/commit` is called
|
|
- Returns 200 OK even if the peer ID does not exist (no validation of existence)
|
|
|
|
---
|
|
|
|
### 7.4 Endpoint: GET /peer/commit
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Commit all peer list changes to disk.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Writes current in-memory state to configured peer list file
|
|
- Changes persist across FNE restarts after commit
|
|
|
|
---
|
|
|
|
## 8. Adjacent Site Map Management
|
|
|
|
Adjacent site map configuration controls peer-to-peer adjacency relationships for network topology.
|
|
|
|
### 8.1 Endpoint: GET /adjmap/list
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Query adjacent site mappings (peer neighbor relationships).
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"peers": [
|
|
{
|
|
"peerId": 10002,
|
|
"neighbors": [10001, 10003, 10004]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
- `peerId`: Peer ID for this entry
|
|
- `neighbors`: Array of peer IDs that are adjacent/neighboring to this peer
|
|
|
|
**Notes:**
|
|
- Returns all peer adjacency mappings
|
|
- Each entry defines which peers are neighbors of a given peer
|
|
|
|
---
|
|
|
|
### 8.2 Endpoint: PUT /adjmap/add
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Add or update an adjacent site mapping (peer neighbor relationship).
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"peerId": 10002,
|
|
"neighbors": [10001, 10003, 10004]
|
|
}
|
|
```
|
|
|
|
**Request Fields:**
|
|
- `peerId` (required): Peer ID for this entry
|
|
- `neighbors` (required): Array of peer IDs that are adjacent/neighboring to this peer
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerId was not a valid integer"
|
|
}
|
|
```
|
|
or
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "Peer \"neighbors\" was not a valid JSON array"
|
|
}
|
|
```
|
|
or
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "Peer neighbor value was not a valid number"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Changes are in-memory only until `/adjmap/commit` is called
|
|
- If adjacency entry for peer already exists, it will be updated
|
|
- Empty neighbors array is valid (peer has no neighbors)
|
|
|
|
---
|
|
|
|
### 8.3 Endpoint: PUT /adjmap/delete
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Remove an adjacent site mapping.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"peerId": 10002
|
|
}
|
|
```
|
|
|
|
**Response (Success):**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Response (Failure - Invalid Request):**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "peerId was not a valid integer"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Changes are in-memory only until `/adjmap/commit` is called
|
|
- Returns 200 OK even if the peer ID does not exist (no validation of existence)
|
|
|
|
---
|
|
|
|
### 8.4 Endpoint: GET /adjmap/commit
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Commit all adjacent site map changes to disk.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Writes current in-memory state to configured adjacent site map file
|
|
- Changes persist across FNE restarts after commit
|
|
|
|
---
|
|
|
|
## 9. System Operations
|
|
|
|
### 9.1 Endpoint: GET /force-update
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Force immediate update of all connected peers with current configuration.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Triggers immediate REPL (replication) messages to all peers
|
|
- Sends updated talkgroup rules, radio ID lists, and peer lists
|
|
- Useful after making configuration changes
|
|
|
|
---
|
|
|
|
### 9.2 Endpoint: GET /reload-tgs
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Reload talkgroup rules from disk.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Discards in-memory changes
|
|
- Reloads from configured talkgroup rules file
|
|
- Useful for reverting uncommitted changes
|
|
|
|
---
|
|
|
|
### 9.3 Endpoint: GET /reload-rids
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Reload radio IDs from disk.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Discards in-memory changes
|
|
- Reloads from configured radio ID file
|
|
- Useful for reverting uncommitted changes
|
|
|
|
---
|
|
|
|
### 9.4 Endpoint: GET /reload-peers
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Reload authorized peer list from disk.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Discards in-memory changes to peer list
|
|
- Reloads from configured peer list file
|
|
- Useful for reverting uncommitted changes to authorized peers
|
|
|
|
---
|
|
|
|
### 9.5 Endpoint: GET /reload-crypto
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Reload cryptographic keys from disk.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Reloads encryption keys from configured crypto key file
|
|
- Used to update encryption keys without restarting the FNE
|
|
- Applies to DMR, P25, and NXDN encryption key tables
|
|
|
|
---
|
|
|
|
### 9.6 Endpoint: GET /stats
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Get FNE statistics and metrics including peer status, table load times, and call counts.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"peerStats": [
|
|
{
|
|
"peerId": 10001,
|
|
"masterId": 10001,
|
|
"address": "192.168.1.100",
|
|
"port": 54321,
|
|
"lastPing": "Fri Dec 6 10:30:45 2025",
|
|
"pingsReceived": 1234,
|
|
"missedMetadataUpdates": 0,
|
|
"isNeighbor": false,
|
|
"isReplica": false
|
|
}
|
|
],
|
|
"tableLastLoad": {
|
|
"ridLastLoadTime": "Fri Dec 6 08:15:30 2025",
|
|
"tgLastLoadTime": "Fri Dec 6 08:15:30 2025",
|
|
"peerListLastLoadTime": "Fri Dec 6 08:15:30 2025",
|
|
"adjSiteMapLastLoadTime": "Fri Dec 6 08:15:30 2025",
|
|
"cryptoKeyLastLoadTime": "Fri Dec 6 08:15:30 2025"
|
|
},
|
|
"totalCallsProcessed": 5678,
|
|
"ridTotalEntries": 150,
|
|
"tgTotalEntries": 45,
|
|
"peerListTotalEntries": 8,
|
|
"adjSiteMapTotalEntries": 6,
|
|
"cryptoKeyTotalEntries": 12
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
|
|
**peerStats[]** - Array of peer statistics:
|
|
- `peerId`: Unique peer identifier
|
|
- `masterId`: Master peer ID
|
|
- `address`: IP address of peer
|
|
- `port`: Network port of peer
|
|
- `lastPing`: Last ping timestamp (human-readable format)
|
|
- `pingsReceived`: Total pings received from this peer
|
|
- `missedMetadataUpdates`: Number of missed metadata updates
|
|
- `isNeighbor`: Whether this is a neighbor FNE peer
|
|
- `isReplica`: Whether this peer participates in replication
|
|
|
|
**tableLastLoad** - Lookup table load timestamps:
|
|
- `ridLastLoadTime`: Radio ID table last load time (human-readable format)
|
|
- `tgLastLoadTime`: Talkgroup table last load time (human-readable format)
|
|
- `peerListLastLoadTime`: Peer list table last load time (human-readable format)
|
|
- `adjSiteMapLastLoadTime`: Adjacent site map table last load time (human-readable format)
|
|
- `cryptoKeyLastLoadTime`: Crypto key table last load time (human-readable format)
|
|
|
|
**Statistics Totals:**
|
|
- `totalCallsProcessed`: Total number of calls processed since FNE startup
|
|
- `ridTotalEntries`: Total entries in radio ID lookup table
|
|
- `tgTotalEntries`: Total entries in talkgroup rules table
|
|
- `peerListTotalEntries`: Total entries in authorized peer list
|
|
- `adjSiteMapTotalEntries`: Total entries in adjacent site map
|
|
- `cryptoKeyTotalEntries`: Total encryption keys loaded
|
|
|
|
**Notes:**
|
|
- Statistics are reset on FNE restart
|
|
- Timestamp fields use `ctime` format (e.g., "Fri Dec 6 10:30:45 2025")
|
|
- Useful for monitoring FNE health, performance, and peer connectivity
|
|
- `peerStats` array contains one entry per connected peer
|
|
- Table load times help identify when configuration files were last reloaded
|
|
|
|
---
|
|
|
|
### 9.7 Endpoint: GET /report-affiliations
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Get current radio affiliations across all peers.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"affiliations": [
|
|
{
|
|
"peerId": 10001,
|
|
"affiliations": [
|
|
{
|
|
"srcId": 123456,
|
|
"dstId": 1
|
|
},
|
|
{
|
|
"srcId": 789012,
|
|
"dstId": 2
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"peerId": 10002,
|
|
"affiliations": [
|
|
{
|
|
"srcId": 345678,
|
|
"dstId": 1
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
- `affiliations[]`: Array of peer affiliation records
|
|
- `peerId`: Peer ID where affiliations are registered
|
|
- `affiliations[]`: Array of affiliation records for this peer
|
|
- `srcId`: Radio ID (subscriber ID)
|
|
- `dstId`: Talkgroup ID the radio is affiliated to
|
|
|
|
**Notes:**
|
|
- Affiliations are grouped by peer ID
|
|
- Each peer may have multiple radio affiliations
|
|
- Empty peers (no affiliations) are not included in the response
|
|
|
|
---
|
|
|
|
### 9.8 Endpoint: GET /spanning-tree
|
|
|
|
**Method:** `GET`
|
|
|
|
**Description:** Get current network spanning tree topology.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"masterTree": [
|
|
{
|
|
"id": 1,
|
|
"masterId": 1,
|
|
"identity": "Master FNE",
|
|
"children": [
|
|
{
|
|
"id": 10001,
|
|
"masterId": 10001,
|
|
"identity": "Site 1 FNE",
|
|
"children": [
|
|
{
|
|
"id": 20001,
|
|
"masterId": 20001,
|
|
"identity": "Site 1 Repeater",
|
|
"children": []
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 10002,
|
|
"masterId": 10002,
|
|
"identity": "Site 2 FNE",
|
|
"children": []
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response Fields:**
|
|
- `masterTree[]`: Array containing the root tree node (typically one element)
|
|
- `id`: Peer ID of this node
|
|
- `masterId`: Master peer ID (usually same as id for FNE nodes)
|
|
- `identity`: Peer identity string
|
|
- `children[]`: Array of child tree nodes with same structure (recursive)
|
|
|
|
**Notes:**
|
|
- Shows hierarchical network structure as a tree
|
|
- The root node represents the master FNE at the top of the tree
|
|
- Each node can have multiple children forming a hierarchical structure
|
|
- Leaf peers (dvmhost, dvmbridge) have empty children arrays
|
|
- FNE nodes with connected peers will have non-empty children arrays
|
|
- Useful for visualizing network topology and detecting duplicate connections
|
|
|
|
---
|
|
|
|
## 10. Protocol-Specific Operations
|
|
|
|
### 10.1 DMR Operations
|
|
|
|
#### 10.1.1 Endpoint: PUT /dmr/rid
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Execute DMR-specific radio ID operations.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"peerId": 10001,
|
|
"command": "check",
|
|
"dstId": 123456,
|
|
"slot": 1
|
|
}
|
|
```
|
|
|
|
**Request Parameters:**
|
|
- `peerId` (optional, integer): Target peer ID. Defaults to 0 (broadcast to all peers)
|
|
- `command` (required, string): Command to execute (see supported commands below)
|
|
- `dstId` (required, integer): Target radio ID
|
|
- `slot` (required, integer): DMR TDMA slot number (1 or 2)
|
|
|
|
**Supported Commands:**
|
|
- `page`: Send call alert (page) to radio
|
|
- `check`: Radio check
|
|
- `inhibit`: Radio inhibit
|
|
- `uninhibit`: Radio un-inhibit
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"message": "OK"
|
|
}
|
|
```
|
|
|
|
**Error Responses:**
|
|
- `400 Bad Request`: If command, dstId, or slot is missing or invalid
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "command was not valid"
|
|
}
|
|
```
|
|
- `400 Bad Request`: If slot is 0 or greater than 2
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "invalid DMR slot number (slot == 0 or slot > 3)"
|
|
}
|
|
```
|
|
- `400 Bad Request`: If command is not recognized
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "invalid command"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Commands are sent to specified peer or broadcast to all peers if peerId is 0
|
|
- `slot` parameter must be 1 or 2 for DMR TDMA slots
|
|
- Radio must be registered/affiliated on peer
|
|
|
|
---
|
|
|
|
### 10.2 P25 Operations
|
|
|
|
#### 10.2.1 Endpoint: PUT /p25/rid
|
|
|
|
**Method:** `PUT`
|
|
|
|
**Description:** Execute P25-specific radio ID operations.
|
|
|
|
**Request Headers:**
|
|
```
|
|
X-DVM-Auth-Token: {token}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"peerId": 10001,
|
|
"command": "check",
|
|
"dstId": 123456
|
|
}
|
|
```
|
|
|
|
**Request Parameters:**
|
|
- `peerId` (optional, integer): Target peer ID. Defaults to 0 (broadcast to all peers)
|
|
- `command` (required, string): Command to execute (see supported commands below)
|
|
- `dstId` (required, integer): Target radio ID
|
|
- `tgId` (required for `dyn-regrp` only, integer): Target talkgroup ID for dynamic regroup
|
|
|
|
**Supported Commands:**
|
|
- `page`: Send call alert (page) to radio
|
|
- `check`: Radio check
|
|
- `inhibit`: Radio inhibit
|
|
- `uninhibit`: Radio un-inhibit
|
|
- `dyn-regrp`: Dynamic regroup (requires `tgId` parameter)
|
|
- `dyn-regrp-cancel`: Cancel dynamic regroup
|
|
- `dyn-regrp-lock`: Lock dynamic regroup
|
|
- `dyn-regrp-unlock`: Unlock dynamic regroup
|
|
- `group-aff-req`: Group affiliation query
|
|
- `unit-reg`: Unit registration request
|
|
|
|
**Example with tgId (dynamic regroup):**
|
|
```json
|
|
{
|
|
"peerId": 10001,
|
|
"command": "dyn-regrp",
|
|
"dstId": 123456,
|
|
"tgId": 1
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"message": "OK"
|
|
}
|
|
```
|
|
|
|
**Error Responses:**
|
|
- `400 Bad Request`: If command or dstId is missing or invalid
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "command was not valid"
|
|
}
|
|
```
|
|
- `400 Bad Request`: If tgId is missing for `dyn-regrp` command
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "talkgroup ID was not valid"
|
|
}
|
|
```
|
|
- `400 Bad Request`: If command is not recognized
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "invalid command"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Commands are sent via P25 TSDU (Trunking System Data Unit) messages
|
|
- Commands are sent to specified peer or broadcast to all peers if peerId is 0
|
|
- Radio must be registered on the P25 system
|
|
- The `dyn-regrp` command requires the `tgId` parameter to specify target talkgroup
|
|
- No `slot` parameter is used for P25 (unlike DMR)
|
|
|
|
---
|
|
|
|
## 11. Response Formats
|
|
|
|
### 11.1 Standard Success Response
|
|
|
|
All successful API calls return HTTP 200 with a JSON object containing at minimum:
|
|
|
|
```json
|
|
{
|
|
"status": 200
|
|
}
|
|
```
|
|
|
|
Some endpoints include an additional message field:
|
|
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"message": "OK"
|
|
}
|
|
```
|
|
|
|
Data-returning endpoints add additional fields based on the endpoint (e.g., `version`, `peers`, `talkgroups`, `affiliations`, etc.).
|
|
|
|
### 11.2 Standard Error Response
|
|
|
|
Error responses include HTTP status code and JSON error object:
|
|
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "descriptive error message"
|
|
}
|
|
```
|
|
|
|
**HTTP Status Codes:**
|
|
- `200 OK`: Request successful
|
|
- `400 Bad Request`: Invalid request format or parameters
|
|
- `401 Unauthorized`: Missing or invalid authentication token
|
|
- `404 Not Found`: Endpoint does not exist (not commonly used by FNE)
|
|
- `500 Internal Server Error`: Server-side error (rare)
|
|
|
|
---
|
|
|
|
## 12. Error Handling
|
|
|
|
### 12.1 Authentication Errors
|
|
|
|
**Missing Token:**
|
|
```json
|
|
{
|
|
"status": 401,
|
|
"message": "no authentication token"
|
|
}
|
|
```
|
|
|
|
**Invalid Token (wrong token value for host):**
|
|
```json
|
|
{
|
|
"status": 401,
|
|
"message": "invalid authentication token"
|
|
}
|
|
```
|
|
|
|
**Illegal Token (host not authenticated):**
|
|
```json
|
|
{
|
|
"status": 401,
|
|
"message": "illegal authentication token"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Tokens are bound to the client's hostname/IP address
|
|
- An invalid token for a known host will devalidate that host's token
|
|
- An illegal token means the host hasn't authenticated yet or token expired
|
|
|
|
### 12.2 Validation Errors
|
|
|
|
**Invalid JSON:**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "JSON parse error: unexpected character at position X"
|
|
}
|
|
```
|
|
|
|
**Invalid Content-Type:**
|
|
|
|
When Content-Type is not `application/json`, the server returns a plain text error response:
|
|
```
|
|
HTTP/1.1 400 Bad Request
|
|
Content-Type: text/plain
|
|
|
|
Invalid Content-Type. Expected: application/json
|
|
```
|
|
|
|
**Not a JSON Object:**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "Request was not a valid JSON object."
|
|
}
|
|
```
|
|
|
|
**Missing or Invalid Required Fields:**
|
|
|
|
Examples of field validation errors:
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "command was not valid"
|
|
}
|
|
```
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "destination ID was not valid"
|
|
}
|
|
```
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "TG \"name\" was not a valid string"
|
|
}
|
|
```
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "TG source \"tgid\" was not a valid number"
|
|
}
|
|
```
|
|
|
|
### 12.3 Resource Errors
|
|
|
|
**Peer Not Found:**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "cannot find peer"
|
|
}
|
|
```
|
|
|
|
**Talkgroup Not Found:**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "cannot find talkgroup"
|
|
}
|
|
```
|
|
|
|
**Radio ID Not Found:**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "cannot find RID"
|
|
}
|
|
```
|
|
|
|
**Invalid Command:**
|
|
```json
|
|
{
|
|
"status": 400,
|
|
"message": "invalid command"
|
|
}
|
|
```
|
|
|
|
### 12.4 Error Handling Best Practices
|
|
|
|
1. **Always check the `status` field** in responses, not just HTTP status code
|
|
2. **Parse the `message` field** for human-readable error descriptions
|
|
3. **Handle 401 errors** by re-authenticating with a new token
|
|
4. **Validate inputs** client-side to minimize 400 errors
|
|
5. **Log error responses** for debugging and audit trails
|
|
|
|
---
|
|
|
|
## 13. Security Considerations
|
|
|
|
### 13.1 Password Security
|
|
|
|
- **Never send plaintext passwords:** Always hash with SHA-256 before transmission
|
|
- **Use HTTPS in production:** Prevents token interception
|
|
- **Rotate passwords regularly:** Change FNE password periodically
|
|
- **Strong passwords:** Use complex passwords (minimum 16 characters recommended)
|
|
|
|
### 13.2 Token Management
|
|
|
|
- **Tokens are session-based:** Bound to client IP/hostname
|
|
- **Token invalidation:** Tokens are invalidated on:
|
|
- Re-authentication
|
|
- Explicit invalidation
|
|
- Server restart
|
|
- **Token format:** 64-bit unsigned integer (not cryptographically secure by itself)
|
|
|
|
### 13.3 Network Security
|
|
|
|
- **Use HTTPS:** Enable SSL/TLS for production deployments
|
|
- **Firewall rules:** Restrict REST API access to trusted networks
|
|
- **Rate limiting:** Consider implementing rate limiting for brute-force protection
|
|
- **Audit logging:** Enable debug logging to track API access
|
|
|
|
### 13.4 SSL/TLS Configuration
|
|
|
|
When using HTTPS, ensure:
|
|
- Valid SSL certificates (not self-signed for production)
|
|
- Strong cipher suites enabled
|
|
- TLS 1.2 or higher
|
|
- Certificate expiration monitoring
|
|
|
|
---
|
|
|
|
## 14. Examples
|
|
|
|
### 14.1 Complete Authentication and Query Flow
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
# Configuration
|
|
FNE_HOST="fne.example.com"
|
|
FNE_PORT="9990"
|
|
PASSWORD="your_password_here"
|
|
|
|
# Step 1: Generate password hash
|
|
echo "Generating password hash..."
|
|
HASH=$(echo -n "$PASSWORD" | sha256sum | cut -d' ' -f1)
|
|
|
|
# Step 2: Authenticate
|
|
echo "Authenticating..."
|
|
AUTH_RESPONSE=$(curl -s -X PUT "http://${FNE_HOST}:${FNE_PORT}/auth" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"auth\":\"$HASH\"}")
|
|
|
|
TOKEN=$(echo "$AUTH_RESPONSE" | jq -r '.token')
|
|
|
|
if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then
|
|
echo "Authentication failed!"
|
|
echo "$AUTH_RESPONSE"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Authenticated successfully. Token: $TOKEN"
|
|
|
|
# Step 3: Get version
|
|
echo -e "\nGetting version..."
|
|
curl -s -X GET "http://${FNE_HOST}:${FNE_PORT}/version" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" | jq
|
|
|
|
# Step 4: Get status
|
|
echo -e "\nGetting status..."
|
|
curl -s -X GET "http://${FNE_HOST}:${FNE_PORT}/status" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" | jq
|
|
|
|
# Step 5: Query peers
|
|
echo -e "\nQuerying peers..."
|
|
curl -s -X GET "http://${FNE_HOST}:${FNE_PORT}/peer/query" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" | jq
|
|
```
|
|
|
|
### 14.2 Add Talkgroup with Inclusion List
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
TOKEN="your_token_here"
|
|
FNE_HOST="fne.example.com"
|
|
FNE_PORT="9990"
|
|
|
|
# Add talkgroup
|
|
curl -X PUT "http://${FNE_HOST}:${FNE_PORT}/tg/add" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"name": "Emergency Services",
|
|
"alias": "EMERG",
|
|
"source": {
|
|
"tgid": 9999,
|
|
"slot": 1
|
|
},
|
|
"config": {
|
|
"active": true,
|
|
"affiliated": true,
|
|
"parrot": false,
|
|
"inclusion": [10001, 10002, 10003],
|
|
"exclusion": [],
|
|
"rewrite": [],
|
|
"always": [10001],
|
|
"preferred": [],
|
|
"permittedRids": [100, 101, 102, 103]
|
|
}
|
|
}' | jq
|
|
|
|
# Commit changes
|
|
curl -X GET "http://${FNE_HOST}:${FNE_PORT}/tg/commit" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" | jq
|
|
|
|
# Force update to all peers
|
|
curl -X GET "http://${FNE_HOST}:${FNE_PORT}/force-update" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" | jq
|
|
```
|
|
|
|
### 14.3 Radio ID Whitelist Management
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
TOKEN="your_token_here"
|
|
FNE_HOST="fne.example.com"
|
|
FNE_PORT="9990"
|
|
|
|
# Add multiple radio IDs
|
|
RADIO_IDS=(123456 234567 345678 456789)
|
|
|
|
for RID in "${RADIO_IDS[@]}"; do
|
|
echo "Adding RID: $RID"
|
|
curl -X PUT "http://${FNE_HOST}:${FNE_PORT}/rid/add" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"rid\":$RID,\"enabled\":true}" | jq
|
|
done
|
|
|
|
# Commit all changes
|
|
echo "Committing changes..."
|
|
curl -X GET "http://${FNE_HOST}:${FNE_PORT}/rid/commit" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" | jq
|
|
|
|
# Query to verify
|
|
echo "Verifying..."
|
|
curl -X GET "http://${FNE_HOST}:${FNE_PORT}/rid/query" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" | jq
|
|
```
|
|
|
|
### 14.4 P25 Radio Operations
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
TOKEN="your_token_here"
|
|
FNE_HOST="fne.example.com"
|
|
FNE_PORT="9990"
|
|
|
|
# Send radio check to radio 123456 via peer 10001
|
|
curl -X PUT "http://${FNE_HOST}:${FNE_PORT}/p25/rid" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"peerId": 10001,
|
|
"command": "check",
|
|
"dstId": 123456
|
|
}' | jq
|
|
|
|
# Send page to radio 123456
|
|
curl -X PUT "http://${FNE_HOST}:${FNE_PORT}/p25/rid" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"peerId": 10001,
|
|
"command": "page",
|
|
"dstId": 123456
|
|
}' | jq
|
|
|
|
# Dynamic regroup radio 123456 to talkgroup 5000
|
|
curl -X PUT "http://${FNE_HOST}:${FNE_PORT}/p25/rid" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"peerId": 10001,
|
|
"command": "dyn-regrp",
|
|
"dstId": 123456,
|
|
"tgId": 5000
|
|
}' | jq
|
|
|
|
# Cancel dynamic regroup for radio 123456
|
|
curl -X PUT "http://${FNE_HOST}:${FNE_PORT}/p25/rid" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"peerId": 10001,
|
|
"command": "dyn-regrp-cancel",
|
|
"dstId": 123456
|
|
}' | jq
|
|
|
|
# Send group affiliation query to radio 123456
|
|
curl -X PUT "http://${FNE_HOST}:${FNE_PORT}/p25/rid" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"peerId": 10001,
|
|
"command": "group-aff-req",
|
|
"dstId": 123456
|
|
}' | jq
|
|
```
|
|
|
|
### 14.5 Python Example with Requests Library
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
|
|
import requests
|
|
import hashlib
|
|
import json
|
|
|
|
class DVMFNEClient:
|
|
def __init__(self, host, port, password, use_https=False):
|
|
self.base_url = f"{'https' if use_https else 'http'}://{host}:{port}"
|
|
self.password = password
|
|
self.token = None
|
|
|
|
def authenticate(self):
|
|
"""Authenticate and get token"""
|
|
password_hash = hashlib.sha256(self.password.encode()).hexdigest()
|
|
|
|
response = requests.put(
|
|
f"{self.base_url}/auth",
|
|
json={"auth": password_hash}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
self.token = data.get('token')
|
|
return True
|
|
else:
|
|
print(f"Authentication failed: {response.text}")
|
|
return False
|
|
|
|
def _headers(self):
|
|
"""Get headers with auth token"""
|
|
return {
|
|
"X-DVM-Auth-Token": self.token,
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
def get_version(self):
|
|
"""Get FNE version"""
|
|
response = requests.get(
|
|
f"{self.base_url}/version",
|
|
headers=self._headers()
|
|
)
|
|
return response.json()
|
|
|
|
def get_peers(self):
|
|
"""Get connected peers"""
|
|
response = requests.get(
|
|
f"{self.base_url}/peer/query",
|
|
headers=self._headers()
|
|
)
|
|
return response.json()
|
|
|
|
def add_talkgroup(self, tgid, name, slot=1, active=True, affiliated=False):
|
|
"""Add a talkgroup"""
|
|
data = {
|
|
"name": name,
|
|
"alias": name[:8],
|
|
"source": {
|
|
"tgid": tgid,
|
|
"slot": slot
|
|
},
|
|
"config": {
|
|
"active": active,
|
|
"affiliated": affiliated,
|
|
"parrot": False,
|
|
"inclusion": [],
|
|
"exclusion": [],
|
|
"rewrite": [],
|
|
"always": [],
|
|
"preferred": [],
|
|
"permittedRids": []
|
|
}
|
|
}
|
|
|
|
response = requests.put(
|
|
f"{self.base_url}/tg/add",
|
|
headers=self._headers(),
|
|
json=data
|
|
)
|
|
return response.json()
|
|
|
|
def commit_talkgroups(self):
|
|
"""Commit talkgroup changes"""
|
|
response = requests.get(
|
|
f"{self.base_url}/tg/commit",
|
|
headers=self._headers()
|
|
)
|
|
return response.json()
|
|
|
|
def get_affiliations(self):
|
|
"""Get current affiliations"""
|
|
response = requests.get(
|
|
f"{self.base_url}/report-affiliations",
|
|
headers=self._headers()
|
|
)
|
|
return response.json()
|
|
|
|
# Example usage
|
|
if __name__ == "__main__":
|
|
# Create client
|
|
client = DVMFNEClient("fne.example.com", 9990, "your_password_here")
|
|
|
|
# Authenticate
|
|
if client.authenticate():
|
|
print("Authenticated successfully!")
|
|
|
|
# Get version
|
|
version = client.get_version()
|
|
print(f"FNE Version: {version['version']}")
|
|
|
|
# Get peers
|
|
peers = client.get_peers()
|
|
print(f"Connected peers: {len(peers.get('peers', []))}")
|
|
|
|
# Add talkgroup
|
|
result = client.add_talkgroup(100, "Test TG", slot=1, active=True)
|
|
print(f"Add talkgroup result: {result}")
|
|
|
|
# Commit
|
|
result = client.commit_talkgroups()
|
|
print(f"Commit result: {result}")
|
|
|
|
# Get affiliations
|
|
affs = client.get_affiliations()
|
|
print(f"Affiliations: {json.dumps(affs, indent=2)}")
|
|
else:
|
|
print("Authentication failed!")
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix A: Endpoint Summary Table
|
|
|
|
| Method | Endpoint | Description | Auth Required |
|
|
|--------|----------|-------------|---------------|
|
|
| PUT | /auth | Authenticate and get token | No |
|
|
| GET | /version | Get FNE version | Yes |
|
|
| GET | /status | Get FNE status | Yes |
|
|
| GET | /peer/query | Query connected peers | Yes |
|
|
| GET | /peer/count | Get peer count | Yes |
|
|
| PUT | /peer/reset | Reset peer connection | Yes |
|
|
| PUT | /peer/connreset | Reset upstream connection | Yes |
|
|
| GET | /rid/query | Query radio IDs | Yes |
|
|
| PUT | /rid/add | Add radio ID | Yes |
|
|
| PUT | /rid/delete | Delete radio ID | Yes |
|
|
| GET | /rid/commit | Commit radio ID changes | Yes |
|
|
| GET | /tg/query | Query talkgroups | Yes |
|
|
| PUT | /tg/add | Add talkgroup | Yes |
|
|
| PUT | /tg/delete | Delete talkgroup | Yes |
|
|
| GET | /tg/commit | Commit talkgroup changes | Yes |
|
|
| GET | /peer/list | Query peer list | Yes |
|
|
| PUT | /peer/add | Add authorized peer | Yes |
|
|
| PUT | /peer/delete | Delete authorized peer | Yes |
|
|
| GET | /peer/commit | Commit peer list changes | Yes |
|
|
| GET | /adjmap/list | Query adjacent site map | Yes |
|
|
| PUT | /adjmap/add | Add adjacent site | Yes |
|
|
| PUT | /adjmap/delete | Delete adjacent site | Yes |
|
|
| GET | /adjmap/commit | Commit adjacent site changes | Yes |
|
|
| GET | /force-update | Force peer updates | Yes |
|
|
| GET | /reload-tgs | Reload talkgroups from disk | Yes |
|
|
| GET | /reload-rids | Reload radio IDs from disk | Yes |
|
|
| GET | /reload-peers | Reload peer list from disk | Yes |
|
|
| GET | /reload-crypto | Reload crypto keys from disk | Yes |
|
|
| GET | /stats | Get FNE statistics | Yes |
|
|
| GET | /report-affiliations | Get affiliations | Yes |
|
|
| GET | /spanning-tree | Get network topology | Yes |
|
|
| PUT | /dmr/rid | DMR radio operations | Yes |
|
|
| PUT | /p25/rid | P25 radio operations | Yes |
|
|
|
|
---
|
|
|
|
## Appendix B: Configuration File Reference
|
|
|
|
### REST API Configuration (YAML)
|
|
|
|
```yaml
|
|
restApi:
|
|
# Enable REST API
|
|
enable: true
|
|
|
|
# Bind address (0.0.0.0 = all interfaces)
|
|
address: 0.0.0.0
|
|
|
|
# Port number
|
|
port: 9990
|
|
|
|
# SHA-256 hashed password (pre-hash before putting in config)
|
|
password: "your_secure_password"
|
|
|
|
# SSL/TLS Configuration (optional)
|
|
ssl:
|
|
enable: false
|
|
keyFile: /path/to/private.key
|
|
certFile: /path/to/certificate.crt
|
|
|
|
# Enable debug logging
|
|
debug: false
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix C: Common Use Cases
|
|
|
|
### C.1 Automated Peer Management
|
|
|
|
Monitor peer connections and automatically reset stuck peers:
|
|
|
|
```bash
|
|
# Get peer status
|
|
PEERS=$(curl -s -X GET "http://fne:9990/peer/query" \
|
|
-H "X-DVM-Auth-Token: $TOKEN")
|
|
|
|
# Check for peers with old lastPing
|
|
CURRENT_TIME=$(date +%s)
|
|
echo "$PEERS" | jq -r '.peers[] | select(.lastPing < ('$CURRENT_TIME' - 300)) | .peerId' | while read PEER_ID; do
|
|
echo "Resetting peer $PEER_ID (stale connection)"
|
|
curl -X PUT "http://fne:9990/peer/reset" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"peerId\":$PEER_ID}"
|
|
done
|
|
```
|
|
|
|
### C.2 Dynamic Talkgroup Provisioning
|
|
|
|
Automatically create talkgroups from external source:
|
|
|
|
```bash
|
|
# Read talkgroups from CSV file
|
|
# Format: TGID,Name,Slot,Affiliated
|
|
while IFS=',' read -r TGID NAME SLOT AFFILIATED; do
|
|
curl -X PUT "http://fne:9990/tg/add" \
|
|
-H "X-DVM-Auth-Token: $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"name\": \"$NAME\",
|
|
\"alias\": \"${NAME:0:8}\",
|
|
\"source\": {\"tgid\": $TGID, \"slot\": $SLOT},
|
|
\"config\": {
|
|
\"active\": true,
|
|
\"affiliated\": $AFFILIATED,
|
|
\"parrot\": false,
|
|
\"inclusion\": [],
|
|
\"exclusion\": [],
|
|
\"rewrite\": [],
|
|
\"always\": [],
|
|
\"preferred\": [],
|
|
\"permittedRids\": []
|
|
}
|
|
}"
|
|
done < talkgroups.csv
|
|
|
|
# Commit all changes
|
|
curl -X GET "http://fne:9990/tg/commit" \
|
|
-H "X-DVM-Auth-Token: $TOKEN"
|
|
```
|
|
|
|
### C.3 Affiliation Monitoring
|
|
|
|
Monitor and alert on specific affiliations:
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
import time
|
|
import requests
|
|
|
|
def monitor_affiliations(fne_host, port, token, watch_tgid):
|
|
"""Monitor affiliations for specific talkgroup"""
|
|
url = f"http://{fne_host}:{port}/report-affiliations"
|
|
headers = {"X-DVM-Auth-Token": token}
|
|
|
|
known_affiliations = set()
|
|
|
|
while True:
|
|
try:
|
|
response = requests.get(url, headers=headers)
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
|
|
# Flatten nested structure: affiliations is array of {peerId, affiliations[]}
|
|
current = set()
|
|
for peer_data in data.get('affiliations', []):
|
|
peer_id = peer_data.get('peerId')
|
|
for aff in peer_data.get('affiliations', []):
|
|
src_id = aff.get('srcId')
|
|
dst_id = aff.get('dstId')
|
|
if dst_id == watch_tgid:
|
|
current.add((src_id, peer_id))
|
|
|
|
# Detect new affiliations
|
|
new_affs = current - known_affiliations
|
|
for src_id, peer_id in new_affs:
|
|
print(f"NEW: Radio {src_id} affiliated to TG {watch_tgid} on peer {peer_id}")
|
|
|
|
# Detect removed affiliations
|
|
removed = known_affiliations - current
|
|
for src_id, peer_id in removed:
|
|
print(f"REMOVED: Radio {src_id} de-affiliated from TG {watch_tgid}")
|
|
|
|
known_affiliations = current
|
|
|
|
except Exception as e:
|
|
print(f"Error monitoring affiliations: {e}")
|
|
|
|
time.sleep(5)
|
|
|
|
# Example usage
|
|
monitor_affiliations("fne.example.com", 9990, "your_token", 1)
|
|
```
|
|
|
|
---
|
|
|
|
## Revision History
|
|
|
|
| Version | Date | Changes |
|
|
|---------|------|---------|
|
|
| 1.0 | Dec 3, 2025 | Initial documentation based on source code analysis |
|
|
| 1.1 | Dec 6, 2025 | Added missing endpoints: `/reload-peers`, `/reload-crypto`, `/stats` |
|
|
|
|
---
|
|
|
|
**End of Document**
|