diff --git a/Analog/AnalogDefines.cs b/Analog/AnalogDefines.cs
new file mode 100644
index 0000000..f58de14
--- /dev/null
+++ b/Analog/AnalogDefines.cs
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - Fixed Network Equipment Core Library
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Fixed Network Equipment Core Library
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
+*
+*/
+
+using System;
+
+namespace fnecore.Analog
+{
+ ///
+ /// Audio Frame Type(s)
+ ///
+ public enum AudioFrameType : byte
+ {
+ ///
+ /// Voice Start Frame
+ ///
+ VOICE_START = 0x00,
+ ///
+ /// Voice
+ ///
+ VOICE = 0x01,
+ ///
+ /// Voice End Frame / Call Terminator
+ ///
+ TERMINATOR = 0x02
+ } // public enum AudioFrameType : byte
+} // namespace fnecore.Analog
diff --git a/Constants.cs b/Constants.cs
index e7e5252..8357f96 100644
--- a/Constants.cs
+++ b/Constants.cs
@@ -7,7 +7,7 @@
* @package DVM / Fixed Network Equipment Core Library
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* Copyright (C) 2022 Bryan Biedenkapp, N2PLL
+* Copyright (C) 2022,2025 Bryan Biedenkapp, N2PLL
*
*/
@@ -196,6 +196,19 @@ namespace fnecore
public const byte DVMRtpPayloadType = 0x56;
public const byte DVMFrameStart = 0xFE;
+ public const uint DMRPacketLength = 55U; // 20 byte header + DMR_FRAME_LENGTH_BYTES + 2 byte trailer
+ public const uint P25LDU1PacketLength = 193U; // 24 byte header + DFSI data + 1 byte frame type + 12 byte enc sync
+ public const uint P25LDU2PacketLength = 181U; // 24 byte header + DFSI data + 1 byte frame type
+ public const uint P25TSDUPacketLength = 69U; // 24 byte header + TSDU data
+ public const uint P25TDULCPacketLength = 78U; // 24 byte header + TDULC data
+ public const uint NXDNPacketLength = 70U; // 20 byte header + NXDN_FRAME_LENGTH_BYTES + 2 byte trailer
+ public const uint AnalogPacketLength = 324U; // 20 byte header + AUDIO_SAMPLES_LENGTH_BYTES + 4 byte trailer
+
+ public const uint HAParamsEntryLen = 20;
+
+ public const int MAX_RETRY_BEFORE_RECONNECT = 4;
+ public const int MAX_RETRY_HA_RECONNECT = 2;
+
/*
** Protocol Functions and Sub-Functions
*/
@@ -205,12 +218,14 @@ namespace fnecore
public const byte NET_PROTOCOL_SUBFUNC_DMR = 0x00; // DMR
public const byte NET_PROTOCOL_SUBFUNC_P25 = 0x01; // P25
public const byte NET_PROTOCOL_SUBFUNC_NXDN = 0x02; // NXDN
+ public const byte NET_PROTOCOL_SUBFUNC_ANALOG = 0x03; // Analog
public const byte NET_FUNC_MASTER = 0x01; // Network Master Function
public const byte NET_MASTER_SUBFUNC_WL_RID = 0x00; // Whitelist RIDs
public const byte NET_MASTER_SUBFUNC_BL_RID = 0x01; // Blacklist RIDs
public const byte NET_MASTER_SUBFUNC_ACTIVE_TGS = 0x02; // Active TGIDs
public const byte NET_MASTER_SUBFUNC_DEACTIVE_TGS = 0x03; // Deactive TGIDs
+ public const byte NET_MASTER_SUBFUNC_HA_PARAMS = 0xA3; // HA Parameters
public const byte NET_FUNC_RPTL = 0x60; // Repeater Login
public const byte NET_FUNC_RPTK = 0x61; // Repeater Authorisation
@@ -223,6 +238,7 @@ namespace fnecore
public const byte NET_FUNC_PONG = 0x75; // Pong
public const byte NET_FUNC_GRANT = 0x7A; // Grant Request
+ public const byte NET_FUNC_INCALL_CTRL = 0x7B; // In-CAll Control
public const byte NET_FUNC_KEY_REQ = 0x7C; // Encryption Key Request
public const byte NET_FUNC_KEY_RSP = 0x7D; // Encryption Key Response
@@ -240,6 +256,9 @@ namespace fnecore
public const byte NET_ANNC_SUBFUNC_GRP_UNAFFIL = 0x03; // Announce Group Affiliation Removal
public const byte NET_ANNC_SUBFUNC_AFFILS = 0x90; // Update All Affiliations
+ public const byte NET_ICC_BUSY_DENY = 0x00; // In-Call Busy Deny
+ public const byte NET_ICC_REJECT_TRAFFIC = 0x01; // In-Call Reject Active Traffic
+
/*
** Protocol Tags (as strings)
*/
diff --git a/FneBase.cs b/FneBase.cs
index cbd2629..9ca841b 100644
--- a/FneBase.cs
+++ b/FneBase.cs
@@ -7,7 +7,7 @@
* @package DVM / Fixed Network Equipment Core Library
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* Copyright (C) 2022-2023 Bryan Biedenkapp, N2PLL
+* Copyright (C) 2022-2025 Bryan Biedenkapp, N2PLL
*
*/
@@ -18,6 +18,7 @@ using System.Security.Cryptography;
using fnecore.DMR;
using fnecore.P25;
using fnecore.NXDN;
+using fnecore.Analog;
using fnecore.EDAC;
using fnecore.P25.KMM;
@@ -190,6 +191,80 @@ namespace fnecore
}
} // public class PeerInformation
+ ///
+ /// Represents a talkgroup as announced from the FNE master.
+ ///
+ public class TalkgroupEntry
+ {
+ ///
+ /// Talkgroup ID.
+ ///
+ public uint ID;
+ ///
+ /// Slot Number.
+ ///
+ public byte Slot;
+
+ ///
+ ///
+ ///
+ public bool Affiliated;
+ ///
+ ///
+ ///
+ public bool NonPreferred;
+
+ ///
+ ///
+ ///
+ public bool Invalid;
+ } // public class TalkgroupEntry
+
+ ///
+ /// Represents high availiability IP address data.
+ ///
+ public class PeerHAIPEntry
+ {
+ ///
+ /// IP Address
+ ///
+ public string Address;
+ ///
+ /// Port Number
+ ///
+ public int Port;
+
+ ///
+ ///
+ ///
+ public IPEndPoint EndPoint;
+
+ /*
+ ** Methods
+ */
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PeerHAIPEntry()
+ {
+ Address = "127.0.0.1";
+ Port = 62031; // this should be a constant not a magic number -- but I digress
+ EndPoint = new IPEndPoint(IPAddress.Parse(Address), Port);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// IP Address
+ /// Port number
+ public PeerHAIPEntry(string address, int port)
+ {
+ Address = address;
+ Port = port;
+ EndPoint = new IPEndPoint(IPAddress.Parse(Address), Port);
+ }
+ } // public class PeerHAIPEntry
+
///
/// Callback used to validate incoming DMR data.
///
@@ -296,6 +371,54 @@ namespace fnecore
Buffer.BlockCopy(data, 0, Data, 0, data.Length);
}
} // public class DMRDataReceivedEvent : EventArgs
+ ///
+ /// Event used to process incoming DMR In-Call control data.
+ ///
+ public class DMRInCallControlEvent : EventArgs
+ {
+ ///
+ /// Peer ID
+ ///
+ public uint PeerId { get; }
+ ///
+ /// Destination Address
+ ///
+ public uint DstId { get; }
+ ///
+ /// Slot Number
+ ///
+ public byte Slot { get; }
+ ///
+ /// In-Call Control Command
+ ///
+ public byte Command { get; }
+
+ /*
+ ** Methods
+ */
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private DMRInCallControlEvent()
+ {
+ /* stub */
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Peer ID
+ /// Destination Address
+ /// Slot Number
+ /// In-Call Command
+ public DMRInCallControlEvent(uint peerId, uint dstId, byte slot, byte command) : base()
+ {
+ this.PeerId = peerId;
+ this.DstId = dstId;
+ this.Slot = slot;
+ this.Command = command;
+ }
+ } // public class DMRDataReceivedEvent : EventArgs
///
/// Callback used to validate incoming P25 data.
@@ -390,6 +513,48 @@ namespace fnecore
Buffer.BlockCopy(data, 0, Data, 0, data.Length);
}
} // public class P25DataReceivedEvent : EventArgs
+ ///
+ /// Event used to process incoming P25 In-Call control data.
+ ///
+ public class P25InCallControlEvent : EventArgs
+ {
+ ///
+ /// Peer ID
+ ///
+ public uint PeerId { get; }
+ ///
+ /// Destination Address
+ ///
+ public uint DstId { get; }
+ ///
+ /// In-Call Control Command
+ ///
+ public byte Command { get; }
+
+ /*
+ ** Methods
+ */
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private P25InCallControlEvent()
+ {
+ /* stub */
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Peer ID
+ /// Destination Address
+ /// In-Call Command
+ public P25InCallControlEvent(uint peerId, uint dstId, byte command) : base()
+ {
+ this.PeerId = peerId;
+ this.DstId = dstId;
+ this.Command = command;
+ }
+ } // public class P25InCallControlEvent : EventArgs
///
/// Callback used to validate incoming NXDN data.
@@ -484,6 +649,184 @@ namespace fnecore
Buffer.BlockCopy(data, 0, Data, 0, data.Length);
}
} // public class NXDNDataReceivedEvent : EventArgs
+ ///
+ /// Event used to process incoming NXDN In-Call control data.
+ ///
+ public class NXDNInCallControlEvent : EventArgs
+ {
+ ///
+ /// Peer ID
+ ///
+ public uint PeerId { get; }
+ ///
+ /// Destination Address
+ ///
+ public uint DstId { get; }
+ ///
+ /// In-Call Control Command
+ ///
+ public byte Command { get; }
+
+ /*
+ ** Methods
+ */
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private NXDNInCallControlEvent()
+ {
+ /* stub */
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Peer ID
+ /// Destination Address
+ /// In-Call Command
+ public NXDNInCallControlEvent(uint peerId, uint dstId, byte command) : base()
+ {
+ this.PeerId = peerId;
+ this.DstId = dstId;
+ this.Command = command;
+ }
+ } // public class NXDNInCallControlEvent : EventArgs
+
+ ///
+ /// Callback used to validate incoming analog data.
+ ///
+ /// Peer ID
+ /// Source Address
+ /// Destination Address
+ /// Call Type (Group or Private)
+ /// Analog Audio Frame Type
+ /// Frame Type
+ /// Stream ID
+ /// Raw message data
+ /// True, if data stream is valid, otherwise false.
+ public delegate bool AnalogDataValidate(uint peerId, uint srcId, uint dstId, CallType callType, AudioFrameType audioFrameType, FrameType frameType, uint streamId, byte[] message);
+ ///
+ /// Event used to process incoming analog data.
+ ///
+ public class AnalogDataReceivedEvent : EventArgs
+ {
+ ///
+ /// Peer ID
+ ///
+ public uint PeerId { get; }
+ ///
+ /// Source Address
+ ///
+ public uint SrcId { get; }
+ ///
+ /// Destination Address
+ ///
+ public uint DstId { get; }
+ ///
+ /// Call Type (Group or Private)
+ ///
+ public CallType CallType { get; }
+ ///
+ /// Audio Frame Type
+ ///
+ public AudioFrameType AudioFrameType { get; }
+ ///
+ /// Frame Type
+ ///
+ public FrameType FrameType { get; }
+ ///
+ /// RTP Packet Sequence
+ ///
+ public ushort PacketSequence { get; }
+ ///
+ /// Stream ID
+ ///
+ public uint StreamId { get; }
+ ///
+ /// Raw message data
+ ///
+ public byte[] Data { get; }
+
+ /*
+ ** Methods
+ */
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private AnalogDataReceivedEvent()
+ {
+ /* stub */
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Peer ID
+ /// Source Address
+ /// Destination Address
+ /// Call Type (Group or Private)
+ /// Audio Message Type
+ /// Frame Type
+ /// RTP Packet Sequence
+ /// Stream ID
+ /// Raw message data
+ public AnalogDataReceivedEvent(uint peerId, uint srcId, uint dstId, CallType callType, AudioFrameType audioFrameType, FrameType frameType, ushort pktSeq, uint streamId, byte[] data) : base()
+ {
+ this.PeerId = peerId;
+ this.SrcId = srcId;
+ this.DstId = dstId;
+ this.CallType = callType;
+ this.AudioFrameType = audioFrameType;
+ this.FrameType = frameType;
+ this.PacketSequence = pktSeq;
+ this.StreamId = streamId;
+
+ this.Data = new byte[data.Length];
+ Buffer.BlockCopy(data, 0, Data, 0, data.Length);
+ }
+ } // public class AnalogDataReceivedEvent : EventArgs
+ ///
+ /// Event used to process incoming analog In-Call control data.
+ ///
+ public class AnalogInCallControlEvent : EventArgs
+ {
+ ///
+ /// Peer ID
+ ///
+ public uint PeerId { get; }
+ ///
+ /// Destination Address
+ ///
+ public uint DstId { get; }
+ ///
+ /// In-Call Control Command
+ ///
+ public byte Command { get; }
+
+ /*
+ ** Methods
+ */
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private AnalogInCallControlEvent()
+ {
+ /* stub */
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Peer ID
+ /// Destination Address
+ /// In-Call Command
+ public AnalogInCallControlEvent(uint peerId, uint dstId, byte command) : base()
+ {
+ this.PeerId = peerId;
+ this.DstId = dstId;
+ this.Command = command;
+ }
+ } // public class AnalogInCallControlEvent : EventArgs
///
/// Callback used to process whether or not a peer is being ignored for traffic.
@@ -629,6 +972,10 @@ namespace fnecore
/// Event action that handles processing a DMR call stream.
///
public event EventHandler DMRDataReceived;
+ ///
+ /// Event action that handles processing a DMR In-Call control request.
+ ///
+ public event EventHandler DMRInCallControl;
///
/// Callback action that handles validating a P25 call stream.
@@ -642,6 +989,10 @@ namespace fnecore
/// Event action that handles processing a P25 call stream.
///
public event EventHandler P25DataReceived;
+ ///
+ /// Event action that handles processing a P25 In-Call control request.
+ ///
+ public event EventHandler P25InCallControl;
///
/// Callback action that handles validating a NXDN call stream.
@@ -651,6 +1002,23 @@ namespace fnecore
/// Event action that handles processing a NXDN call stream.
///
public event EventHandler NXDNDataReceived;
+ ///
+ /// Event action that handles processing a NXDN In-Call control request.
+ ///
+ public event EventHandler NXDNInCallControl;
+
+ ///
+ /// Callback action that handles validating a analog call stream.
+ ///
+ public AnalogDataValidate AnalogDataValidate = null;
+ ///
+ /// Event action that handles processing a analog call stream.
+ ///
+ public event EventHandler AnalogDataReceived;
+ ///
+ /// Event action that handles processing a analog In-Call control request.
+ ///
+ public event EventHandler AnalogInCallControl;
///
/// Callback action that handles verifying if a peer is ignored for a call stream.
@@ -877,6 +1245,16 @@ namespace fnecore
DMRDataReceived.Invoke(this, e);
}
+ ///
+ /// Helper to fire the DMR In-Call control event.
+ ///
+ /// instance
+ protected void FireDMRInCallControl(DMRInCallControlEvent e)
+ {
+ if (DMRInCallControl != null)
+ DMRInCallControl.Invoke(this, e);
+ }
+
///
/// Helper to fire the P25 data pre-process event.
///
@@ -897,6 +1275,16 @@ namespace fnecore
P25DataReceived.Invoke(this, e);
}
+ ///
+ /// Helper to fire the P25 In-Call control event.
+ ///
+ /// instance
+ protected void FireP25InCallControl(P25InCallControlEvent e)
+ {
+ if (P25InCallControl != null)
+ P25InCallControl.Invoke(this, e);
+ }
+
///
/// Helper to fire the NXDN data received event.
///
@@ -907,6 +1295,36 @@ namespace fnecore
NXDNDataReceived.Invoke(this, e);
}
+ ///
+ /// Helper to fire the NXDN In-Call control event.
+ ///
+ /// instance
+ protected void FireNXDNInCallControl(NXDNInCallControlEvent e)
+ {
+ if (NXDNInCallControl != null)
+ NXDNInCallControl.Invoke(this, e);
+ }
+
+ ///
+ /// Helper to fire the analog data received event.
+ ///
+ /// instance
+ protected void FireAnalogDataReceived(AnalogDataReceivedEvent e)
+ {
+ if (AnalogDataReceived != null)
+ AnalogDataReceived.Invoke(this, e);
+ }
+
+ ///
+ /// Helper to fire the analog In-Call control event.
+ ///
+ /// instance
+ protected void FireAnalogInCallControl(AnalogInCallControlEvent e)
+ {
+ if (AnalogInCallControl != null)
+ AnalogInCallControl.Invoke(this, e);
+ }
+
///
/// Helper to fire the peer connected event.
///
diff --git a/FnePeer.cs b/FnePeer.cs
index 4273547..22c9559 100644
--- a/FnePeer.cs
+++ b/FnePeer.cs
@@ -7,7 +7,7 @@
* @package DVM / Fixed Network Equipment Core Library
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* Copyright (C) 2022-2023 Bryan Biedenkapp, N2PLL
+* Copyright (C) 2022-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2024 Caleb, KO4UYJ
*
*/
@@ -26,8 +26,8 @@ using System.Collections.Generic;
using fnecore.DMR;
using fnecore.P25;
using fnecore.NXDN;
+using fnecore.Analog;
using fnecore.P25.KMM;
-using System.Net.NetworkInformation;
namespace fnecore
{
@@ -62,6 +62,13 @@ namespace fnecore
private ushort currPktSeq = 0;
private uint streamId = 0;
+ private List announcedTGs = new List();
+ private List haIPs = new List();
+ private int currentHAIP;
+
+ private int retryCount;
+ private int maxRetryCount = Constants.MAX_RETRY_BEFORE_RECONNECT;
+
/*
** Properties
*/
@@ -84,6 +91,11 @@ namespace fnecore
set;
}
+ ///
+ /// Gets the list of talkgroups announced from the master FNE.
+ ///
+ public List AnnouncedTGs => announcedTGs;
+
///
/// Gets the number of pings sent.
///
@@ -402,6 +414,30 @@ namespace fnecore
return curr;
}
+ ///
+ /// Helper to rotate the master endpoint to the next HA endpoint.
+ ///
+ private void RotateMasterEndpont()
+ {
+ // are we rotating IPs for HA reconnect?
+ if (haIPs.Count() > 0 && retryCount > 0U && maxRetryCount == Constants.MAX_RETRY_HA_RECONNECT)
+ {
+
+ PeerHAIPEntry entry = haIPs[currentHAIP];
+ currentHAIP++;
+
+ if (currentHAIP > haIPs.Count)
+ {
+ currentHAIP = 0;
+ }
+
+ Log(LogLevel.ERROR, $"({systemName}) Not connected or lost connection to {masterEndpoint}; trying next HA {entry.EndPoint}...");
+ masterEndpoint = entry.EndPoint;
+ }
+
+ ++retryCount;
+ }
+
///
/// Internal UDP listen routine.
///
@@ -461,7 +497,7 @@ namespace fnecore
{
case Constants.NET_FUNC_PROTOCOL:
{
- if (fneHeader.SubFunction == Constants.NET_PROTOCOL_SUBFUNC_DMR) // Encapsulated DMR data frame
+ if (fneHeader.SubFunction == Constants.NET_PROTOCOL_SUBFUNC_DMR) // Encapsulated DMR data frame
{
if (peerId != this.peerId)
{
@@ -472,6 +508,8 @@ namespace fnecore
// is this for our peer?
if (peerId == this.peerId)
{
+ // TODO: port the mux validation logic from dvmhost:src/common/network/Network.cpp
+
byte seqNo = message[4];
uint srcId = FneUtils.Bytes3ToUInt32(message, 5);
uint dstId = FneUtils.Bytes3ToUInt32(message, 8);
@@ -493,7 +531,7 @@ namespace fnecore
FireDMRDataReceived(new DMRDataReceivedEvent(peerId, srcId, dstId, slot, callType, frameType, dataType, n, rtpHeader.Sequence, streamId, message));
}
}
- else if (fneHeader.SubFunction == Constants.NET_PROTOCOL_SUBFUNC_P25) // Encapsulated P25 data frame
+ else if (fneHeader.SubFunction == Constants.NET_PROTOCOL_SUBFUNC_P25) // Encapsulated P25 data frame
{
if (peerId != this.peerId)
{
@@ -504,6 +542,8 @@ namespace fnecore
// is this for our peer?
if (peerId == this.peerId)
{
+ // TODO: port the mux validation logic from dvmhost:src/common/network/Network.cpp
+
uint srcId = FneUtils.Bytes3ToUInt32(message, 5);
uint dstId = FneUtils.Bytes3ToUInt32(message, 8);
CallType callType = (message[4] == P25Defines.LC_PRIVATE) ? CallType.PRIVATE : CallType.GROUP;
@@ -516,7 +556,7 @@ namespace fnecore
FireP25DataReceived(new P25DataReceivedEvent(peerId, srcId, dstId, callType, duid, frameType, rtpHeader.Sequence, streamId, message));
}
}
- else if (fneHeader.SubFunction == Constants.NET_PROTOCOL_SUBFUNC_NXDN) // Encapsulated NXDN data frame
+ else if (fneHeader.SubFunction == Constants.NET_PROTOCOL_SUBFUNC_NXDN) // Encapsulated NXDN data frame
{
if (peerId != this.peerId)
{
@@ -527,6 +567,8 @@ namespace fnecore
// is this for our peer?
if (peerId == this.peerId)
{
+ // TODO: port the mux validation logic from dvmhost:src/common/network/Network.cpp
+
NXDNMessageType messageType = (NXDNMessageType)message[4];
uint srcId = FneUtils.Bytes3ToUInt32(message, 5);
uint dstId = FneUtils.Bytes3ToUInt32(message, 8);
@@ -541,6 +583,31 @@ namespace fnecore
FireNXDNDataReceived(new NXDNDataReceivedEvent(peerId, srcId, dstId, callType, messageType, frameType, rtpHeader.Sequence, streamId, message));
}
}
+ else if (fneHeader.SubFunction == Constants.NET_PROTOCOL_SUBFUNC_ANALOG) // Encapsulated Analog data frame
+ {
+ if (peerId != this.peerId)
+ {
+ //Log(LogLevel.WARNING, $"({systemName}) PEER {peerId}; routed traffic, rewriting PEER {this.peerId}");
+ peerId = this.peerId;
+ }
+
+ // is this for our peer?
+ if (peerId == this.peerId)
+ {
+ // TODO: port the mux validation logic from dvmhost:src/common/network/Network.cpp
+
+ uint srcId = FneUtils.Bytes3ToUInt32(message, 5);
+ uint dstId = FneUtils.Bytes3ToUInt32(message, 8);
+ CallType callType = CallType.GROUP; /* analog calls cannot be private calls right now ... */
+ AudioFrameType audioFrameType = (AudioFrameType)(message[15] & 0x0F);
+ FrameType frameType = (audioFrameType != AudioFrameType.TERMINATOR) ? FrameType.VOICE : FrameType.TERMINATOR;
+#if DEBUG
+ Log(LogLevel.DEBUG, $"{systemName} P25D: SRC_PEER {peerId} SRC_ID {srcId} DST_ID {dstId} [STREAM ID {streamId}]");
+#endif
+ // perform any userland actions with the data
+ FireAnalogDataReceived(new AnalogDataReceivedEvent(peerId, srcId, dstId, callType, audioFrameType, frameType, rtpHeader.Sequence, streamId, message));
+ }
+ }
else
{
Log(LogLevel.ERROR, $"({systemName}) Unknown protocol opcode {FneUtils.BytesToString(message, 0, 4)} -- {FneUtils.HexDump(message, 0)}");
@@ -548,9 +615,166 @@ namespace fnecore
}
break;
- case Constants.NET_FUNC_MASTER:
+ case Constants.NET_FUNC_MASTER: // Master
+ {
+ if (this.peerId == peerId)
+ {
+ // process incoming message subfunction opcodes
+ switch (fneHeader.SubFunction)
+ {
+ case Constants.NET_MASTER_SUBFUNC_ACTIVE_TGS: // Talkgroup Active IDs
+ {
+ uint len = FneUtils.ToUInt32(message, 6);
+ int offs = 11;
+ for (int i = 0; i < len; i++)
+ {
+ uint id = FneUtils.Bytes3ToUInt32(message, offs);
+ byte slot = (byte)(message[offs + 3] & 0x03);
+ bool affiliated = (message[offs + 3] & 0x40) == 0x40;
+ bool nonPreferred = (message[offs + 3] & 0x80) == 0x80;
+
+ TalkgroupEntry entry = new TalkgroupEntry()
+ {
+ ID = id,
+ Slot = slot,
+ Affiliated = affiliated,
+ NonPreferred = nonPreferred,
+ Invalid = false
+ };
+
+ int idx = announcedTGs.FindIndex(x => x.ID == id && x.Slot == slot);
+ if (idx != -1)
+ announcedTGs[idx] = entry;
+ else
+ announcedTGs.Add(entry);
+
+ offs += 5;
+ }
+
+ Log(LogLevel.INFO, $"Activated {len} TGs; loaded {announcedTGs.Count} entries into talkgroup table");
+ }
+ break;
+ case Constants.NET_MASTER_SUBFUNC_DEACTIVE_TGS: // Talkgroup Deactivated IDs
+ {
+ uint len = FneUtils.ToUInt32(message, 6);
+ int offs = 11;
+ for (int i = 0; i < len; i++)
+ {
+ uint id = FneUtils.Bytes3ToUInt32(message, offs);
+ byte slot = message[offs + 3];
+
+ int idx = announcedTGs.FindIndex(x => x.ID == id && x.Slot == slot);
+ if (idx != -1)
+ announcedTGs[idx].Invalid = true;
+ else
+ {
+ TalkgroupEntry entry = new TalkgroupEntry()
+ {
+ ID = id,
+ Slot = slot,
+ Affiliated = false,
+ NonPreferred = false,
+ Invalid = true
+ };
+ announcedTGs.Add(entry);
+ }
+
+ offs += 5;
+ }
+
+ Log(LogLevel.INFO, $"Deactivated {len} TGs; loaded {announcedTGs.Count} entries into talkgroup table");
+ }
+ break;
+
+ case Constants.NET_MASTER_SUBFUNC_HA_PARAMS: // HA Parameters
+ {
+ haIPs.Clear();
+ currentHAIP = 0;
+ maxRetryCount = Constants.MAX_RETRY_HA_RECONNECT;
+
+ // always add the configured address to the HA IP list
+ haIPs.Add(new PeerHAIPEntry(masterEndpoint.Address.ToString(), masterEndpoint.Port));
+
+ uint len = FneUtils.ToUInt32(message, 6);
+ if (len > 0)
+ len /= Constants.HAParamsEntryLen;
+
+ int offs = 10;
+ for (int i = 0; i < len; i++, offs += (int)Constants.HAParamsEntryLen)
+ {
+ uint ipAddr = FneUtils.ToUInt32(message, offs + 4);
+ ushort port = FneUtils.ToUInt16(message, offs + 8);
+
+ IPAddress address = new IPAddress(ipAddr);
+ haIPs.Add(new PeerHAIPEntry(address.ToString(), port));
+ }
+
+ if (haIPs.Count > 1)
+ {
+ currentHAIP = 1;
+ Log(LogLevel.INFO, $"Loaded {haIPs.Count} HA IPs from master");
+ }
+ }
+ break;
+
+ default:
+ Log(LogLevel.ERROR, $"({systemName}) Unknown protocol opcode {FneUtils.BytesToString(message, 0, 4)} -- {FneUtils.HexDump(message, 0)}");
+ break;
+ }
+ }
+ }
+ break;
+
+ case Constants.NET_FUNC_INCALL_CTRL: // In-Call Control
{
- /* stub */
+ if (this.peerId == peerId)
+ {
+ // process incoming message subfunction opcodes
+ switch (fneHeader.SubFunction)
+ {
+ case Constants.NET_PROTOCOL_SUBFUNC_DMR: // DMR In-Call Control
+ {
+ byte command = message[10];
+ uint dstId = FneUtils.Bytes3ToUInt32(message, 11);
+ byte slot = message[14];
+
+ // fire off DMR in-call callback if we have one
+ FireDMRInCallControl(new DMRInCallControlEvent(peerId, dstId, slot, command));
+ }
+ break;
+ case Constants.NET_PROTOCOL_SUBFUNC_P25: // P25 In-Call Control
+ {
+ byte command = message[10];
+ uint dstId = FneUtils.Bytes3ToUInt32(message, 11);
+
+ // fire off P25 in-call callback if we have one
+ FireP25InCallControl(new P25InCallControlEvent(peerId, dstId, command));
+ }
+ break;
+ case Constants.NET_PROTOCOL_SUBFUNC_NXDN: // NXDN In-Call Control
+ {
+ byte command = message[10];
+ uint dstId = FneUtils.Bytes3ToUInt32(message, 11);
+
+ // fire off NXDN in-call callback if we have one
+ FireNXDNInCallControl(new NXDNInCallControlEvent(peerId, dstId, command));
+ }
+ break;
+ case Constants.NET_PROTOCOL_SUBFUNC_ANALOG: // Analog In-Call Control
+ {
+ byte command = message[10];
+ uint dstId = FneUtils.Bytes3ToUInt32(message, 11);
+
+ // fire off analog in-call callback if we have one
+ FireAnalogInCallControl(new AnalogInCallControlEvent(peerId, dstId, command));
+ }
+ break;
+
+ default:
+ Log(LogLevel.ERROR, $"({systemName}) Unknown incall control opcode {FneUtils.BytesToString(message, 0, 4)} -- {FneUtils.HexDump(message, 0)}");
+ break;
+ }
+ }
}
break;
@@ -735,6 +959,7 @@ namespace fnecore
Log(LogLevel.INFO, $"({systemName}) PEER {this.peerId} connection to MASTER completed");
this.streamId = 0;
+ retryCount = 0; // reset retry count
// userland actions
FirePeerConnected(new PeerConnectedEvent(peerId, info));
@@ -799,6 +1024,7 @@ namespace fnecore
catch (InvalidOperationException)
{
Log(LogLevel.ERROR, $"({systemName}) Not connected or lost connection to {masterEndpoint}; reconnecting...");
+ RotateMasterEndpont();
// reset states
PingsSent = 0;
@@ -817,6 +1043,7 @@ namespace fnecore
case SocketError.ConnectionAborted:
case SocketError.ConnectionRefused:
Log(LogLevel.ERROR, $"({systemName}) Not connected or lost connection to {masterEndpoint}; reconnecting...");
+ RotateMasterEndpont();
// reset states
PingsSent = 0;
@@ -871,6 +1098,7 @@ namespace fnecore
if (PingsSent > (PingsAcked + MAX_MISSED_PEER_PINGS))
{
Log(LogLevel.WARNING, $"({systemName} Peer connection lost to {masterEndpoint}; reconnecting...");
+ RotateMasterEndpont();
// reset states
PingsSent = 0;
@@ -893,6 +1121,7 @@ namespace fnecore
catch (InvalidOperationException)
{
Log(LogLevel.ERROR, $"({systemName}) Not connected or lost connection to {masterEndpoint}; reconnecting...");
+ RotateMasterEndpont();
// reset states
PingsSent = 0;
@@ -911,6 +1140,7 @@ namespace fnecore
case SocketError.ConnectionAborted:
case SocketError.ConnectionRefused:
Log(LogLevel.ERROR, $"({systemName}) Not connected or lost connection to {masterEndpoint}; reconnecting...");
+ RotateMasterEndpont();
// reset states
PingsSent = 0;
diff --git a/README.md b/README.md
index d769891..05c7836 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Digital Voice Modem Fixed Network Equipment
-This is a library project that implements the basic communications layer for implementing DVM FNE clients (peers) and servers (masters).
+This is a library project that implements the basic communications layer for implementing DVM FNE clients (peers).
## License