diff --git a/FneSystemBase.cs b/FneSystemBase.cs
index 2699275..03f6f28 100644
--- a/FneSystemBase.cs
+++ b/FneSystemBase.cs
@@ -7,7 +7,7 @@
* @package DVM / Fixed Network Equipment Core Library
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL
+* Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
*
*/
@@ -22,6 +22,7 @@ using fnecore.EDAC;
using fnecore.DMR;
using fnecore.P25;
using fnecore.NXDN;
+using fnecore.P25.LC.TSBK;
namespace fnecore
{
@@ -526,6 +527,30 @@ namespace fnecore
peer.SendMaster(FneBase.CreateOpcode(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, Constants.RtpCallEndSeq, callData.TxStreamID);
}
+ ///
+ /// Helper to send a DVM call termination TSDU.
+ ///
+ ///
+ ///
+ public void SendDVMCallTermination(uint srcId, uint dstId)
+ {
+ OSP_DVM_LC_CALL_TERM osp = new OSP_DVM_LC_CALL_TERM(dstId, srcId);
+
+ RemoteCallData callData = new RemoteCallData
+ {
+ MFId = P25Defines.P25_MFG_DVM_OCS,
+ SrcId = srcId,
+ DstId = dstId,
+ LCO = P25Defines.LC_CALL_TERM
+ };
+
+ byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES];
+
+ osp.Encode(ref tsbk);
+
+ SendP25TSBK(callData, tsbk);
+ }
+
///
/// Helper to send a P25 TDU message.
///
diff --git a/P25/P25Defines.cs b/P25/P25Defines.cs
index 345fb85..0328497 100644
--- a/P25/P25Defines.cs
+++ b/P25/P25Defines.cs
@@ -154,6 +154,7 @@ namespace fnecore.P25
public const byte P25_FT_DATA_UNIT = 0x00;
public const byte P25_MFG_STANDARD = 0x00;
+ public const byte P25_MFG_DVM_OCS = 0x9C;
public const byte P25_ALGO_UNENCRYPT = 0x80;
public const byte P25_ALGO_DES = 0x81;
diff --git a/P25/lc/tsbk/OSP_DVM_LC_CALL_TERM.cs b/P25/lc/tsbk/OSP_DVM_LC_CALL_TERM.cs
new file mode 100644
index 0000000..543a4f1
--- /dev/null
+++ b/P25/lc/tsbk/OSP_DVM_LC_CALL_TERM.cs
@@ -0,0 +1,80 @@
+// 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) 2026 Bryan Biedenkapp, N2PLL
+*
+*/
+
+namespace fnecore.P25.LC.TSBK
+{
+ ///
+ /// OSP_DVM_LC_CALL_TERM TSBK
+ ///
+ public class OSP_DVM_LC_CALL_TERM : TSBKBase
+ {
+ public byte GrpVchId;
+ public uint GrpVchNo;
+ public uint DstId;
+ public uint SrcId;
+
+ ///
+ /// Creates an instance of
+ ///
+ ///
+ ///
+ public OSP_DVM_LC_CALL_TERM(uint dstId = 0, uint srcId = 0)
+ {
+ DstId = dstId;
+ SrcId = srcId;
+ Lco = P25Defines.LC_CALL_TERM;
+ }
+
+ ///
+ /// Decode CALL_ALRT TSBK
+ ///
+ ///
+ ///
+ ///
+ public override bool Decode(byte[] data, bool rawTSBK = true)
+ {
+ if (!base.Decode(data, rawTSBK))
+ return false;
+
+ ulong tsbkValue = FneUtils.ToUInt64(Payload, 0);
+
+ GrpVchId = (byte)((tsbkValue >> 52) & 0x0F); // Channel ID
+ GrpVchNo = (uint)((tsbkValue >> 40) & 0xFFF); // Channel Number
+ DstId = (uint)((tsbkValue >> 24) & 0xFFFF); // Target Radio Address
+ SrcId = (uint)(tsbkValue & 0xFFFFFF); // Source Radio Address
+
+ return true;
+ }
+
+ ///
+ /// Encode CALL_ALRT TSBK
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override void Encode(ref byte[] data, bool rawTSBK = true, bool noTrellis = true)
+ {
+ ulong tsbkValue = 0;
+ tsbkValue = (tsbkValue << 4) + GrpVchNo; // Channel ID
+ tsbkValue = (tsbkValue << 12) + GrpVchId; // Channel Number
+ tsbkValue = (tsbkValue << 16) + DstId; // Target Radio Address
+ tsbkValue = (tsbkValue << 24) + SrcId; // Source Radio Address
+
+ FneUtils.Memset(Payload, 0x00, Payload.Length);
+ FneUtils.WriteBytes(tsbkValue, ref Payload, 0);
+
+ base.Encode(ref data, rawTSBK, noTrellis);
+ }
+ } // public class OSP_DVM_LC_CALL_TERM
+} // namespace fnecore.P25.LC.TSBK