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