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

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
  2. Authentication
  3. Common Endpoints
  4. Peer Management
  5. Radio ID (RID) Management
  6. Talkgroup (TGID) Management
  7. Peer List Management
  8. Adjacent Site Map Management
  9. System Operations
  10. Protocol-Specific Operations
  11. Response Formats
  12. Error Handling
  13. Security Considerations
  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:

{
  "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):

{
  "status": 200,
  "token": "12345678901234567890"
}

Response (Failure):

{
  "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):

# 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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "peerId": 10001
}

Response (Success):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "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:

{
  "peerId": 10001
}

Response (Success):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "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:

{
  "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:

{
  "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):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "status": 400,
  "message": "rid was not a valid integer"
}

or

{
  "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:

{
  "rid": 123456
}

Response (Success):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "status": 400,
  "message": "rid was not a valid integer"
}

Response (Failure - RID Not Found):

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "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:

{
  "tgid": 1,
  "slot": 1
}

Response (Success):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "status": 400,
  "message": "tgid was not a valid integer"
}

or

{
  "status": 400,
  "message": "slot was not a valid char"
}

Response (Failure - TGID Not Found):

{
  "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):

{
  "status": 200
}

Response (Failure - Write Error):

{
  "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:

{
  "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:

{
  "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):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "status": 400,
  "message": "peerId was not a valid integer"
}

or

{
  "status": 400,
  "message": "peerAlias was not a valid string"
}

or

{
  "status": 400,
  "message": "peerPassword was not a valid string"
}

or

{
  "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:

{
  "peerId": 10001
}

Response (Success):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "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:

{
  "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:

{
  "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:

{
  "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):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "status": 400,
  "message": "peerId was not a valid integer"
}

or

{
  "status": 400,
  "message": "Peer \"neighbors\" was not a valid JSON array"
}

or

{
  "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:

{
  "peerId": 10002
}

Response (Success):

{
  "status": 200
}

Response (Failure - Invalid Request):

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "status": 200,
  "message": "OK"
}

Error Responses:

  • 400 Bad Request: If command, dstId, or slot is missing or invalid
    {
      "status": 400,
      "message": "command was not valid"
    }
    
  • 400 Bad Request: If slot is 0 or greater than 2
    {
      "status": 400,
      "message": "invalid DMR slot number (slot == 0 or slot > 3)"
    }
    
  • 400 Bad Request: If command is not recognized
    {
      "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:

{
  "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):

{
  "peerId": 10001,
  "command": "dyn-regrp",
  "dstId": 123456,
  "tgId": 1
}

Response:

{
  "status": 200,
  "message": "OK"
}

Error Responses:

  • 400 Bad Request: If command or dstId is missing or invalid
    {
      "status": 400,
      "message": "command was not valid"
    }
    
  • 400 Bad Request: If tgId is missing for dyn-regrp command
    {
      "status": 400,
      "message": "talkgroup ID was not valid"
    }
    
  • 400 Bad Request: If command is not recognized
    {
      "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:

{
  "status": 200
}

Some endpoints include an additional message field:

{
  "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:

{
  "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:

{
  "status": 401,
  "message": "no authentication token"
}

Invalid Token (wrong token value for host):

{
  "status": 401,
  "message": "invalid authentication token"
}

Illegal Token (host not authenticated):

{
  "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:

{
  "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:

{
  "status": 400,
  "message": "Request was not a valid JSON object."
}

Missing or Invalid Required Fields:

Examples of field validation errors:

{
  "status": 400,
  "message": "command was not valid"
}
{
  "status": 400,
  "message": "destination ID was not valid"
}
{
  "status": 400,
  "message": "TG \"name\" was not a valid string"
}
{
  "status": 400,
  "message": "TG source \"tgid\" was not a valid number"
}

12.3 Resource Errors

Peer Not Found:

{
  "status": 400,
  "message": "cannot find peer"
}

Talkgroup Not Found:

{
  "status": 400,
  "message": "cannot find talkgroup"
}

Radio ID Not Found:

{
  "status": 400,
  "message": "cannot find RID"
}

Invalid Command:

{
  "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

#!/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

#!/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

#!/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

#!/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

#!/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)

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:

# 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:

# 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:

#!/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.