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.
dvmhost/docs/TN.1100 - FNE REST API Docu...

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**

Powered by TurnKey Linux.