reorganize source files; add mirror functionality for data router and packet scheduler from P25 to DMR; add tests;

pull/133/head
Bryan Biedenkapp 1 month ago
parent 3060769f9b
commit 4d1300826c

@ -1,245 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file ConventionalDataService.h
* @ingroup p25_pdu
* @file ConventionalDataService.cpp
* @ingroup p25_pdu
*/
#if !defined(__P25_DATA__CONVENTIONALDATASERVICE_H__)
#define __P25_DATA__CONVENTIONALDATASERVICE_H__
#include "common/Defines.h"
#include "common/p25/data/ConventionalRegistration.h"
#include "common/p25/data/PacketDataState.h"
#include <cstdint>
#include <functional>
#include <unordered_map>
namespace p25
{
namespace data
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
* @addtogroup p25_pdu
* @{
*/
/**
* @brief Conventional packet data registration decision.
*/
enum class RegistrationDecision : uint8_t {
ACCEPT, //!< Registration request accepted.
DENY, //!< Registration request denied.
DISCONNECTED //!< Registration disconnected.
};
/**
* @brief Reason a conventional packet data registration was denied.
*/
enum class RegistrationDenyReason : uint8_t {
NONE, //!< No specific reason.
MALFORMED, //!< Registration request was malformed.
IDENTITY_MISMATCH, //!< Identity did not match expected value.
NOT_PROVISIONED, //!< Device not provisioned.
DISABLED, //!< Registration disabled.
UNSUPPORTED_OPTIONS, //!< Registration request contained unsupported options.
ADDRESS_UNAVAILABLE, //!< Requested address unavailable.
ADDRESS_CONFLICT //!< Address conflict detected.
};
/* @} */
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Provisioning inputs used to decide a registration request.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API ConventionalRegistrationProvisioning {
/**
* @brief Indicates whether the device is known.
*/
bool known = false;
/**
* @brief Indicates whether the device is enabled.
*/
bool enabled = false;
/**
* @brief Indicates whether dynamic address allocation is allowed.
*/
bool allowDynamicAddress = false;
/**
* @brief The static IP address of the device.
*/
uint32_t staticIPAddress = 0U;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Local policy applied to conventional registration requests.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API ConventionalRegistrationPolicy {
/**
* @brief Indicates whether CMS scan is allowed.
*/
bool allowCMSScan = false;
/**
* @brief The lifetime of a registration in milliseconds.
*/
uint64_t registrationLifetimeMs = 0U;
/**
* @brief Function to allocate a dynamic IP address.
*/
std::function<uint32_t(uint32_t, uint32_t)> allocateDynamicAddress;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Result of a conventional registration transaction.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API ConventionalRegistrationResult {
/**
* @brief The decision of the registration request.
*/
RegistrationDecision decision = RegistrationDecision::DENY;
/**
* @brief The reason for denying the registration request.
*/
RegistrationDenyReason denyReason = RegistrationDenyReason::MALFORMED;
/**
* @brief The response to the registration request.
*/
ConventionalRegistration response;
/**
* @brief Indicates whether the binding has changed as a result of the registration request.
*/
bool bindingChanged { false };
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements conventional packet data registration and binding state.
* @ingroup p25_pdu
*/
class DVM_COMMON_API ConventionalDataService {
public:
/**
* @brief Initializes a new instance of the ConventionalDataService class.
* @param bindings The data binding registry.
* @param links The data link manager.
* @param policy The conventional registration policy.
*/
ConventionalDataService(DataBindingRegistry& bindings, DataLinkManager& links,
const ConventionalRegistrationPolicy& policy = ConventionalRegistrationPolicy());
/**
* @brief Processes a conventional registration request.
* @param data The raw registration request data.
* @param length The length of the registration request data.
* @param headerLlId The logical link identifier from the header.
* @param provisioning The conventional registration provisioning information.
* @param nowMs The current time in milliseconds.
* @return The result of the registration request.
*/
ConventionalRegistrationResult process(const uint8_t* data, uint32_t length,
uint32_t headerLlId, const ConventionalRegistrationProvisioning& provisioning,
uint64_t nowMs = 0U);
/**
* @brief Installs a static registration for the specified logical link identifier.
* @param llId The logical link identifier.
* @param ipAddress The IP address associated with the registration.
* @param authorized Indicates whether the registration is authorized.
* @return True if the static registration was successfully installed; otherwise, false.
*/
bool installStatic(uint32_t llId, uint32_t ipAddress, bool authorized = true);
/**
* @brief Expires registrations that have passed their expiration time.
* @param nowMs The current time in milliseconds.
*/
void expire(uint64_t nowMs);
/**
* @brief Checks if the specified logical link identifier is registered.
* @param llId The logical link identifier.
* @return True if the logical link identifier is registered; otherwise, false.
*/
bool isRegistered(uint32_t llId) const;
/**
* @brief Gets the number of current registrations.
* @return The number of current registrations.
*/
size_t registrationCount() const noexcept { return m_registrations.size(); }
/**
* @brief Converts a registration deny reason to a string representation.
* @param reason The registration deny reason.
* @return A string representation of the registration deny reason.
*/
static std::string denyReasonToString(RegistrationDenyReason reason);
private:
/**
* @brief Represents the state of a registration.
*/
struct RegistrationState {
/**
* @brief The IP address associated with the registration.
*/
uint32_t ipAddress = 0U;
/**
* @brief The options associated with the registration.
*/
uint8_t options = 0U;
/**
* @brief The expiration time of the registration in milliseconds.
*/
uint64_t expiresAtMs = 0U;
/**
* @brief Indicates whether the registration is dynamic.
*/
bool dynamic = false;
};
DataBindingRegistry& m_bindings;
DataLinkManager& m_links;
ConventionalRegistrationPolicy m_policy;
std::unordered_map<uint32_t, RegistrationState> m_registrations;
/**
* @brief Denies a registration request.
* @param request The registration request to deny.
* @param reason The reason for denying the registration.
* @return The result of the denial operation.
*/
ConventionalRegistrationResult deny(const ConventionalRegistration& request,
RegistrationDenyReason reason) const;
};
} // namespace data
} // namespace p25
#endif // __P25_DATA__CONVENTIONALDATASERVICE_H__

@ -1,320 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file DataRouting.h
* @ingroup p25_pdu
* @file DataRouting.cpp
* @ingroup p25_pdu
*/
#if !defined(__P25_DATA__DATAROUTING_H__)
#define __P25_DATA__DATAROUTING_H__
#include "common/Defines.h"
#include "common/p25/data/PacketDataState.h"
#include <cstdint>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace p25
{
namespace data
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
* @addtogroup p25_pdu
* @{
*/
/**
* @brief Access mode used to reach a P25 packet data subscriber.
*/
enum class AccessMode : uint8_t {
CONVENTIONAL, //!< Conventional access mode.
TRUNKED //!< Trunked access mode.
};
/**
* @brief Policy applied when no current subscriber location is known.
*/
enum class UnknownLocationPolicy : uint8_t {
QUEUE, //!< Queue the packet until location is known.
ARP, //!< Attempt ARP resolution.
DROP //!< Drop the packet if location is unknown.
};
/* @} */
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Last observed conventional location for a subscriber.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API ConventionalLocation {
/**
* @brief Peer ID of the subscriber.
*/
uint32_t peerId = 0U;
/**
* @brief Channel ID of the subscriber.
*/
uint8_t channelId = 0U;
/**
* @brief Channel number of the subscriber.
*/
uint16_t channelNo = 0U;
/**
* @brief Timestamp of the last observation.
*/
uint64_t lastSeen = 0U;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Resolved RF route for a P25 packet data subscriber.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API DataRoute {
/**
* @brief Access mode used to reach the subscriber.
*/
AccessMode mode = AccessMode::CONVENTIONAL;
/**
* @brief Peer ID of the subscriber.
*/
uint32_t peerId = 0U;
/**
* @brief Channel ID of the subscriber.
*/
uint8_t channelId = 0U;
/**
* @brief Channel number of the subscriber.
*/
uint16_t channelNo = 0U;
/**
* @brief Grant ID associated with the subscriber.
*/
uint32_t grantId = 0U;
/**
* @brief Timestamp of the last observation.
*/
uint64_t lastSeen = 0U;
/**
* @brief Indicates if the route is valid.
*/
bool valid = false;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Observed ARP neighbor information without authorization semantics.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API RouteNeighbor {
/**
* @brief Link-layer ID of the neighbor.
*/
uint32_t llId = 0U;
/**
* @brief IP address of the neighbor.
*/
uint32_t ipAddress = 0U;
/**
* @brief Timestamp of the last observation.
*/
uint64_t lastSeen = 0U;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Stores ARP observations separately from authorized bindings.
* @ingroup p25_pdu
*/
class DVM_COMMON_API RouteNeighborCache {
public:
/**
* @brief Observes a route neighbor with the given link-layer ID and IP address.
* @param llId Link-layer ID of the neighbor.
* @param ipAddress IP address of the neighbor.
* @param nowMs Timestamp of the observation in milliseconds.
*/
void observe(uint32_t llId, uint32_t ipAddress, uint64_t nowMs = 0U);
/**
* @brief Erases a route neighbor with the given link-layer ID.
* @param llId Link-layer ID of the neighbor to erase.
* @return True if the neighbor was found and erased, false otherwise.
*/
bool erase(uint32_t llId);
/**
* @brief Clears all observed route neighbors.
*/
void clear();
/**
* @brief Finds a route neighbor by its link-layer ID.
* @param llId Link-layer ID of the neighbor to find.
* @return Pointer to the route neighbor if found, nullptr otherwise.
*/
const RouteNeighbor* findByLLId(uint32_t llId) const;
/**
* @brief Finds a route neighbor by its IP address.
* @param ipAddress IP address of the neighbor to find.
* @return Pointer to the route neighbor if found, nullptr otherwise.
*/
const RouteNeighbor* findByIPAddress(uint32_t ipAddress) const;
/**
* @brief Returns the number of observed route neighbors.
* @return Number of observed route neighbors.
*/
size_t size() const noexcept { return m_neighbors.size(); }
private:
std::unordered_map<uint32_t, RouteNeighbor> m_neighbors;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Resolves conventional locations and future trunked data routes.
* @ingroup p25_pdu
*/
class DVM_COMMON_API DataLocationRegistry {
public:
/**
* @brief Initializes a new instance of the LocationRegistry class.
* @param staleAfterMs Duration in milliseconds after which locations are considered stale.
* @param learnFromInbound Whether to learn locations from inbound traffic.
* @param unknownLocationPolicy Policy for handling unknown locations.
*/
explicit DataLocationRegistry(uint64_t staleAfterMs = 0U, bool learnFromInbound = true,
UnknownLocationPolicy unknownLocationPolicy = UnknownLocationPolicy::ARP);
/**
* @brief Updates the conventional location for a given link-layer ID.
* @param llId Link-layer ID of the neighbor.
* @param location Conventional location to update.
* @return True if the location was updated, false otherwise.
*/
bool updateConventional(uint32_t llId, const ConventionalLocation& location);
/**
* @brief Updates the trunked data route for a given link-layer ID.
* @param llId Link-layer ID of the neighbor.
* @param route Trunked data route to update.
* @return True if the route was updated, false otherwise.
*/
bool updateTrunked(uint32_t llId, const DataRoute& route);
/**
* @brief Erases the location or route for a given link-layer ID and access mode.
* @param llId Link-layer ID of the neighbor.
* @param mode Access mode (conventional or trunked).
* @return True if the entry was erased, false otherwise.
*/
bool erase(uint32_t llId, AccessMode mode);
/**
* @brief Clears all subscriber and group route state.
*/
void clear();
/**
* @brief Expires stale conventional and trunked route state based on the current time.
* @param nowMs Current time in milliseconds.
*/
void expire(uint64_t nowMs);
/**
* @brief Resolves the data route for a given link-layer ID and access mode.
* @param llId Link-layer ID of the neighbor.
* @param mode Access mode (conventional or trunked).
* @param nowMs Current time in milliseconds.
* @return Resolved data route.
*/
DataRoute resolve(uint32_t llId, AccessMode mode, uint64_t nowMs = 0U) const;
/**
* @brief Sets the group peers for group-based routing.
* @param peerIds Vector of peer link-layer IDs.
*/
void setGroupPeers(const std::vector<uint32_t>& peerIds);
/**
* @brief Resolves the data routes for a group of available peers.
* @param availablePeerIds Vector of available peer link-layer IDs.
* @return Vector of resolved data routes for the available peers.
*/
std::vector<DataRoute> resolveGroup(const std::vector<uint32_t>& availablePeerIds) const;
/**
* @brief Sets whether to learn from inbound data routes.
* @param enabled True to enable learning from inbound routes, false to disable.
*/
void setLearnFromInbound(bool enabled) noexcept { m_learnFromInbound = enabled; }
/**
* @brief Checks whether learning from inbound data routes is enabled.
* @return True if learning from inbound routes is enabled, false otherwise.
*/
bool learnFromInbound() const noexcept { return m_learnFromInbound; }
/**
* @brief Sets the policy for handling unknown locations.
* @param policy The unknown location policy to set.
*/
void setUnknownLocationPolicy(UnknownLocationPolicy policy) noexcept { m_unknownLocationPolicy = policy; }
/**
* @brief Retrieves the current policy for handling unknown locations.
* @return The current unknown location policy.
*/
UnknownLocationPolicy unknownLocationPolicy() const noexcept { return m_unknownLocationPolicy; }
/**
* @brief Retrieves the count of conventional locations.
* @return The number of conventional locations.
*/
size_t conventionalCount() const noexcept { return m_conventional.size(); }
/**
* @brief Retrieves the count of trunked locations.
* @return The number of trunked locations.
*/
size_t trunkedCount() const noexcept { return m_trunked.size(); }
private:
uint64_t m_staleAfterMs;
bool m_learnFromInbound;
UnknownLocationPolicy m_unknownLocationPolicy;
std::unordered_map<uint32_t, ConventionalLocation> m_conventional;
std::unordered_map<uint32_t, DataRoute> m_trunked;
std::unordered_set<uint32_t> m_groupPeers;
/**
* @brief Checks if a location is considered stale based on the last seen timestamp and the current time.
* @param lastSeen The timestamp when the location was last seen.
* @param nowMs The current time in milliseconds.
* @return True if the location is stale, false otherwise.
*/
bool stale(uint64_t lastSeen, uint64_t nowMs) const;
};
} // namespace data
} // namespace p25
#endif // __P25_DATA__DATAROUTING_H__

@ -1,248 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file IPConvergenceService.h
* @ingroup p25_pdu
* @file IPConvergenceService.cpp
* @ingroup p25_pdu
*/
#if !defined(__P25_DATA__IPCONVERGENCESERVICE_H__)
#define __P25_DATA__IPCONVERGENCESERVICE_H__
#include "common/Defines.h"
#include "common/p25/data/IPv4Packet.h"
#include "common/p25/data/PacketDataState.h"
#include <cstdint>
namespace p25
{
namespace data
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
* @addtogroup p25_pdu
* @{
*/
/**
* @brief Result of an IP convergence operation.
*/
enum class ConvergenceResult : uint8_t {
OK, //!< Operation was successful.
MALFORMED, //!< The IP datagram is malformed.
NO_BINDING, //!< No binding exists for the IP datagram.
SOURCE_MISMATCH, //!< The source address does not match the expected source.
MTU_EXCEEDED, //!< The IP datagram exceeds the maximum transmission unit.
POLICY_REJECTED //!< The IP datagram was rejected by policy.
};
/**
* @brief LLC delivery mode selected for an IP datagram.
*/
enum class DataDeliveryMode : uint8_t {
CONFIRMED, //!< The IP datagram requires confirmed delivery.
UNCONFIRMED //!< The IP datagram does not require confirmed delivery.
};
/* @} */
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Result returned when decoding an IP datagram.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API IPDecodeResult {
/**
* @brief The result of the IP convergence operation.
*/
ConvergenceResult result = ConvergenceResult::MALFORMED;
/**
* @brief The decoded IP packet.
*/
IPv4Packet packet;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Result returned when routing an IP datagram for encoding.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API IPEncodeResult {
/**
* @brief The result of the IP convergence operation.
*/
ConvergenceResult result = ConvergenceResult::MALFORMED;
/**
* @brief The logical link identifier for the encoded IP datagram.
*/
uint32_t llId = 0U;
/**
* @brief The length of the encoded IP datagram.
*/
uint16_t length = 0U;
/**
* @brief The delivery mode selected for the encoded IP datagram.
*/
DataDeliveryMode delivery = DataDeliveryMode::CONFIRMED;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Policy controlling SCEP routing and delivery.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API SCEPPolicy {
/**
* @brief The maximum transmission unit for the SCEP policy.
*/
uint16_t mtu = 510U;
/**
* @brief Indicates whether broadcast transmission is allowed under the SCEP policy.
*/
bool allowBroadcast = true;
/**
* @brief Indicates whether multicast transmission is allowed under the SCEP policy.
*/
bool allowMulticast = false;
/**
* @brief The delivery mode selected for unicast transmissions under the SCEP policy.
*/
DataDeliveryMode unicastDelivery = DataDeliveryMode::CONFIRMED;
/**
* @brief The delivery mode selected for broadcast transmissions under the SCEP policy.
*/
DataDeliveryMode broadcastDelivery = DataDeliveryMode::UNCONFIRMED;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Defines an IP convergence service for a P25 packet data bearer.
* @ingroup p25_pdu
*/
class DVM_COMMON_API IPConvergenceService {
public:
/**
* @brief Finalizes a instance of IPConvergenceService.
*/
virtual ~IPConvergenceService() = default;
/**
* @brief Decodes an IP datagram from the provided data buffer.
* @param data Pointer to the buffer containing the IP datagram.
* @param length Length of the data buffer.
* @return The result of the IP datagram decoding operation.
*/
virtual IPDecodeResult decode(const uint8_t* data, uint32_t length) const = 0;
/**
* @brief Encodes an IP datagram into the provided data buffer.
* @param data Pointer to the buffer where the IP datagram will be encoded.
* @param length Length of the data buffer.
* @return The result of the IP datagram encoding operation.
*/
virtual IPEncodeResult encode(const uint8_t* data, uint32_t length) const = 0;
/**
* @brief Authorizes an uplink transmission for the specified logical link and IP packet.
* @param llId The logical link identifier.
* @param packet The IP packet to be transmitted.
* @return The result of the uplink authorization operation.
*/
virtual ConvergenceResult authorizeUplink(uint32_t llId,
const IPv4Packet& packet) const = 0;
/**
* @brief Routes a downlink transmission to the specified destination address.
* @param destinationAddress The destination IP address for the downlink transmission.
* @return The result of the downlink routing operation.
*/
virtual uint32_t routeDownlink(uint32_t destinationAddress) const = 0;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements the conventional SCEP IP convergence service.
* @ingroup p25_pdu
*/
class DVM_COMMON_API SCEPService : public IPConvergenceService {
public:
/**
* @brief Initializes an instance of the SCEPService class.
* @param bindings The data binding registry to be used by the service.
* @param policy The SCEP policy to be applied by the service.
*/
explicit SCEPService(const DataBindingRegistry& bindings,
const SCEPPolicy& policy = SCEPPolicy());
/**
* @brief Decodes an IP datagram from the provided data buffer.
* @param data Pointer to the buffer containing the IP datagram.
* @param length Length of the data buffer.
* @return The result of the IP datagram decoding operation.
*/
IPDecodeResult decode(const uint8_t* data, uint32_t length) const override;
/**
* @brief Encodes an IP datagram into the provided data buffer.
* @param data Pointer to the buffer containing the IP datagram.
* @param length Length of the data buffer.
* @return The result of the IP datagram encoding operation.
*/
IPEncodeResult encode(const uint8_t* data, uint32_t length) const override;
/**
* @brief Authorizes an uplink transmission for the specified logical link ID and IP packet.
* @param llId The logical link ID for the uplink transmission.
* @param packet The IP packet to be transmitted.
* @return The result of the uplink authorization operation.
*/
ConvergenceResult authorizeUplink(uint32_t llId, const IPv4Packet& packet) const override;
/**
* @brief Routes a downlink transmission to the specified destination address.
* @param destinationAddress The destination address for the downlink transmission.
* @return The logical link ID to which the downlink transmission should be routed.
*/
uint32_t routeDownlink(uint32_t destinationAddress) const override;
/**
* @brief Determines if the specified address is a broadcast address.
* @param address The address to be checked.
* @return True if the address is a broadcast address, false otherwise.
*/
static bool isBroadcast(uint32_t address);
/**
* @brief Determines if the specified address is a multicast address.
* @param address The address to be checked.
* @return True if the address is a multicast address, false otherwise.
*/
static bool isMulticast(uint32_t address);
private:
const DataBindingRegistry& m_bindings;
SCEPPolicy m_policy;
};
} // namespace data
} // namespace p25
#endif // __P25_DATA__IPCONVERGENCESERVICE_H__

@ -1,298 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file PacketDataState.h
* @ingroup p25_pdu
* @file PacketDataState.cpp
* @ingroup p25_pdu
*/
#if !defined(__P25_DATA__PACKETDATASTATE_H__)
#define __P25_DATA__PACKETDATASTATE_H__
#include "common/Defines.h"
#include <cstdint>
#include <unordered_map>
namespace p25
{
namespace data
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
* @addtogroup p25_pdu
* @{
*/
/**
* @brief P25 packet data access mode.
*/
enum class DataAccessMode : uint8_t {
CONVENTIONAL, //!< Conventional access mode
TRUNKED //!< Trunked access mode
};
/**
* @brief P25 packet data convergence mode.
*/
enum class DataConvergenceMode : uint8_t {
CAI_DATA, //!< CAI data convergence mode
SCEP, //!< SCEP data convergence mode
SNDCP //!< SNDCP data convergence mode
};
/**
* @brief Source from which an IP binding was learned.
*/
enum class DataBindingOrigin : uint8_t {
PROVISIONED, //!< Provisioned binding origin
CONVENTIONAL_REGISTRATION, //!< Conventional registration binding origin
SNDCP_STATIC, //!< SNDCP static binding origin
SNDCP_DYNAMIC, //!< SNDCP dynamic binding origin
ARP_LEARNED //!< ARP learned binding origin
};
/**
* @brief Result of LLC receive sequence processing.
*/
enum class ReceiveSequenceResult : uint8_t {
ACCEPTED, //!< Accepted receive sequence result
DUPLICATE, //!< Duplicate receive sequence result
OUT_OF_SEQUENCE //!< Out of sequence receive sequence result
};
/* @} */
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Identifies a P25 logical data link.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API DataLinkKey {
/**
* @brief The logical link identifier.
*/
uint32_t llId = 0U;
/**
* @brief Compares this DataLinkKey with another for equality.
* @param other The other DataLinkKey to compare with.
* @return True if the DataLinkKeys are equal; otherwise, false.
*/
bool operator==(const DataLinkKey& other) const noexcept
{
return llId == other.llId;
}
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Associates a logical data link with an authorized IP address.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API IPBinding {
/**
* @brief The logical data link associated with this IP binding.
*/
DataLinkKey link;
/**
* @brief The IP address associated with this binding.
*/
uint32_t ipAddress = 0U;
/**
* @brief The access mode for this binding.
*/
DataAccessMode accessMode = DataAccessMode::CONVENTIONAL;
/**
* @brief The convergence mode for this binding.
*/
DataConvergenceMode convergenceMode = DataConvergenceMode::SCEP;
/**
* @brief The origin from which this binding was learned.
*/
DataBindingOrigin origin = DataBindingOrigin::PROVISIONED;
/**
* @brief The NSAPI associated with this binding.
*/
uint8_t nsapi = 0U;
/**
* @brief Indicates whether the NSAPI is valid.
*/
bool hasNSAPI = false;
/**
* @brief Indicates whether this binding is authorized.
*/
bool authorized = false;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief LLC sequencing and readiness state for one logical link.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API DataLinkState {
/**
* @brief The send sequence number for this logical link.
*/
uint8_t sendSequence = 0U;
/**
* @brief The receive sequence number for this logical link.
*/
uint8_t receiveSequence = 0U;
/**
* @brief The receive fingerprint for this logical link.
*/
uint32_t receiveFingerprint = 0U;
/**
* @brief Indicates whether the send sequence has been initialized.
*/
bool sendInitialized = false;
/**
* @brief Indicates whether the receive sequence has been initialized.
*/
bool receiveInitialized = false;
/**
* @brief Indicates whether the link is ready for the next packet.
*/
bool readyForNextPacket = true;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Stores typed P25 packet data IP bindings.
* @ingroup p25_pdu
*/
class DVM_COMMON_API DataBindingRegistry {
public:
/**
* @brief Inserts or updates an IP binding in the registry. Returns true if the operation was successful.
* @param binding The IP binding to insert or update.
* @return True if the operation was successful, false otherwise.
*/
bool upsert(const IPBinding& binding);
/**
* @brief Erases an IP binding from the registry by its logical link ID. Returns true if the operation was successful.
* @param llId The logical link ID of the IP binding to erase.
* @return True if the operation was successful, false otherwise.
*/
bool erase(uint32_t llId);
/**
* @brief Clears all IP bindings from the registry.
*/
void clear();
/**
* @brief Finds an IP binding by its logical link ID.
* @param llId The logical link ID of the IP binding to find.
* @return A pointer to the IP binding if found, nullptr otherwise.
*/
const IPBinding* findByLLId(uint32_t llId) const;
/**
* @brief Finds an IP binding by its IP address.
* @param ipAddress The IP address of the IP binding to find.
* @return A pointer to the IP binding if found, nullptr otherwise.
*/
const IPBinding* findByIPAddress(uint32_t ipAddress) const;
/**
* @brief Returns the number of IP bindings in the registry.
* @return The number of IP bindings.
*/
size_t size() const noexcept { return m_bindings.size(); }
private:
std::unordered_map<uint32_t, IPBinding> m_bindings;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Stores LLC state independently for each logical link.
* @ingroup p25_pdu
*/
class DVM_COMMON_API DataLinkManager {
public:
/**
* @brief Returns the next send sequence number for the specified logical link.
* @param llId The logical link ID.
* @param synchronize Indicates whether to synchronize the sequence.
* @return The next send sequence number.
*/
uint8_t nextSendSequence(uint32_t llId, bool& synchronize);
/**
* @brief Accepts a received sequence number for the specified logical link.
* @param llId The logical link ID.
* @param sequence The received sequence number.
* @param synchronize Indicates whether to synchronize the sequence.
* @param packetFingerprint The fingerprint of the received packet.
* @param expectedSequence The expected sequence number.
* @return The result of the receive sequence acceptance.
*/
ReceiveSequenceResult acceptReceiveSequence(uint32_t llId, uint8_t sequence,
bool synchronize, uint32_t packetFingerprint, uint8_t& expectedSequence);
/**
* @brief Computes the fingerprint of the given data.
* @param data Pointer to the data.
* @param length Length of the data.
* @return The computed fingerprint.
*/
static uint32_t fingerprint(const uint8_t* data, uint32_t length);
/**
* @brief Sets the readiness state for the specified logical link.
* @param llId The logical link ID.
* @param ready The readiness state to set.
*/
void setReady(uint32_t llId, bool ready);
/**
* @brief Checks if the specified logical link is ready.
* @param llId The logical link ID.
* @return True if the logical link is ready, false otherwise.
*/
bool isReady(uint32_t llId) const;
/**
* @brief Checks if the specified logical link has an associated state.
* @param llId The logical link ID.
* @return True if the logical link has an associated state, false otherwise.
*/
bool hasState(uint32_t llId) const;
/**
* @brief Erases the state associated with the specified logical link.
* @param llId The logical link ID.
*/
void erase(uint32_t llId);
/**
* @brief Clears all logical link states.
*/
void clear();
private:
std::unordered_map<uint32_t, DataLinkState> m_states;
};
} // namespace data
} // namespace p25
#endif // __P25_DATA__PACKETDATASTATE_H__

@ -1,115 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file PacketScheduler.h
* @ingroup p25_pdu
* @file PacketScheduler.cpp
* @ingroup p25_pdu
*/
#if !defined(__P25_DATA__PACKETSCHEDULER_H__)
#define __P25_DATA__PACKETSCHEDULER_H__
#include "common/Defines.h"
#include "common/p25/data/DataHeader.h"
#include <cstdint>
#include <deque>
#include <vector>
namespace p25
{
namespace data
{
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief One queued packet data downlink.
* @ingroup p25_pdu
*/
struct DVM_COMMON_API ScheduledDataPacket {
DataHeader header; //!< Header information for the P25 packet.
std::vector<uint8_t> userData; //!< User data payload for the P25 packet.
uint32_t llId = 0U; //!< Link-layer ID associated with the scheduled packet.
uint32_t targetIPAddress = 0U; //!< Target IP address for the scheduled packet.
uint64_t dueAt = 0U; //!< Timestamp indicating when the packet is due.
uint8_t retryCount = 0U; //!< Number of retry attempts for the packet.
bool extendedRetry = false; //!< Indicates if extended retry is enabled.
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Owns bounded queued packet data downlinks.
* @ingroup p25_pdu
*/
class DVM_COMMON_API PacketScheduler {
public:
/**
* @brief Initializes a new instance of the PacketScheduler class.
* @param maxFrames The maximum number of frames the scheduler can hold.
* @param maxBytes The maximum number of bytes the scheduler can hold.
*/
PacketScheduler(uint32_t maxFrames = 0U, uint32_t maxBytes = 0U);
/**
* @brief Enqueues a scheduled packet into the scheduler.
* @param packet The scheduled packet to enqueue.
* @return The number of oldest frames dropped to make room.
*/
uint32_t enqueue(ScheduledDataPacket&& packet);
/**
* @brief Checks if the scheduler is empty.
* @return True if the scheduler is empty, false otherwise.
*/
bool empty() const noexcept { return m_packets.empty(); }
/**
* @brief Gets the number of packets currently in the scheduler.
* @return The number of packets in the scheduler.
*/
size_t size() const noexcept { return m_packets.size(); }
/**
* @brief Gets the total byte count of packets currently in the scheduler.
* @return The total byte count of packets in the scheduler.
*/
uint32_t byteCount() const noexcept { return m_byteCount; }
/**
* @brief Gets the next queued packet.
* @return A pointer to the next scheduled packet, or nullptr if the scheduler is empty.
*/
ScheduledDataPacket* front();
/**
* @brief Removes the next queued packet.
*/
void pop();
/**
* @brief Moves the next queued packet to the back of the queue.
*/
void rotate();
/**
* @brief Clears all queued packets from the scheduler.
*/
void clear();
private:
uint32_t m_maxFrames;
uint32_t m_maxBytes;
uint32_t m_byteCount;
std::deque<ScheduledDataPacket> m_packets;
};
} // namespace data
} // namespace p25
#endif // __P25_DATA__PACKETSCHEDULER_H__

@ -14,6 +14,8 @@ file(GLOB dvmfne_SRC
"src/fne/network/callhandler/*.cpp"
"src/fne/network/callhandler/packetdata/*.h"
"src/fne/network/callhandler/packetdata/*.cpp"
"src/fne/network/callhandler/packetdata/p25/*.h"
"src/fne/network/callhandler/packetdata/p25/*.cpp"
"src/fne/network/influxdb/*.h"
"src/fne/network/influxdb/*.cpp"
"src/fne/network/packet/*.h"

@ -16,6 +16,10 @@
* @brief Implementation for the FNE call handlers.
* @ingroup fne_network
*
* @defgroup fne_packetdata Packet Data Handlers
* @brief Implementation for the FNE packet data handlers.
* @ingroup fne_callhandler
*
* @file TrafficNetwork.h
* @ingroup fne_network
* @file TrafficNetwork.cpp

@ -30,6 +30,7 @@ using namespace dmr::defines;
#include <cassert>
#include <chrono>
#include <utility>
#if !defined(_WIN32)
#include <netinet/ip.h>
@ -44,6 +45,7 @@ const uint8_t MAX_PKT_RETRY_CNT = 2U;
const uint32_t INTERPACKET_DELAY = 100U; // milliseconds
const uint32_t ARP_RETRY_MS = 5000U; // milliseconds
const uint32_t CONVENTIONAL_LOCATION_MAX_AGE_MS = 300000U; // 5 minutes
// ---------------------------------------------------------------------------
// Public Class Members
@ -54,10 +56,10 @@ const uint32_t ARP_RETRY_MS = 5000U; // milliseconds
DMRPacketData::DMRPacketData(TrafficNetwork* network, TagDMRData* tag, bool debug) :
m_network(network),
m_tag(tag),
m_queuedFrames(),
m_queuedFrameBytes(0U),
m_packetScheduler(network->m_vtunQueueMaxFrames, network->m_vtunQueueMaxBytes),
m_status(),
m_arpTable(),
m_neighborCache(),
m_locationRegistry(CONVENTIONAL_LOCATION_MAX_AGE_MS),
m_readyForNextPkt(),
m_suSendSeq(),
m_debug(debug)
@ -70,17 +72,7 @@ DMRPacketData::DMRPacketData(TrafficNetwork* network, TagDMRData* tag, bool debu
DMRPacketData::~DMRPacketData()
{
while (m_queuedFrames.size() > 0U) {
QueuedDataFrame* frame = m_queuedFrames[0U];
m_queuedFrames.pop_front();
if (frame != nullptr) {
if (frame->header != nullptr)
delete frame->header;
if (frame->userData != nullptr)
delete[] frame->userData;
delete frame;
}
}
/* stub */
}
/* Process a data frame from the network. */
@ -248,16 +240,25 @@ bool DMRPacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee
}
}
uint32_t srcId = status->header.getSrcId();
if (m_locationRegistry.learnFromInbound() && srcId != 0U && srcId != DMRDEF::WUID_ALL) {
ConventionalLocation location;
location.peerId = peerId;
location.slotNo = status->slotNo;
location.lastSeen = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
m_locationRegistry.updateConventional(srcId, location);
}
status->callBusy = true;
dispatch(peerId, dmrData, data, len);
uint64_t duration = hrc::diff(pktTime, status->callStartTime);
bool gi = status->header.getGI();
uint32_t srcId = status->header.getSrcId();
uint32_t dstId = status->header.getDstId();
LogInfoEx((fromUpstream) ? LOG_PEER : LOG_MASTER, "DMR, Data Call End, peer = %u, slot = %u, srcId = %u, dstId = %u, group = %u, blocks = %u, duration = %u, streamId = %u, fromUpstream = %u",
peerId, srcId, dstId, gi, status->header.getBlocksToFollow(), duration / 1000, streamId, fromUpstream);
peerId, status->slotNo, srcId, dstId, gi, status->header.getBlocksToFollow(), duration / 1000, streamId, fromUpstream);
TrafficNetwork::MetricsLogging::incrementCallsProcessed(m_network);
@ -334,77 +335,35 @@ void DMRPacketData::processPacketFrame(const uint8_t* data, uint32_t len, bool a
srcIpStr.c_str(), DMRDEF::WUID_IPI, tgtIpStr.c_str(), dstId, pktLen, proto);
// assemble a DMR PDU frame header for transport...
dmr::data::DataHeader* pktHeader = new dmr::data::DataHeader();
pktHeader->setDPF(DPF::CONFIRMED_DATA);
pktHeader->setA(true);
pktHeader->setSAP(PDUSAP::PACKET_DATA);
pktHeader->setSrcId(DMRDEF::WUID_IPI);
pktHeader->setDstId(dstId);
pktHeader->setGI(false);
pktHeader->setFullMesage(true);
dmr::data::DataHeader pktHeader;
pktHeader.setDPF(DPF::CONFIRMED_DATA);
pktHeader.setA(true);
pktHeader.setSAP(PDUSAP::PACKET_DATA);
pktHeader.setSrcId(DMRDEF::WUID_IPI);
pktHeader.setDstId(dstId);
pktHeader.setGI(false);
pktHeader.setFullMesage(true);
// bryanb: we are always sending data as 3/4 rate?
pktHeader->calculateLength(DataType::RATE_34_DATA, pktLen);
pktHeader.calculateLength(DataType::RATE_34_DATA, pktLen);
uint32_t pduLength = pktLen;
DECLARE_UINT8_ARRAY(pduUserData, pduLength);
::memcpy(pduUserData, data, pktLen);
if (pduLength > m_network->m_vtunQueueMaxBytes) {
LogWarning(LOG_DMR, "VTUN queue drop, frame too large for queue cap, frameBytes = %u, capBytes = %u",
pduLength, m_network->m_vtunQueueMaxBytes);
return;
}
uint32_t droppedFrames = 0U;
while (m_queuedFrames.size() >= m_network->m_vtunQueueMaxFrames ||
(m_queuedFrameBytes + pduLength) > m_network->m_vtunQueueMaxBytes) {
if (m_queuedFrames.size() == 0U) {
break;
}
QueuedDataFrame* oldFrame = m_queuedFrames[0U];
m_queuedFrames.pop_front();
if (oldFrame != nullptr) {
if (oldFrame->userDataLen <= m_queuedFrameBytes) {
m_queuedFrameBytes -= oldFrame->userDataLen;
}
else {
m_queuedFrameBytes = 0U;
}
if (oldFrame->header != nullptr)
delete oldFrame->header;
if (oldFrame->userData != nullptr)
delete[] oldFrame->userData;
delete oldFrame;
}
droppedFrames++;
}
ScheduledDMRDataPacket packet;
packet.header = pktHeader;
packet.dstId = dstId;
packet.targetIPAddress = tgtProtoAddr;
packet.dueAt = now + INTERPACKET_DELAY;
packet.userData.assign(pduUserData, pduUserData + pduLength);
uint32_t droppedFrames = m_packetScheduler.enqueue(std::move(packet));
if (droppedFrames > 0U) {
LogWarning(LOG_DMR, "VTUN queue cap reached, dropped %u oldest frame(s), queuedFrames = %u, queuedBytes = %u",
droppedFrames, (uint32_t)m_queuedFrames.size(), m_queuedFrameBytes);
LogWarning(LOG_DMR, "VTUN queue cap reached, dropped %u frame(s), queuedFrames = %u, queuedBytes = %u",
droppedFrames, uint32_t(m_packetScheduler.size()), m_packetScheduler.byteCount());
}
// queue frame for dispatch
QueuedDataFrame* qf = new QueuedDataFrame();
qf->retryCnt = 0U;
qf->extendRetry = false;
qf->timestamp = now + INTERPACKET_DELAY;
qf->header = pktHeader;
qf->dstId = dstId;
qf->tgtProtoAddr = tgtProtoAddr;
qf->userData = new uint8_t[pduLength];
::memcpy(qf->userData, pduUserData, pduLength);
qf->userDataLen = pduLength;
m_queuedFrames.push_back(qf);
m_queuedFrameBytes += pduLength;
#endif // !defined(_WIN32)
}
@ -434,53 +393,51 @@ void DMRPacketData::write_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, ui
void DMRPacketData::clock(uint32_t ms)
{
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_locationRegistry.expire(now);
if (m_queuedFrames.size() == 0U) {
if (m_packetScheduler.empty()) {
return;
}
// transmit queued data frames
auto& frame = m_queuedFrames[0];
if (frame != nullptr) {
if (now >= frame->timestamp) {
// check if we have an ARP entry for the destination
if (!hasARPEntry(frame->dstId)) {
if (frame->retryCnt < MAX_PKT_RETRY_CNT || frame->extendRetry) {
// send ARP request
write_PDU_ARP(frame->tgtProtoAddr);
frame->timestamp = now + ARP_RETRY_MS;
frame->retryCnt++;
} else {
LogWarning(LOG_DMR, "DMR, failed to resolve ARP for dstId %u", frame->dstId);
if (frame->userDataLen <= m_queuedFrameBytes) {
m_queuedFrameBytes -= frame->userDataLen;
}
else {
m_queuedFrameBytes = 0U;
}
delete frame->header;
delete[] frame->userData;
delete frame;
m_queuedFrames.pop_front();
}
} else {
// transmit the PDU frame
dispatchUserFrameToFNE(*frame->header, frame->userData);
ScheduledDMRDataPacket* packet = m_packetScheduler.front();
if (packet == nullptr || now < packet->dueAt)
return;
if (frame->userDataLen <= m_queuedFrameBytes) {
m_queuedFrameBytes -= frame->userDataLen;
}
else {
m_queuedFrameBytes = 0U;
}
if (packet->retryCount >= MAX_PKT_RETRY_CNT) {
LogWarning(LOG_DMR, "DMR, max packet retry count exceeded, dropping packet, dstId = %u",
packet->dstId);
m_packetScheduler.pop();
return;
}
delete frame->header;
delete[] frame->userData;
delete frame;
m_queuedFrames.pop_front();
// check if we have an ARP entry for the destination
if (!hasARPEntry(packet->dstId)) {
write_PDU_ARP(packet->targetIPAddress);
packet->dueAt = now + ARP_RETRY_MS;
packet->retryCount++;
return;
}
if (packet->dstId != DMRDEF::WUID_ALL) {
DataRoute route = m_locationRegistry.resolve(packet->dstId,
AccessMode::CONVENTIONAL, now);
if (!route.valid) {
UnknownLocationPolicy policy = m_locationRegistry.unknownLocationPolicy();
if (policy == UnknownLocationPolicy::DROP) {
m_packetScheduler.pop();
return;
}
if (policy == UnknownLocationPolicy::ARP)
write_PDU_ARP(packet->targetIPAddress);
packet->dueAt = now + ARP_RETRY_MS;
packet->retryCount++;
return;
}
}
dispatchUserFrameToFNE(packet->header, packet->userData.data());
m_packetScheduler.pop();
}
/* Helper to cleanup any call's left in a dangling state without any further updates. */
@ -576,13 +533,13 @@ void DMRPacketData::dispatch(uint32_t peerId, dmr::data::NetData& dmrData, const
if (opcode == DMR_PDU_ARP_REQUEST) {
LogInfoEx(LOG_DMR, "DMR, ARP request, who has %s? tell %s (%u)", __IP_FROM_UINT(tgtProtoAddr).c_str(), __IP_FROM_UINT(srcProtoAddr).c_str(), srcHWAddr);
m_arpTable[srcHWAddr] = srcProtoAddr; // update ARP table
m_neighborCache.observe(srcHWAddr, srcProtoAddr); // update neighbor cache
if (tgtProtoAddr == fneIPv4) {
write_PDU_ARP_Reply(fneIPv4, srcHWAddr, srcProtoAddr, DMRDEF::WUID_ALLL);
}
} else if (opcode == DMR_PDU_ARP_REPLY) {
LogInfoEx(LOG_DMR, "DMR, ARP reply, %s is at %u", __IP_FROM_UINT(srcProtoAddr).c_str(), srcHWAddr);
m_arpTable[srcHWAddr] = srcProtoAddr; // update ARP table
m_neighborCache.observe(srcHWAddr, srcProtoAddr); // update neighbor cache
}
#endif // !defined(_WIN32)
}
@ -679,6 +636,34 @@ void DMRPacketData::dispatchToFNE(uint32_t peerId, dmr::data::NetData& dmrData,
uint32_t srcId = status->header.getSrcId();
uint32_t dstId = status->header.getDstId();
std::vector<DataRoute> routes;
bool groupDelivery = status->header.getGI() || dstId == DMRDEF::WUID_ALL;
if (groupDelivery) {
std::vector<uint32_t> availablePeers;
for (const auto& peer : m_network->m_peers)
availablePeers.push_back(peer.first);
for (const auto& peer : m_network->m_host->m_peerNetworks) {
if (peer.second->isEnabled())
availablePeers.push_back(peer.second->getPeerId());
}
routes = m_locationRegistry.resolveGroup(availablePeers);
}
else {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
DataRoute route = m_locationRegistry.resolve(dstId, AccessMode::CONVENTIONAL, now);
if (!route.valid) {
LogWarning(LOG_DMR, "DMR, no RF route for relayed packet, dstId = %u", dstId);
return;
}
routes.push_back(route);
}
auto selectedPeer = [&routes](uint32_t candidatePeerId) {
return std::any_of(routes.cbegin(), routes.cend(),
[candidatePeerId](const DataRoute& route) { return route.peerId == candidatePeerId; });
};
/*
** MASTER TRAFFIC
*/
@ -686,7 +671,7 @@ void DMRPacketData::dispatchToFNE(uint32_t peerId, dmr::data::NetData& dmrData,
// repeat traffic to the connected peers
if (m_network->m_peers.size() > 0U) {
for (auto peer : m_network->m_peers) {
if (peerId != peer.first) {
if (peerId != peer.first && selectedPeer(peer.first)) {
// is this peer ignored?
if (!m_tag->isPeerPermitted(peer.first, dmrData, streamId)) {
continue;
@ -712,7 +697,7 @@ void DMRPacketData::dispatchToFNE(uint32_t peerId, dmr::data::NetData& dmrData,
// don't try to repeat traffic to the source peer...if this traffic
// is coming from a neighbor FNE peer
if (dstPeerId != peerId) {
if (dstPeerId != peerId && selectedPeer(dstPeerId)) {
// skip peer if it isn't enabled
if (!peer.second->isEnabled()) {
continue;
@ -740,6 +725,26 @@ void DMRPacketData::dispatchUserFrameToFNE(dmr::data::DataHeader& dataHeader, ui
uint32_t srcId = dataHeader.getSrcId();
uint32_t dstId = dataHeader.getDstId();
std::vector<DataRoute> routes;
if (dataHeader.getGI() || dstId == DMRDEF::WUID_ALL) {
std::vector<uint32_t> availablePeers;
for (const auto& peer : m_network->m_peers)
availablePeers.push_back(peer.first);
routes = m_locationRegistry.resolveGroup(availablePeers);
}
else {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
DataRoute route = m_locationRegistry.resolve(dstId, AccessMode::CONVENTIONAL, now);
if (route.valid)
routes.push_back(route);
}
if (routes.empty()) {
LogWarning(LOG_DMR, "DMR, no RF route for downlink, dstId = %u", dstId);
return;
}
// update the sequence number
m_suSendSeq[srcId]++;
if (m_suSendSeq[srcId] >= 8U) {
@ -768,7 +773,6 @@ void DMRPacketData::dispatchUserFrameToFNE(dmr::data::DataHeader& dataHeader, ui
*/
dmr::data::NetData dmrData;
dmrData.setSlotNo(1U);
dmrData.setSrcId(srcId);
dmrData.setDstId(dstId);
dmrData.setFLCO(FLCO::PRIVATE);
@ -776,18 +780,29 @@ void DMRPacketData::dispatchUserFrameToFNE(dmr::data::DataHeader& dataHeader, ui
dmrData.setSeqNo(0U);
dmrData.setDataType((currentBlock == 0U) ? DataType::DATA_HEADER : DataType::RATE_34_DATA);
// create DMR network message
uint32_t messageLength = 0U;
UInt8Array message = m_network->createDMR_Message(messageLength, streamId, dmrData);
if (message != nullptr) {
// copy the DMR frame data
for (const DataRoute& route : routes) {
dmrData.setSlotNo(route.slotNo != 0U ? route.slotNo : 1U);
uint32_t messageLength = 0U;
UInt8Array message = m_network->createDMR_Message(messageLength, streamId, dmrData);
if (message == nullptr)
continue;
::memcpy(message.get() + 20U, data, len);
// repeat traffic to the connected peers
if (m_network->m_peers.size() > 0U) {
for (auto peer : m_network->m_peers) {
m_network->writePeer(peer.first, m_network->m_peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR },
message.get(), messageLength, pktSeq, streamId);
auto localPeer = m_network->m_peers.find(route.peerId);
if (localPeer != m_network->m_peers.end()) {
m_network->writePeer(route.peerId, m_network->m_peerId,
{ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR },
message.get(), messageLength, pktSeq, streamId);
}
else {
for (const auto& peer : m_network->m_host->m_peerNetworks) {
if (peer.second->getPeerId() == route.peerId && peer.second->isEnabled()) {
peer.second->writeMaster(
{ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR },
message.get(), messageLength, pktSeq, streamId);
break;
}
}
}
}
@ -891,18 +906,8 @@ bool DMRPacketData::hasARPEntry(uint32_t id) const
return false;
}
// lookup ARP table entry
try {
uint32_t addr = m_arpTable.at(id);
if (addr != 0U) {
return true;
}
else {
return false;
}
} catch (...) {
return false;
}
const RouteNeighbor* neighbor = m_neighborCache.findBySubscriberId(id);
return neighbor != nullptr && neighbor->ipAddress != 0U;
}
/* Helper to get the IP address for the given radio ID. */
@ -914,7 +919,8 @@ uint32_t DMRPacketData::getIPAddress(uint32_t id)
}
if (hasARPEntry(id)) {
return m_arpTable[id];
const RouteNeighbor* neighbor = m_neighborCache.findBySubscriberId(id);
return neighbor != nullptr ? neighbor->ipAddress : 0U;
} else {
// do we have a static entry for this ID?
lookups::RadioId rid = m_network->m_ridLookup->find(id);
@ -938,11 +944,9 @@ uint32_t DMRPacketData::getRadioIdAddress(uint32_t addr)
return 0U;
}
for (auto entry : m_arpTable) {
if (entry.second == addr) {
return entry.first;
}
}
const RouteNeighbor* neighbor = m_neighborCache.findByIPAddress(addr);
if (neighbor != nullptr)
return neighbor->subscriberId;
// check if we have an entry in the RID lookup
std::string ipAddr = __IP_FROM_UINT(addr);
@ -955,7 +959,7 @@ uint32_t DMRPacketData::getRadioIdAddress(uint32_t addr)
return false;
});
if (it != ridTable.end()) {
m_arpTable[it->first] = addr;
m_neighborCache.observe(it->first, addr);
return it->first;
}

@ -9,24 +9,23 @@
*/
/**
* @file DMRPacketData.h
* @ingroup fne_callhandler
* @ingroup fne_packetdata
* @file DMRPacketData.cpp
* @ingroup fne_callhandler
* @ingroup fne_packetdata
*/
#if !defined(__PACKETDATA__DMR_PACKET_DATA_H__)
#define __PACKETDATA__DMR_PACKET_DATA_H__
#include "fne/Defines.h"
#include "common/Clock.h"
#include "common/concurrent/deque.h"
#include "common/concurrent/unordered_map.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/data/DataHeader.h"
#include "network/TrafficNetwork.h"
#include "network/PeerNetwork.h"
#include "network/callhandler/TagDMRData.h"
#include <deque>
#include "network/callhandler/packetdata/DataRouting.h"
#include "network/callhandler/packetdata/DMRPacketScheduler.h"
namespace network
{
@ -40,7 +39,7 @@ namespace network
/**
* @brief Implements the DMR packet data handler.
* @ingroup fne_callhandler
* @ingroup fne_packetdata
*/
class HOST_SW_API DMRPacketData {
public:
@ -101,24 +100,7 @@ namespace network
TrafficNetwork* m_network;
TagDMRData *m_tag;
/**
* @brief Represents a queued data frame from the VTUN.
*/
class QueuedDataFrame {
public:
dmr::data::DataHeader* header; //!< Instance of a PDU data header.
uint32_t dstId; //!< Destination Radio ID
uint32_t tgtProtoAddr; //!< Target Protocol Address
uint8_t* userData; //!< Raw data buffer
uint32_t userDataLen; //!< Length of raw data buffer
uint64_t timestamp; //!< Timestamp in milliseconds
uint8_t retryCnt; //!< Packet Retry Counter
bool extendRetry; //!< Flag indicating whether or not to extend the retry count for this packet.
};
concurrent::deque<QueuedDataFrame*> m_queuedFrames;
uint32_t m_queuedFrameBytes;
DMRPacketScheduler m_packetScheduler;
/**
* @brief Represents the receive status of a call.
@ -186,8 +168,8 @@ namespace network
typedef std::pair<const uint32_t, RxStatus*> StatusMapPair;
concurrent::unordered_map<uint32_t, RxStatus*> m_status;
typedef std::pair<const uint32_t, uint32_t> ArpTablePair;
std::unordered_map<uint32_t, uint32_t> m_arpTable;
RouteNeighborCache m_neighborCache;
DataLocationRegistry m_locationRegistry;
typedef std::pair<const uint32_t, bool> ReadyForNextPktPair;
std::unordered_map<uint32_t, bool> m_readyForNextPkt;
std::unordered_map<uint32_t, uint8_t> m_suSendSeq;

@ -0,0 +1,88 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "network/callhandler/packetdata/DMRPacketScheduler.h"
#include <utility>
using namespace network::callhandler::packetdata;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the DMRPacketScheduler class. */
DMRPacketScheduler::DMRPacketScheduler(uint32_t maxFrames, uint32_t maxBytes) :
m_maxFrames(maxFrames),
m_maxBytes(maxBytes),
m_byteCount(0U),
m_packets()
{
/* stub */
}
/* Enqueues a scheduled DMR packet. */
uint32_t DMRPacketScheduler::enqueue(ScheduledDMRDataPacket&& packet)
{
uint32_t dropped = 0U;
uint32_t packetBytes = uint32_t(packet.userData.size());
if (m_maxBytes != 0U && packetBytes > m_maxBytes)
return 1U;
while (!m_packets.empty() &&
((m_maxFrames != 0U && m_packets.size() >= m_maxFrames) ||
(m_maxBytes != 0U && m_byteCount + packetBytes > m_maxBytes))) {
pop();
dropped++;
}
m_byteCount += packetBytes;
m_packets.emplace_back(std::move(packet));
return dropped;
}
/* Gets the next queued packet. */
ScheduledDMRDataPacket* DMRPacketScheduler::front()
{
return m_packets.empty() ? nullptr : &m_packets.front();
}
/* Removes the next queued packet. */
void DMRPacketScheduler::pop()
{
if (m_packets.empty())
return;
uint32_t packetBytes = uint32_t(m_packets.front().userData.size());
m_byteCount = packetBytes <= m_byteCount ? m_byteCount - packetBytes : 0U;
m_packets.pop_front();
}
/* Moves the next queued packet to the back of the queue. */
void DMRPacketScheduler::rotate()
{
if (m_packets.size() < 2U)
return;
m_packets.emplace_back(std::move(m_packets.front()));
m_packets.pop_front();
}
/* Clears all queued packets. */
void DMRPacketScheduler::clear()
{
m_packets.clear();
m_byteCount = 0U;
}

@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file DMRPacketScheduler.h
* @ingroup fne_packetdata
* @file DMRPacketScheduler.cpp
* @ingroup fne_packetdata
*/
#if !defined(__PACKETDATA__DMR_PACKET_SCHEDULER_H__)
#define __PACKETDATA__DMR_PACKET_SCHEDULER_H__
#include "fne/Defines.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/data/DataHeader.h"
#include <cstdint>
#include <deque>
#include <vector>
namespace network
{
namespace callhandler
{
namespace packetdata
{
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief One queued DMR packet data downlink.
* @ingroup fne_packetdata
*/
struct HOST_SW_API ScheduledDMRDataPacket {
dmr::data::DataHeader header; //!< Header information for the DMR packet.
std::vector<uint8_t> userData; //!< User data payload for the DMR packet.
uint32_t dstId = 0U; //!< Destination radio ID.
uint32_t targetIPAddress = 0U; //!< Target IP address for the scheduled packet.
uint64_t dueAt = 0U; //!< Timestamp indicating when the packet is due.
uint8_t retryCount = 0U; //!< Number of retry attempts for the packet.
bool extendedRetry = false; //!< Indicates if extended retry is enabled.
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Owns bounded queued DMR packet data downlinks.
* @ingroup fne_packetdata
*/
class HOST_SW_API DMRPacketScheduler {
public:
/**
* @brief Initializes a new instance of the DMRPacketScheduler class.
* @param maxFrames The maximum number of frames the scheduler can hold.
* @param maxBytes The maximum number of bytes the scheduler can hold.
*/
DMRPacketScheduler(uint32_t maxFrames = 0U, uint32_t maxBytes = 0U);
/**
* @brief Enqueues a scheduled DMR packet.
* @param packet The scheduled packet to enqueue.
* @return The number of oldest frames dropped to make room.
*/
uint32_t enqueue(ScheduledDMRDataPacket&& packet);
bool empty() const noexcept { return m_packets.empty(); }
size_t size() const noexcept { return m_packets.size(); }
uint32_t byteCount() const noexcept { return m_byteCount; }
ScheduledDMRDataPacket* front();
void pop();
void rotate();
void clear();
private:
uint32_t m_maxFrames;
uint32_t m_maxBytes;
uint32_t m_byteCount;
std::deque<ScheduledDMRDataPacket> m_packets;
};
} // namespace packetdata
} // namespace callhandler
} // namespace network
#endif // __PACKETDATA__DMR_PACKET_SCHEDULER_H__

@ -1,15 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "common/p25/data/DataRouting.h"
#include "network/callhandler/packetdata/DataRouting.h"
using namespace p25::data;
using namespace network::callhandler::packetdata;
// ---------------------------------------------------------------------------
// Public Class Members
@ -17,22 +17,22 @@ using namespace p25::data;
/* Observes a route neighbor with the given link-layer ID and IP address. */
void RouteNeighborCache::observe(uint32_t llId, uint32_t ipAddress, uint64_t nowMs)
void RouteNeighborCache::observe(uint32_t subscriberId, uint32_t ipAddress, uint64_t nowMs)
{
if (llId == 0U || ipAddress == 0U)
if (subscriberId == 0U || ipAddress == 0U)
return;
RouteNeighbor& neighbor = m_neighbors[llId];
neighbor.llId = llId;
RouteNeighbor& neighbor = m_neighbors[subscriberId];
neighbor.subscriberId = subscriberId;
neighbor.ipAddress = ipAddress;
neighbor.lastSeen = nowMs;
}
/* Erases a route neighbor with the given link-layer ID. */
bool RouteNeighborCache::erase(uint32_t llId)
bool RouteNeighborCache::erase(uint32_t subscriberId)
{
return m_neighbors.erase(llId) > 0U;
return m_neighbors.erase(subscriberId) > 0U;
}
/* Clears all observed route neighbors. */
@ -44,9 +44,9 @@ void RouteNeighborCache::clear()
/* Finds a route neighbor by its link-layer ID. */
const RouteNeighbor* RouteNeighborCache::findByLLId(uint32_t llId) const
const RouteNeighbor* RouteNeighborCache::findBySubscriberId(uint32_t subscriberId) const
{
auto it = m_neighbors.find(llId);
auto it = m_neighbors.find(subscriberId);
return it == m_neighbors.end() ? nullptr : &it->second;
}
@ -165,6 +165,7 @@ DataRoute DataLocationRegistry::resolve(uint32_t llId, AccessMode mode, uint64_t
result.peerId = it->second.peerId;
result.channelId = it->second.channelId;
result.channelNo = it->second.channelNo;
result.slotNo = it->second.slotNo;
result.lastSeen = it->second.lastSeen;
result.valid = true;

@ -0,0 +1,329 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file DataRouting.h
* @ingroup fne_packetdata
* @file DataRouting.cpp
* @ingroup fne_packetdata
*/
#if !defined(__PACKETDATA__DATA_ROUTING_H__)
#define __PACKETDATA__DATA_ROUTING_H__
#include "fne/Defines.h"
#include <cstdint>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace network
{
namespace callhandler
{
namespace packetdata
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
* @addtogroup fne_packetdata
* @{
*/
/**
* @brief Access mode used to reach a packet data subscriber.
*/
enum class AccessMode : uint8_t {
CONVENTIONAL, //!< Conventional access mode.
TRUNKED //!< Trunked access mode.
};
/**
* @brief Policy applied when no current subscriber location is known.
*/
enum class UnknownLocationPolicy : uint8_t {
QUEUE, //!< Queue the packet until location is known.
ARP, //!< Attempt ARP resolution.
DROP //!< Drop the packet if location is unknown.
};
/* @} */
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Last observed conventional location for a subscriber.
* @ingroup fne_packetdata
*/
struct HOST_SW_API ConventionalLocation {
/**
* @brief Peer ID of the subscriber.
*/
uint32_t peerId = 0U;
/**
* @brief Channel ID of the subscriber.
*/
uint8_t channelId = 0U;
/**
* @brief Channel number of the subscriber.
*/
uint16_t channelNo = 0U;
/**
* @brief DMR timeslot, or zero when it is not applicable or known.
*/
uint8_t slotNo = 0U;
/**
* @brief Timestamp of the last observation.
*/
uint64_t lastSeen = 0U;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Resolved RF route for a packet data subscriber.
* @ingroup fne_packetdata
*/
struct HOST_SW_API DataRoute {
/**
* @brief Access mode used to reach the subscriber.
*/
AccessMode mode = AccessMode::CONVENTIONAL;
/**
* @brief Peer ID of the subscriber.
*/
uint32_t peerId = 0U;
/**
* @brief Channel ID of the subscriber.
*/
uint8_t channelId = 0U;
/**
* @brief Channel number of the subscriber.
*/
uint16_t channelNo = 0U;
/**
* @brief DMR timeslot, or zero when it is not applicable or known.
*/
uint8_t slotNo = 0U;
/**
* @brief Grant ID associated with the subscriber.
*/
uint32_t grantId = 0U;
/**
* @brief Timestamp of the last observation.
*/
uint64_t lastSeen = 0U;
/**
* @brief Indicates if the route is valid.
*/
bool valid = false;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Observed ARP neighbor information without authorization semantics.
* @ingroup fne_packetdata
*/
struct HOST_SW_API RouteNeighbor {
/**
* @brief Protocol subscriber identity of the neighbor.
*/
uint32_t subscriberId = 0U;
/**
* @brief IP address of the neighbor.
*/
uint32_t ipAddress = 0U;
/**
* @brief Timestamp of the last observation.
*/
uint64_t lastSeen = 0U;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Stores ARP observations separately from authorized bindings.
* @ingroup fne_packetdata
*/
class HOST_SW_API RouteNeighborCache {
public:
/**
* @brief Observes a route neighbor with the given link-layer ID and IP address.
* @param subscriberId Protocol subscriber identity of the neighbor.
* @param ipAddress IP address of the neighbor.
* @param nowMs Timestamp of the observation in milliseconds.
*/
void observe(uint32_t subscriberId, uint32_t ipAddress, uint64_t nowMs = 0U);
/**
* @brief Erases a route neighbor with the given link-layer ID.
* @param subscriberId Protocol subscriber identity of the neighbor to erase.
* @return True if the neighbor was found and erased, false otherwise.
*/
bool erase(uint32_t subscriberId);
/**
* @brief Clears all observed route neighbors.
*/
void clear();
/**
* @brief Finds a route neighbor by its subscriber identity.
* @param subscriberId Protocol subscriber identity of the neighbor to find.
* @return Pointer to the route neighbor if found, nullptr otherwise.
*/
const RouteNeighbor* findBySubscriberId(uint32_t subscriberId) const;
/**
* @brief Finds a route neighbor by its IP address.
* @param ipAddress IP address of the neighbor to find.
* @return Pointer to the route neighbor if found, nullptr otherwise.
*/
const RouteNeighbor* findByIPAddress(uint32_t ipAddress) const;
/**
* @brief Returns the number of observed route neighbors.
* @return Number of observed route neighbors.
*/
size_t size() const noexcept { return m_neighbors.size(); }
private:
std::unordered_map<uint32_t, RouteNeighbor> m_neighbors;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Resolves conventional locations and future trunked data routes.
* @ingroup fne_packetdata
*/
class HOST_SW_API DataLocationRegistry {
public:
/**
* @brief Initializes a new instance of the LocationRegistry class.
* @param staleAfterMs Duration in milliseconds after which locations are considered stale.
* @param learnFromInbound Whether to learn locations from inbound traffic.
* @param unknownLocationPolicy Policy for handling unknown locations.
*/
explicit DataLocationRegistry(uint64_t staleAfterMs = 0U, bool learnFromInbound = true,
UnknownLocationPolicy unknownLocationPolicy = UnknownLocationPolicy::ARP);
/**
* @brief Updates the conventional location for a given link-layer ID.
* @param llId Link-layer ID of the neighbor.
* @param location Conventional location to update.
* @return True if the location was updated, false otherwise.
*/
bool updateConventional(uint32_t llId, const ConventionalLocation& location);
/**
* @brief Updates the trunked data route for a given link-layer ID.
* @param llId Link-layer ID of the neighbor.
* @param route Trunked data route to update.
* @return True if the route was updated, false otherwise.
*/
bool updateTrunked(uint32_t llId, const DataRoute& route);
/**
* @brief Erases the location or route for a given link-layer ID and access mode.
* @param llId Link-layer ID of the neighbor.
* @param mode Access mode (conventional or trunked).
* @return True if the entry was erased, false otherwise.
*/
bool erase(uint32_t llId, AccessMode mode);
/**
* @brief Clears all subscriber and group route state.
*/
void clear();
/**
* @brief Expires stale conventional and trunked route state based on the current time.
* @param nowMs Current time in milliseconds.
*/
void expire(uint64_t nowMs);
/**
* @brief Resolves the data route for a given link-layer ID and access mode.
* @param llId Link-layer ID of the neighbor.
* @param mode Access mode (conventional or trunked).
* @param nowMs Current time in milliseconds.
* @return Resolved data route.
*/
DataRoute resolve(uint32_t llId, AccessMode mode, uint64_t nowMs = 0U) const;
/**
* @brief Sets the group peers for group-based routing.
* @param peerIds Vector of peer link-layer IDs.
*/
void setGroupPeers(const std::vector<uint32_t>& peerIds);
/**
* @brief Resolves the data routes for a group of available peers.
* @param availablePeerIds Vector of available peer link-layer IDs.
* @return Vector of resolved data routes for the available peers.
*/
std::vector<DataRoute> resolveGroup(const std::vector<uint32_t>& availablePeerIds) const;
/**
* @brief Sets whether to learn from inbound data routes.
* @param enabled True to enable learning from inbound routes, false to disable.
*/
void setLearnFromInbound(bool enabled) noexcept { m_learnFromInbound = enabled; }
/**
* @brief Checks whether learning from inbound data routes is enabled.
* @return True if learning from inbound routes is enabled, false otherwise.
*/
bool learnFromInbound() const noexcept { return m_learnFromInbound; }
/**
* @brief Sets the policy for handling unknown locations.
* @param policy The unknown location policy to set.
*/
void setUnknownLocationPolicy(UnknownLocationPolicy policy) noexcept { m_unknownLocationPolicy = policy; }
/**
* @brief Retrieves the current policy for handling unknown locations.
* @return The current unknown location policy.
*/
UnknownLocationPolicy unknownLocationPolicy() const noexcept { return m_unknownLocationPolicy; }
/**
* @brief Retrieves the count of conventional locations.
* @return The number of conventional locations.
*/
size_t conventionalCount() const noexcept { return m_conventional.size(); }
/**
* @brief Retrieves the count of trunked locations.
* @return The number of trunked locations.
*/
size_t trunkedCount() const noexcept { return m_trunked.size(); }
private:
uint64_t m_staleAfterMs;
bool m_learnFromInbound;
UnknownLocationPolicy m_unknownLocationPolicy;
std::unordered_map<uint32_t, ConventionalLocation> m_conventional;
std::unordered_map<uint32_t, DataRoute> m_trunked;
std::unordered_set<uint32_t> m_groupPeers;
/**
* @brief Checks if a location is considered stale based on the last seen timestamp and the current time.
* @param lastSeen The timestamp when the location was last seen.
* @param nowMs The current time in milliseconds.
* @return True if the location is stale, false otherwise.
*/
bool stale(uint64_t lastSeen, uint64_t nowMs) const;
};
} // namespace packetdata
} // namespace callhandler
} // namespace network
#endif // __PACKETDATA__DATA_ROUTING_H__

@ -27,6 +27,7 @@ using namespace system_clock;
using namespace network;
using namespace network::callhandler;
using namespace network::callhandler::packetdata;
using namespace network::callhandler::packetdata::p25data;
using namespace p25;
using namespace p25::defines;
using namespace p25::data;
@ -366,7 +367,7 @@ void P25PacketData::processPacketFrame(const uint8_t* data, uint32_t len, bool a
Utils::dump(1U, "P25, P25PacketData::processPacketFrame(), pduUserData", pduUserData, pduLength);
//#endif
ScheduledDataPacket packet;
ScheduledP25DataPacket packet;
packet.header = pktHeader;
packet.llId = llId;
packet.targetIPAddress = tgtProtoAddr;
@ -460,7 +461,7 @@ void P25PacketData::clock(uint32_t ms)
return;
}
ScheduledDataPacket* packet = m_packetScheduler.front();
ScheduledP25DataPacket* packet = m_packetScheduler.front();
if (packet == nullptr || now <= packet->dueAt)
return;
@ -1471,7 +1472,7 @@ bool P25PacketData::hasARPEntry(uint32_t llId) const
return false;
}
const RouteNeighbor* neighbor = m_neighborCache.findByLLId(llId);
const RouteNeighbor* neighbor = m_neighborCache.findBySubscriberId(llId);
return neighbor != nullptr && neighbor->ipAddress != 0U;
}

@ -9,9 +9,9 @@
*/
/**
* @file P25PacketData.h
* @ingroup fne_callhandler
* @ingroup fne_packetdata
* @file P25PacketData.cpp
* @ingroup fne_callhandler
* @ingroup fne_packetdata
*/
#if !defined(__PACKETDATA__P25_PACKET_DATA_H__)
#define __PACKETDATA__P25_PACKET_DATA_H__
@ -21,16 +21,16 @@
#include "common/concurrent/unordered_map.h"
#include "common/p25/P25Defines.h"
#include "common/p25/data/Assembler.h"
#include "common/p25/data/ConventionalDataService.h"
#include "common/p25/data/IPConvergenceService.h"
#include "common/p25/data/PacketDataState.h"
#include "common/p25/data/DataRouting.h"
#include "common/p25/data/PacketScheduler.h"
#include "common/p25/data/DataHeader.h"
#include "common/p25/data/DataBlock.h"
#include "network/TrafficNetwork.h"
#include "network/PeerNetwork.h"
#include "network/callhandler/TagP25Data.h"
#include "network/callhandler/packetdata/DataRouting.h"
#include "network/callhandler/packetdata/P25PacketScheduler.h"
#include "network/callhandler/packetdata/p25/ConventionalDataService.h"
#include "network/callhandler/packetdata/p25/IPConvergenceService.h"
#include "network/callhandler/packetdata/p25/PacketDataState.h"
namespace network
{
@ -44,7 +44,7 @@ namespace network
/**
* @brief Implements the P25 packet data handler.
* @ingroup fne_callhandler
* @ingroup fne_packetdata
*/
class HOST_SW_API P25PacketData {
public:
@ -120,7 +120,7 @@ namespace network
TrafficNetwork* m_network;
TagP25Data* m_tag;
p25::data::Assembler* m_assembler;
::p25::data::Assembler* m_assembler;
/**
* @brief Represents the data required for a PDU assembler custom writer context.
@ -131,12 +131,12 @@ namespace network
uint32_t peerId; //!< Peer ID for this request.
uint32_t srcPeerId; //!< Source Peer ID for this request.
network::PeerNetwork* peerNet; //!< Instance of the peer network for an upstream peer.
p25::data::DataHeader* header; //!< PDU data header.
::p25::data::DataHeader* header; //!< PDU data header.
uint16_t pktSeq; //!< Packet sequence.
uint32_t streamId; //!< Stream ID.
};
p25::data::PacketScheduler m_packetScheduler;
P25PacketScheduler m_packetScheduler;
/**
* @brief Represents the receive status of a call.
@ -150,7 +150,7 @@ namespace network
uint32_t peerId; //!< Peer ID
std::unordered_map<uint16_t, uint8_t*> receivedBlocks;
p25::data::Assembler assembler; //!< PDU Assembler Instance
::p25::data::Assembler assembler; //!< PDU Assembler Instance
bool hasRxHeader; //!< Flag indicating whether or not a valid Rx header has been received
uint16_t dataBlockCnt; //!< Number of data blocks received
uint16_t totalBlocks; //!< Total number of blocks expected
@ -211,12 +211,12 @@ namespace network
typedef std::pair<const uint32_t, RxStatus*> StatusMapPair;
concurrent::unordered_map<uint32_t, RxStatus*> m_status;
p25::data::RouteNeighborCache m_neighborCache;
p25::data::DataBindingRegistry m_bindingRegistry;
p25::data::DataLocationRegistry m_locationRegistry;
p25::data::DataLinkManager m_dataLinkManager;
p25::data::ConventionalDataService m_conventionalDataService;
p25::data::SCEPService m_scepService;
RouteNeighborCache m_neighborCache;
p25data::DataBindingRegistry m_bindingRegistry;
DataLocationRegistry m_locationRegistry;
p25data::DataLinkManager m_dataLinkManager;
p25data::ConventionalDataService m_conventionalDataService;
p25data::SCEPService m_scepService;
bool m_debug;
@ -237,7 +237,7 @@ namespace network
* @param auxiliaryES Flag indicating whether or not an auxiliary ES is included.
* @param pduUserData Buffer containing user data to transmit.
*/
void dispatchUserFrameToFNE(p25::data::DataHeader& dataHeader, bool extendedAddress, bool auxiliaryES, uint8_t* pduUserData);
void dispatchUserFrameToFNE(::p25::data::DataHeader& dataHeader, bool extendedAddress, bool auxiliaryES, uint8_t* pduUserData);
/**
* @brief Helper used to process conventional data registration from PDU data.
@ -276,7 +276,7 @@ namespace network
* @param auxiliaryES Flag indicating whether or not an auxiliary ES is included.
* @param pduUserData Buffer containing user data to transmit.
*/
void write_PDU_User(uint32_t peerId, uint32_t srcPeerId, network::PeerNetwork* peerNet, p25::data::DataHeader& dataHeader,
void write_PDU_User(uint32_t peerId, uint32_t srcPeerId, network::PeerNetwork* peerNet, ::p25::data::DataHeader& dataHeader,
bool extendedAddress, bool auxiliaryES, uint8_t* pduUserData);
/**
@ -291,7 +291,7 @@ namespace network
* @param pktSeq RTP packet sequence.
* @param streamId Stream ID.
*/
bool writeNetwork(uint32_t peerId, uint32_t srcPeerId, network::PeerNetwork* peerNet, const p25::data::DataHeader& dataHeader, const uint8_t currentBlock,
bool writeNetwork(uint32_t peerId, uint32_t srcPeerId, network::PeerNetwork* peerNet, const ::p25::data::DataHeader& dataHeader, const uint8_t currentBlock,
const uint8_t* data, uint32_t len, uint16_t pktSeq, uint32_t streamId);
/**

@ -1,25 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "common/p25/data/PacketScheduler.h"
#include "network/callhandler/packetdata/P25PacketScheduler.h"
#include <utility>
using namespace p25::data;
using namespace network::callhandler::packetdata;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the PacketScheduler class. */
/* Initializes a new instance of the P25PacketScheduler class. */
PacketScheduler::PacketScheduler(uint32_t maxFrames, uint32_t maxBytes) :
P25PacketScheduler::P25PacketScheduler(uint32_t maxFrames, uint32_t maxBytes) :
m_maxFrames(maxFrames),
m_maxBytes(maxBytes),
m_byteCount(0U),
@ -30,7 +30,7 @@ PacketScheduler::PacketScheduler(uint32_t maxFrames, uint32_t maxBytes) :
/* Enqueues a scheduled packet into the scheduler. */
uint32_t PacketScheduler::enqueue(ScheduledDataPacket&& packet)
uint32_t P25PacketScheduler::enqueue(ScheduledP25DataPacket&& packet)
{
uint32_t dropped = 0U;
uint32_t packetBytes = uint32_t(packet.userData.size());
@ -51,14 +51,14 @@ uint32_t PacketScheduler::enqueue(ScheduledDataPacket&& packet)
/* Gets the next queued packet. */
ScheduledDataPacket* PacketScheduler::front()
ScheduledP25DataPacket* P25PacketScheduler::front()
{
return m_packets.empty() ? nullptr : &m_packets.front();
}
/* Removes the next queued packet. */
void PacketScheduler::pop()
void P25PacketScheduler::pop()
{
if (m_packets.empty())
return;
@ -70,7 +70,7 @@ void PacketScheduler::pop()
/* Moves the next queued packet to the back of the queue. */
void PacketScheduler::rotate()
void P25PacketScheduler::rotate()
{
if (m_packets.size() < 2U)
return;
@ -81,7 +81,7 @@ void PacketScheduler::rotate()
/* Clears all queued packets from the scheduler. */
void PacketScheduler::clear()
void P25PacketScheduler::clear()
{
m_packets.clear();
m_byteCount = 0U;

@ -0,0 +1,118 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file P25PacketScheduler.h
* @ingroup fne_packetdata
* @file P25PacketScheduler.cpp
* @ingroup fne_packetdata
*/
#if !defined(__PACKETDATA__P25_PACKET_SCHEDULER_H__)
#define __PACKETDATA__P25_PACKET_SCHEDULER_H__
#include "fne/Defines.h"
#include "common/p25/data/DataHeader.h"
#include <cstdint>
#include <deque>
#include <vector>
namespace network
{
namespace callhandler
{
namespace packetdata
{
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief One queued packet data downlink.
* @ingroup fne_packetdata
*/
struct HOST_SW_API ScheduledP25DataPacket {
::p25::data::DataHeader header; //!< Header information for the P25 packet.
std::vector<uint8_t> userData; //!< User data payload for the P25 packet.
uint32_t llId = 0U; //!< Link-layer ID associated with the scheduled packet.
uint32_t targetIPAddress = 0U; //!< Target IP address for the scheduled packet.
uint64_t dueAt = 0U; //!< Timestamp indicating when the packet is due.
uint8_t retryCount = 0U; //!< Number of retry attempts for the packet.
bool extendedRetry = false; //!< Indicates if extended retry is enabled.
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Owns bounded queued packet data downlinks.
* @ingroup fne_packetdata
*/
class HOST_SW_API P25PacketScheduler {
public:
/**
* @brief Initializes a new instance of the P25PacketScheduler class.
* @param maxFrames The maximum number of frames the scheduler can hold.
* @param maxBytes The maximum number of bytes the scheduler can hold.
*/
P25PacketScheduler(uint32_t maxFrames = 0U, uint32_t maxBytes = 0U);
/**
* @brief Enqueues a scheduled packet into the scheduler.
* @param packet The scheduled packet to enqueue.
* @return The number of oldest frames dropped to make room.
*/
uint32_t enqueue(ScheduledP25DataPacket&& packet);
/**
* @brief Checks if the scheduler is empty.
* @return True if the scheduler is empty, false otherwise.
*/
bool empty() const noexcept { return m_packets.empty(); }
/**
* @brief Gets the number of packets currently in the scheduler.
* @return The number of packets in the scheduler.
*/
size_t size() const noexcept { return m_packets.size(); }
/**
* @brief Gets the total byte count of packets currently in the scheduler.
* @return The total byte count of packets in the scheduler.
*/
uint32_t byteCount() const noexcept { return m_byteCount; }
/**
* @brief Gets the next queued packet.
* @return A pointer to the next scheduled packet, or nullptr if the scheduler is empty.
*/
ScheduledP25DataPacket* front();
/**
* @brief Removes the next queued packet.
*/
void pop();
/**
* @brief Moves the next queued packet to the back of the queue.
*/
void rotate();
/**
* @brief Clears all queued packets from the scheduler.
*/
void clear();
private:
uint32_t m_maxFrames;
uint32_t m_maxBytes;
uint32_t m_byteCount;
std::deque<ScheduledP25DataPacket> m_packets;
};
} // namespace packetdata
} // namespace callhandler
} // namespace network
#endif // __PACKETDATA__P25_PACKET_SCHEDULER_H__

@ -1,17 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "common/p25/data/ConventionalDataService.h"
#include "network/callhandler/packetdata/p25/ConventionalDataService.h"
#include "common/p25/P25Defines.h"
using namespace p25::data;
using namespace p25::defines;
using namespace network::callhandler::packetdata::p25data;
using namespace ::p25::data;
using namespace ::p25::defines;
// ---------------------------------------------------------------------------
// Public Class Members

@ -0,0 +1,251 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file ConventionalDataService.h
* @ingroup fne_packetdata
* @file ConventionalDataService.cpp
* @ingroup fne_packetdata
*/
#if !defined(__P25_DATA__CONVENTIONALDATASERVICE_H__)
#define __P25_DATA__CONVENTIONALDATASERVICE_H__
#include "common/Defines.h"
#include "common/p25/data/ConventionalRegistration.h"
#include "network/callhandler/packetdata/p25/PacketDataState.h"
#include <cstdint>
#include <functional>
#include <unordered_map>
namespace network
{
namespace callhandler
{
namespace packetdata
{
namespace p25data
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
* @addtogroup fne_packetdata
* @{
*/
/**
* @brief Conventional packet data registration decision.
*/
enum class RegistrationDecision : uint8_t {
ACCEPT, //!< Registration request accepted.
DENY, //!< Registration request denied.
DISCONNECTED //!< Registration disconnected.
};
/**
* @brief Reason a conventional packet data registration was denied.
*/
enum class RegistrationDenyReason : uint8_t {
NONE, //!< No specific reason.
MALFORMED, //!< Registration request was malformed.
IDENTITY_MISMATCH, //!< Identity did not match expected value.
NOT_PROVISIONED, //!< Device not provisioned.
DISABLED, //!< Registration disabled.
UNSUPPORTED_OPTIONS, //!< Registration request contained unsupported options.
ADDRESS_UNAVAILABLE, //!< Requested address unavailable.
ADDRESS_CONFLICT //!< Address conflict detected.
};
/* @} */
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Provisioning inputs used to decide a registration request.
* @ingroup fne_packetdata
*/
struct ConventionalRegistrationProvisioning {
/**
* @brief Indicates whether the device is known.
*/
bool known = false;
/**
* @brief Indicates whether the device is enabled.
*/
bool enabled = false;
/**
* @brief Indicates whether dynamic address allocation is allowed.
*/
bool allowDynamicAddress = false;
/**
* @brief The static IP address of the device.
*/
uint32_t staticIPAddress = 0U;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Local policy applied to conventional registration requests.
* @ingroup fne_packetdata
*/
struct ConventionalRegistrationPolicy {
/**
* @brief Indicates whether CMS scan is allowed.
*/
bool allowCMSScan = false;
/**
* @brief The lifetime of a registration in milliseconds.
*/
uint64_t registrationLifetimeMs = 0U;
/**
* @brief Function to allocate a dynamic IP address.
*/
std::function<uint32_t(uint32_t, uint32_t)> allocateDynamicAddress;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Result of a conventional registration transaction.
* @ingroup fne_packetdata
*/
struct ConventionalRegistrationResult {
/**
* @brief The decision of the registration request.
*/
RegistrationDecision decision = RegistrationDecision::DENY;
/**
* @brief The reason for denying the registration request.
*/
RegistrationDenyReason denyReason = RegistrationDenyReason::MALFORMED;
/**
* @brief The response to the registration request.
*/
::p25::data::ConventionalRegistration response;
/**
* @brief Indicates whether the binding has changed as a result of the registration request.
*/
bool bindingChanged { false };
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements conventional packet data registration and binding state.
* @ingroup fne_packetdata
*/
class ConventionalDataService {
public:
/**
* @brief Initializes a new instance of the ConventionalDataService class.
* @param bindings The data binding registry.
* @param links The data link manager.
* @param policy The conventional registration policy.
*/
ConventionalDataService(DataBindingRegistry& bindings, DataLinkManager& links,
const ConventionalRegistrationPolicy& policy = ConventionalRegistrationPolicy());
/**
* @brief Processes a conventional registration request.
* @param data The raw registration request data.
* @param length The length of the registration request data.
* @param headerLlId The logical link identifier from the header.
* @param provisioning The conventional registration provisioning information.
* @param nowMs The current time in milliseconds.
* @return The result of the registration request.
*/
ConventionalRegistrationResult process(const uint8_t* data, uint32_t length,
uint32_t headerLlId, const ConventionalRegistrationProvisioning& provisioning,
uint64_t nowMs = 0U);
/**
* @brief Installs a static registration for the specified logical link identifier.
* @param llId The logical link identifier.
* @param ipAddress The IP address associated with the registration.
* @param authorized Indicates whether the registration is authorized.
* @return True if the static registration was successfully installed; otherwise, false.
*/
bool installStatic(uint32_t llId, uint32_t ipAddress, bool authorized = true);
/**
* @brief Expires registrations that have passed their expiration time.
* @param nowMs The current time in milliseconds.
*/
void expire(uint64_t nowMs);
/**
* @brief Checks if the specified logical link identifier is registered.
* @param llId The logical link identifier.
* @return True if the logical link identifier is registered; otherwise, false.
*/
bool isRegistered(uint32_t llId) const;
/**
* @brief Gets the number of current registrations.
* @return The number of current registrations.
*/
size_t registrationCount() const noexcept { return m_registrations.size(); }
/**
* @brief Converts a registration deny reason to a string representation.
* @param reason The registration deny reason.
* @return A string representation of the registration deny reason.
*/
static std::string denyReasonToString(RegistrationDenyReason reason);
private:
/**
* @brief Represents the state of a registration.
*/
struct RegistrationState {
/**
* @brief The IP address associated with the registration.
*/
uint32_t ipAddress = 0U;
/**
* @brief The options associated with the registration.
*/
uint8_t options = 0U;
/**
* @brief The expiration time of the registration in milliseconds.
*/
uint64_t expiresAtMs = 0U;
/**
* @brief Indicates whether the registration is dynamic.
*/
bool dynamic = false;
};
DataBindingRegistry& m_bindings;
DataLinkManager& m_links;
ConventionalRegistrationPolicy m_policy;
std::unordered_map<uint32_t, RegistrationState> m_registrations;
/**
* @brief Denies a registration request.
* @param request The registration request to deny.
* @param reason The reason for denying the registration.
* @return The result of the denial operation.
*/
ConventionalRegistrationResult deny(const ::p25::data::ConventionalRegistration& request,
RegistrationDenyReason reason) const;
};
} // namespace p25data
} // namespace packetdata
} // namespace callhandler
} // namespace network
#endif // __P25_DATA__CONVENTIONALDATASERVICE_H__

@ -1,17 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "common/p25/data/IPConvergenceService.h"
#include "network/callhandler/packetdata/p25/IPConvergenceService.h"
#include "common/p25/P25Defines.h"
using namespace p25::data;
using namespace p25::defines;
using namespace network::callhandler::packetdata::p25data;
using namespace ::p25::data;
using namespace ::p25::defines;
// ---------------------------------------------------------------------------
// Public Class Members

@ -0,0 +1,253 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file IPConvergenceService.h
* @ingroup fne_packetdata
* @file IPConvergenceService.cpp
* @ingroup fne_packetdata
*/
#if !defined(__P25_DATA__IPCONVERGENCESERVICE_H__)
#define __P25_DATA__IPCONVERGENCESERVICE_H__
#include "common/Defines.h"
#include "common/p25/data/IPv4Packet.h"
#include "network/callhandler/packetdata/p25/PacketDataState.h"
#include <cstdint>
namespace network
{
namespace callhandler
{
namespace packetdata
{
namespace p25data
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
* @addtogroup fne_packetdata
* @{
*/
/**
* @brief Result of an IP convergence operation.
*/
enum class ConvergenceResult : uint8_t {
OK, //!< Operation was successful.
MALFORMED, //!< The IP datagram is malformed.
NO_BINDING, //!< No binding exists for the IP datagram.
SOURCE_MISMATCH, //!< The source address does not match the expected source.
MTU_EXCEEDED, //!< The IP datagram exceeds the maximum transmission unit.
POLICY_REJECTED //!< The IP datagram was rejected by policy.
};
/**
* @brief LLC delivery mode selected for an IP datagram.
*/
enum class DataDeliveryMode : uint8_t {
CONFIRMED, //!< The IP datagram requires confirmed delivery.
UNCONFIRMED //!< The IP datagram does not require confirmed delivery.
};
/* @} */
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Result returned when decoding an IP datagram.
* @ingroup fne_packetdata
*/
struct IPDecodeResult {
/**
* @brief The result of the IP convergence operation.
*/
ConvergenceResult result = ConvergenceResult::MALFORMED;
/**
* @brief The decoded IP packet.
*/
::p25::data::IPv4Packet packet;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Result returned when routing an IP datagram for encoding.
* @ingroup fne_packetdata
*/
struct IPEncodeResult {
/**
* @brief The result of the IP convergence operation.
*/
ConvergenceResult result = ConvergenceResult::MALFORMED;
/**
* @brief The logical link identifier for the encoded IP datagram.
*/
uint32_t llId = 0U;
/**
* @brief The length of the encoded IP datagram.
*/
uint16_t length = 0U;
/**
* @brief The delivery mode selected for the encoded IP datagram.
*/
DataDeliveryMode delivery = DataDeliveryMode::CONFIRMED;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Policy controlling SCEP routing and delivery.
* @ingroup fne_packetdata
*/
struct SCEPPolicy {
/**
* @brief The maximum transmission unit for the SCEP policy.
*/
uint16_t mtu = 510U;
/**
* @brief Indicates whether broadcast transmission is allowed under the SCEP policy.
*/
bool allowBroadcast = true;
/**
* @brief Indicates whether multicast transmission is allowed under the SCEP policy.
*/
bool allowMulticast = false;
/**
* @brief The delivery mode selected for unicast transmissions under the SCEP policy.
*/
DataDeliveryMode unicastDelivery = DataDeliveryMode::CONFIRMED;
/**
* @brief The delivery mode selected for broadcast transmissions under the SCEP policy.
*/
DataDeliveryMode broadcastDelivery = DataDeliveryMode::UNCONFIRMED;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Defines an IP convergence service for a P25 packet data bearer.
* @ingroup fne_packetdata
*/
class IPConvergenceService {
public:
/**
* @brief Finalizes a instance of IPConvergenceService.
*/
virtual ~IPConvergenceService() = default;
/**
* @brief Decodes an IP datagram from the provided data buffer.
* @param data Pointer to the buffer containing the IP datagram.
* @param length Length of the data buffer.
* @return The result of the IP datagram decoding operation.
*/
virtual IPDecodeResult decode(const uint8_t* data, uint32_t length) const = 0;
/**
* @brief Encodes an IP datagram into the provided data buffer.
* @param data Pointer to the buffer where the IP datagram will be encoded.
* @param length Length of the data buffer.
* @return The result of the IP datagram encoding operation.
*/
virtual IPEncodeResult encode(const uint8_t* data, uint32_t length) const = 0;
/**
* @brief Authorizes an uplink transmission for the specified logical link and IP packet.
* @param llId The logical link identifier.
* @param packet The IP packet to be transmitted.
* @return The result of the uplink authorization operation.
*/
virtual ConvergenceResult authorizeUplink(uint32_t llId,
const ::p25::data::IPv4Packet& packet) const = 0;
/**
* @brief Routes a downlink transmission to the specified destination address.
* @param destinationAddress The destination IP address for the downlink transmission.
* @return The result of the downlink routing operation.
*/
virtual uint32_t routeDownlink(uint32_t destinationAddress) const = 0;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements the conventional SCEP IP convergence service.
* @ingroup fne_packetdata
*/
class SCEPService : public IPConvergenceService {
public:
/**
* @brief Initializes an instance of the SCEPService class.
* @param bindings The data binding registry to be used by the service.
* @param policy The SCEP policy to be applied by the service.
*/
explicit SCEPService(const DataBindingRegistry& bindings,
const SCEPPolicy& policy = SCEPPolicy());
/**
* @brief Decodes an IP datagram from the provided data buffer.
* @param data Pointer to the buffer containing the IP datagram.
* @param length Length of the data buffer.
* @return The result of the IP datagram decoding operation.
*/
IPDecodeResult decode(const uint8_t* data, uint32_t length) const override;
/**
* @brief Encodes an IP datagram into the provided data buffer.
* @param data Pointer to the buffer containing the IP datagram.
* @param length Length of the data buffer.
* @return The result of the IP datagram encoding operation.
*/
IPEncodeResult encode(const uint8_t* data, uint32_t length) const override;
/**
* @brief Authorizes an uplink transmission for the specified logical link ID and IP packet.
* @param llId The logical link ID for the uplink transmission.
* @param packet The IP packet to be transmitted.
* @return The result of the uplink authorization operation.
*/
ConvergenceResult authorizeUplink(uint32_t llId, const ::p25::data::IPv4Packet& packet) const override;
/**
* @brief Routes a downlink transmission to the specified destination address.
* @param destinationAddress The destination address for the downlink transmission.
* @return The logical link ID to which the downlink transmission should be routed.
*/
uint32_t routeDownlink(uint32_t destinationAddress) const override;
/**
* @brief Determines if the specified address is a broadcast address.
* @param address The address to be checked.
* @return True if the address is a broadcast address, false otherwise.
*/
static bool isBroadcast(uint32_t address);
/**
* @brief Determines if the specified address is a multicast address.
* @param address The address to be checked.
* @return True if the address is a multicast address, false otherwise.
*/
static bool isMulticast(uint32_t address);
private:
const DataBindingRegistry& m_bindings;
SCEPPolicy m_policy;
};
} // namespace p25data
} // namespace packetdata
} // namespace callhandler
} // namespace network
#endif // __P25_DATA__IPCONVERGENCESERVICE_H__

@ -1,15 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "common/p25/data/PacketDataState.h"
#include "network/callhandler/packetdata/p25/PacketDataState.h"
using namespace p25::data;
using namespace network::callhandler::packetdata::p25data;
// ---------------------------------------------------------------------------
// Public Class Members

@ -0,0 +1,304 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file PacketDataState.h
* @ingroup fne_packetdata
* @file PacketDataState.cpp
* @ingroup fne_packetdata
*/
#if !defined(__P25_DATA__PACKETDATASTATE_H__)
#define __P25_DATA__PACKETDATASTATE_H__
#include "common/Defines.h"
#include <cstdint>
#include <unordered_map>
namespace network
{
namespace callhandler
{
namespace packetdata
{
namespace p25data
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/**
* @addtogroup fne_packetdata
* @{
*/
/**
* @brief P25 packet data access mode.
*/
enum class DataAccessMode : uint8_t {
CONVENTIONAL, //!< Conventional access mode
TRUNKED //!< Trunked access mode
};
/**
* @brief P25 packet data convergence mode.
*/
enum class DataConvergenceMode : uint8_t {
CAI_DATA, //!< CAI data convergence mode
SCEP, //!< SCEP data convergence mode
SNDCP //!< SNDCP data convergence mode
};
/**
* @brief Source from which an IP binding was learned.
*/
enum class DataBindingOrigin : uint8_t {
PROVISIONED, //!< Provisioned binding origin
CONVENTIONAL_REGISTRATION, //!< Conventional registration binding origin
SNDCP_STATIC, //!< SNDCP static binding origin
SNDCP_DYNAMIC, //!< SNDCP dynamic binding origin
ARP_LEARNED //!< ARP learned binding origin
};
/**
* @brief Result of LLC receive sequence processing.
*/
enum class ReceiveSequenceResult : uint8_t {
ACCEPTED, //!< Accepted receive sequence result
DUPLICATE, //!< Duplicate receive sequence result
OUT_OF_SEQUENCE //!< Out of sequence receive sequence result
};
/* @} */
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Identifies a P25 logical data link.
* @ingroup fne_packetdata
*/
struct DataLinkKey {
/**
* @brief The logical link identifier.
*/
uint32_t llId = 0U;
/**
* @brief Compares this DataLinkKey with another for equality.
* @param other The other DataLinkKey to compare with.
* @return True if the DataLinkKeys are equal; otherwise, false.
*/
bool operator==(const DataLinkKey& other) const noexcept
{
return llId == other.llId;
}
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief Associates a logical data link with an authorized IP address.
* @ingroup fne_packetdata
*/
struct IPBinding {
/**
* @brief The logical data link associated with this IP binding.
*/
DataLinkKey link;
/**
* @brief The IP address associated with this binding.
*/
uint32_t ipAddress = 0U;
/**
* @brief The access mode for this binding.
*/
DataAccessMode accessMode = DataAccessMode::CONVENTIONAL;
/**
* @brief The convergence mode for this binding.
*/
DataConvergenceMode convergenceMode = DataConvergenceMode::SCEP;
/**
* @brief The origin from which this binding was learned.
*/
DataBindingOrigin origin = DataBindingOrigin::PROVISIONED;
/**
* @brief The NSAPI associated with this binding.
*/
uint8_t nsapi = 0U;
/**
* @brief Indicates whether the NSAPI is valid.
*/
bool hasNSAPI = false;
/**
* @brief Indicates whether this binding is authorized.
*/
bool authorized = false;
};
// ---------------------------------------------------------------------------
// Struct Declaration
// ---------------------------------------------------------------------------
/**
* @brief LLC sequencing and readiness state for one logical link.
* @ingroup fne_packetdata
*/
struct DataLinkState {
/**
* @brief The send sequence number for this logical link.
*/
uint8_t sendSequence = 0U;
/**
* @brief The receive sequence number for this logical link.
*/
uint8_t receiveSequence = 0U;
/**
* @brief The receive fingerprint for this logical link.
*/
uint32_t receiveFingerprint = 0U;
/**
* @brief Indicates whether the send sequence has been initialized.
*/
bool sendInitialized = false;
/**
* @brief Indicates whether the receive sequence has been initialized.
*/
bool receiveInitialized = false;
/**
* @brief Indicates whether the link is ready for the next packet.
*/
bool readyForNextPacket = true;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Stores typed P25 packet data IP bindings.
* @ingroup fne_packetdata
*/
class DataBindingRegistry {
public:
/**
* @brief Inserts or updates an IP binding in the registry. Returns true if the operation was successful.
* @param binding The IP binding to insert or update.
* @return True if the operation was successful, false otherwise.
*/
bool upsert(const IPBinding& binding);
/**
* @brief Erases an IP binding from the registry by its logical link ID. Returns true if the operation was successful.
* @param llId The logical link ID of the IP binding to erase.
* @return True if the operation was successful, false otherwise.
*/
bool erase(uint32_t llId);
/**
* @brief Clears all IP bindings from the registry.
*/
void clear();
/**
* @brief Finds an IP binding by its logical link ID.
* @param llId The logical link ID of the IP binding to find.
* @return A pointer to the IP binding if found, nullptr otherwise.
*/
const IPBinding* findByLLId(uint32_t llId) const;
/**
* @brief Finds an IP binding by its IP address.
* @param ipAddress The IP address of the IP binding to find.
* @return A pointer to the IP binding if found, nullptr otherwise.
*/
const IPBinding* findByIPAddress(uint32_t ipAddress) const;
/**
* @brief Returns the number of IP bindings in the registry.
* @return The number of IP bindings.
*/
size_t size() const noexcept { return m_bindings.size(); }
private:
std::unordered_map<uint32_t, IPBinding> m_bindings;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Stores LLC state independently for each logical link.
* @ingroup fne_packetdata
*/
class DataLinkManager {
public:
/**
* @brief Returns the next send sequence number for the specified logical link.
* @param llId The logical link ID.
* @param synchronize Indicates whether to synchronize the sequence.
* @return The next send sequence number.
*/
uint8_t nextSendSequence(uint32_t llId, bool& synchronize);
/**
* @brief Accepts a received sequence number for the specified logical link.
* @param llId The logical link ID.
* @param sequence The received sequence number.
* @param synchronize Indicates whether to synchronize the sequence.
* @param packetFingerprint The fingerprint of the received packet.
* @param expectedSequence The expected sequence number.
* @return The result of the receive sequence acceptance.
*/
ReceiveSequenceResult acceptReceiveSequence(uint32_t llId, uint8_t sequence,
bool synchronize, uint32_t packetFingerprint, uint8_t& expectedSequence);
/**
* @brief Computes the fingerprint of the given data.
* @param data Pointer to the data.
* @param length Length of the data.
* @return The computed fingerprint.
*/
static uint32_t fingerprint(const uint8_t* data, uint32_t length);
/**
* @brief Sets the readiness state for the specified logical link.
* @param llId The logical link ID.
* @param ready The readiness state to set.
*/
void setReady(uint32_t llId, bool ready);
/**
* @brief Checks if the specified logical link is ready.
* @param llId The logical link ID.
* @return True if the logical link is ready, false otherwise.
*/
bool isReady(uint32_t llId) const;
/**
* @brief Checks if the specified logical link has an associated state.
* @param llId The logical link ID.
* @return True if the logical link has an associated state, false otherwise.
*/
bool hasState(uint32_t llId) const;
/**
* @brief Erases the state associated with the specified logical link.
* @param llId The logical link ID.
*/
void erase(uint32_t llId);
/**
* @brief Clears all logical link states.
*/
void clear();
private:
std::unordered_map<uint32_t, DataLinkState> m_states;
};
} // namespace p25data
} // namespace packetdata
} // namespace callhandler
} // namespace network
#endif // __P25_DATA__PACKETDATASTATE_H__

@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Test Suite
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "network/callhandler/packetdata/DMRPacketScheduler.h"
#include "network/callhandler/packetdata/DataRouting.h"
using namespace network::callhandler::packetdata;
#include <catch2/catch_test_macros.hpp>
#include <utility>
TEST_CASE("DMR packet scheduler bounds queued downlinks",
"[dmr][packet-data][scheduler]")
{
DMRPacketScheduler scheduler(2U, 8U);
ScheduledDMRDataPacket first;
first.dstId = 1U;
first.userData.assign(4U, 0x11U);
CHECK(scheduler.enqueue(std::move(first)) == 0U);
ScheduledDMRDataPacket second;
second.dstId = 2U;
second.userData.assign(4U, 0x22U);
CHECK(scheduler.enqueue(std::move(second)) == 0U);
REQUIRE(scheduler.front() != nullptr);
CHECK(scheduler.front()->dstId == 1U);
ScheduledDMRDataPacket third;
third.dstId = 3U;
third.userData.assign(4U, 0x33U);
CHECK(scheduler.enqueue(std::move(third)) == 1U);
REQUIRE(scheduler.front() != nullptr);
CHECK(scheduler.front()->dstId == 2U);
CHECK(scheduler.size() == 2U);
CHECK(scheduler.byteCount() == 8U);
}
TEST_CASE("DMR scheduled packet owns its header and payload",
"[dmr][packet-data][scheduler][memory]")
{
DMRPacketScheduler scheduler(1U, 64U);
{
dmr::data::DataHeader source;
source.setSrcId(1001U);
source.setDstId(2002U);
ScheduledDMRDataPacket packet;
packet.header = source;
packet.userData.assign(20U, 0x55U);
REQUIRE(scheduler.enqueue(std::move(packet)) == 0U);
}
REQUIRE(scheduler.front() != nullptr);
CHECK(scheduler.front()->header.getSrcId() == 1001U);
CHECK(scheduler.front()->header.getDstId() == 2002U);
CHECK(scheduler.front()->userData.size() == 20U);
scheduler.clear();
CHECK(scheduler.empty());
}
TEST_CASE("DMR subscriber locations preserve peer and timeslot",
"[dmr][packet-data][routing]")
{
DataLocationRegistry locations(100U);
ConventionalLocation location;
location.peerId = 20U;
location.slotNo = 2U;
location.lastSeen = 1000U;
REQUIRE(locations.updateConventional(1001U, location));
DataRoute route = locations.resolve(1001U, AccessMode::CONVENTIONAL, 1050U);
REQUIRE(route.valid);
CHECK(route.peerId == 20U);
CHECK(route.slotNo == 2U);
location.peerId = 30U;
location.slotNo = 1U;
location.lastSeen = 1060U;
REQUIRE(locations.updateConventional(1001U, location));
route = locations.resolve(1001U, AccessMode::CONVENTIONAL, 1070U);
CHECK(route.peerId == 30U);
CHECK(route.slotNo == 1U);
locations.expire(1160U);
CHECK_FALSE(locations.resolve(1001U, AccessMode::CONVENTIONAL, 1160U).valid);
}

@ -10,15 +10,17 @@
#include "host/Defines.h"
#include "common/p25/P25Defines.h"
#include "common/p25/data/ConventionalRegistration.h"
#include "common/p25/data/ConventionalDataService.h"
#include "common/p25/data/IPConvergenceService.h"
#include "common/p25/data/IPv4Packet.h"
#include "common/p25/data/PacketDataState.h"
#include "common/p25/data/DataRouting.h"
#include "common/p25/data/PacketScheduler.h"
#include "network/callhandler/packetdata/p25/PacketDataState.h"
#include "network/callhandler/packetdata/DataRouting.h"
#include "network/callhandler/packetdata/P25PacketScheduler.h"
#include "network/callhandler/packetdata/p25/ConventionalDataService.h"
#include "network/callhandler/packetdata/p25/IPConvergenceService.h"
using namespace p25::data;
using namespace p25::defines;
using namespace network::callhandler::packetdata::p25data;
using namespace network::callhandler::packetdata;
#include <catch2/catch_test_macros.hpp>
@ -375,6 +377,7 @@ TEST_CASE("P25 conventional locations move, expire, and resolve targeted routes"
location.peerId = 10U;
location.channelId = 1U;
location.channelNo = 101U;
location.slotNo = 2U;
location.lastSeen = 1000U;
REQUIRE(locations.updateConventional(1001U, location));
@ -383,6 +386,7 @@ TEST_CASE("P25 conventional locations move, expire, and resolve targeted routes"
CHECK(route.peerId == 10U);
CHECK(route.channelId == 1U);
CHECK(route.channelNo == 101U);
CHECK(route.slotNo == 2U);
// Later inbound traffic moves the subscriber to peer B.
location.peerId = 20U;
@ -451,8 +455,8 @@ TEST_CASE("P25 neighbor observations do not create bindings or routes",
DataLocationRegistry locations;
neighbors.observe(1001U, 0x0A000001U, 10U);
REQUIRE(neighbors.findByLLId(1001U) != nullptr);
CHECK(neighbors.findByIPAddress(0x0A000001U)->llId == 1001U);
REQUIRE(neighbors.findBySubscriberId(1001U) != nullptr);
CHECK(neighbors.findByIPAddress(0x0A000001U)->subscriberId == 1001U);
CHECK(bindings.findByLLId(1001U) == nullptr);
CHECK_FALSE(locations.resolve(1001U, AccessMode::CONVENTIONAL).valid);
}
@ -460,13 +464,13 @@ TEST_CASE("P25 neighbor observations do not create bindings or routes",
TEST_CASE("P25 packet scheduler bounds and rotates queued downlinks",
"[p25][packet-data][scheduler]")
{
PacketScheduler scheduler(2U, 8U);
ScheduledDataPacket first;
P25PacketScheduler scheduler(2U, 8U);
ScheduledP25DataPacket first;
first.llId = 1U;
first.userData.assign(4U, 0x11U);
CHECK(scheduler.enqueue(std::move(first)) == 0U);
ScheduledDataPacket second;
ScheduledP25DataPacket second;
second.llId = 2U;
second.userData.assign(4U, 0x22U);
CHECK(scheduler.enqueue(std::move(second)) == 0U);
@ -476,7 +480,7 @@ TEST_CASE("P25 packet scheduler bounds and rotates queued downlinks",
scheduler.rotate();
CHECK(scheduler.front()->llId == 2U);
ScheduledDataPacket third;
ScheduledP25DataPacket third;
third.llId = 3U;
third.userData.assign(4U, 0x33U);
CHECK(scheduler.enqueue(std::move(third)) == 1U);
@ -487,7 +491,7 @@ TEST_CASE("P25 packet scheduler bounds and rotates queued downlinks",
TEST_CASE("P25 scheduled packet header assignment owns an independent copy",
"[p25][packet-data][scheduler][memory]")
{
PacketScheduler scheduler(1U, 64U);
P25PacketScheduler scheduler(1U, 64U);
std::array<uint8_t, MI_LENGTH_BYTES> originalMI {
0x01U, 0x23U, 0x45U, 0x67U, 0x89U, 0xABU, 0xCDU, 0xEFU, 0x10U
};
@ -498,7 +502,7 @@ TEST_CASE("P25 scheduled packet header assignment owns an independent copy",
source.setSAP(PDUSAP::PACKET_DATA);
source.setMI(originalMI.data());
ScheduledDataPacket packet;
ScheduledP25DataPacket packet;
packet.header = source;
packet.userData.assign(20U, 0x55U);
REQUIRE(scheduler.enqueue(std::move(packet)) == 0U);

Loading…
Cancel
Save

Powered by TurnKey Linux.