2083 lines
87 KiB
C#
2083 lines
87 KiB
C#
|
using Independentsoft.Sip;
|
|||
|
using Independentsoft.Sip.Methods;
|
|||
|
using Independentsoft.Sip.Responses;
|
|||
|
using Independentsoft.Sip.Sdp;
|
|||
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Collections.ObjectModel;
|
|||
|
using System.Globalization;
|
|||
|
using System.Linq;
|
|||
|
using System.Net;
|
|||
|
using System.Net.Sockets;
|
|||
|
using System.Threading;
|
|||
|
using System.Threading.Tasks;
|
|||
|
|
|||
|
namespace SipComponent
|
|||
|
{
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Class used to make SD talk to Linx using Adi's protocol, version 3
|
|||
|
/// <para>Accepts more than one sip call at a time</para>
|
|||
|
/// <para>Does not include a socket.IO component</para>
|
|||
|
/// </summary>
|
|||
|
class SipClientClass3
|
|||
|
{
|
|||
|
#region Private Fields
|
|||
|
|
|||
|
private SipClient _sipClient;
|
|||
|
|
|||
|
private int _registrationInterval = 100; // 100s
|
|||
|
private RegistrationData _registrationData;
|
|||
|
private int _minPortNb = 25284;
|
|||
|
private int _maxPortNb = 25300;
|
|||
|
private int _audioBitRate = 16;
|
|||
|
private int _maxNbOfDialogs = 3;
|
|||
|
private int _secondsToWaitForSmsConfirmation = 3;
|
|||
|
private int _sipDomainPort;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Nr de secunde cat este mentinut call-ul cand nu se transmite voce
|
|||
|
/// </summary>
|
|||
|
private int _hangTimeDuration = -1;
|
|||
|
private int _bufferMiliseconds;
|
|||
|
private string _localIPaddress;
|
|||
|
private Random _rand = new Random();
|
|||
|
private bool _sipClassClosed = false;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets or sets a value indicating if the Asterisk server confirms sms
|
|||
|
/// <para>Default value is false</para>
|
|||
|
/// </summary>
|
|||
|
protected bool _smsConfirmationFromServer = false;
|
|||
|
|
|||
|
private Dictionary<string, Invite> _IDsentInviteDict = new Dictionary<string, Invite>();
|
|||
|
private Dictionary<string, Request> _IDreceivedInviteDict = new Dictionary<string, Request>();
|
|||
|
private Dictionary<string, Tuple<UdpClient, RTPSender2, RTPListener2, TimerWithTag<string>>> _IDdialogTuple = new Dictionary<string, Tuple<UdpClient, RTPSender2, RTPListener2, TimerWithTag<string>>>();
|
|||
|
private Dictionary<string, bool?> _smsSentDict = new Dictionary<string, bool?>();
|
|||
|
private Dictionary<string, Tuple<System.Timers.Timer, RegistrationStatus>> _sipID_regTimer_regStatus_Dict =
|
|||
|
new Dictionary<string, Tuple<System.Timers.Timer, RegistrationStatus>>();
|
|||
|
private Dictionary<string, Tuple<byte[], TimerWithTag<string>>> _ID_pingBytest_timer_dict = new Dictionary<string, Tuple<byte[], TimerWithTag<string>>>();
|
|||
|
private Dictionary<string, string> _simocoID_emergencyStatusReport_dict = new Dictionary<string, string>();
|
|||
|
|
|||
|
|
|||
|
private object _lockerSmsSet = new object();
|
|||
|
|
|||
|
private List<string> _IDsCalledByMeList = new List<string>();
|
|||
|
private List<string> _IDsCallingMeList = new List<string>();
|
|||
|
private List<string> _IDsInDialogWithList = new List<string>();
|
|||
|
private List<string> _IDsregisteredList = new List<string>();
|
|||
|
|
|||
|
private static object _lockerRtpPort = new object();
|
|||
|
private object _lockerSipDialog = new object();
|
|||
|
private object _lockerCallingMe = new object();
|
|||
|
|
|||
|
#region Protection against multiple sip messages sent (sometimes) by Asterisk
|
|||
|
|
|||
|
string _previousEmergencySeqID = "";
|
|||
|
string _previousLocationSeqID = "";
|
|||
|
string _previousArsSeqID = "";
|
|||
|
string _previousSmsSeqID = "";
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
HashSet<string> _linxGroupSipIDsInDialogWith = new HashSet<string>();
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Public Properties
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets or sets the minimum port number in the port range used for RTP voice transmission
|
|||
|
/// </summary>
|
|||
|
public int MinRtpPortNumber
|
|||
|
{
|
|||
|
get { return _minPortNb; }
|
|||
|
set
|
|||
|
{
|
|||
|
if (ValidRtpPort(value))
|
|||
|
{
|
|||
|
_minPortNb = value;
|
|||
|
}
|
|||
|
else
|
|||
|
throw new SipClassException(
|
|||
|
string.Format("Port {0} is not a valid RTP port", value));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets or sets the maximum port number in the port range used for RTP voice transmission
|
|||
|
/// </summary>
|
|||
|
public int MaxRtpPortNumber
|
|||
|
{
|
|||
|
get { return _maxPortNb; }
|
|||
|
set
|
|||
|
{
|
|||
|
if (ValidRtpPort(value))
|
|||
|
{
|
|||
|
_maxPortNb = value;
|
|||
|
}
|
|||
|
else
|
|||
|
throw new SipClassException(
|
|||
|
string.Format("Port {0} is not a valid RTP port", value));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets the sip ID
|
|||
|
/// </summary>
|
|||
|
public string UserName
|
|||
|
{
|
|||
|
get { return _sipClient.Username; }
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// <para>Gets or sets the maximum number of simultaneous dialogs that the SipClientClass accepts</para>
|
|||
|
/// <para>The default is 3</para>
|
|||
|
/// </summary>
|
|||
|
public int MaxNbOfDialogs
|
|||
|
{
|
|||
|
get { return _maxNbOfDialogs; }
|
|||
|
set
|
|||
|
{
|
|||
|
_maxNbOfDialogs = value;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// <para>Gets or sets the number of seconds the call is maintained when no voice is transmited</para>
|
|||
|
/// <para>Value is in seconds</para>
|
|||
|
/// <para>Default value is -1, call is maintained indefinitely</para>
|
|||
|
/// </summary>
|
|||
|
public int HangTimeDuration
|
|||
|
{
|
|||
|
get { return _hangTimeDuration; }
|
|||
|
set
|
|||
|
{
|
|||
|
if (value > 0)
|
|||
|
_hangTimeDuration = value;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// <para>Gets or sets a value indicating the nb of seconds to wait</para>
|
|||
|
/// <para>for a sms confirmation from sip server</para>
|
|||
|
/// <para>Default value is 3</para>
|
|||
|
/// </summary>
|
|||
|
public int SecondsToWaitForSmsConfirmation
|
|||
|
{
|
|||
|
get { return _secondsToWaitForSmsConfirmation; }
|
|||
|
set
|
|||
|
{
|
|||
|
if (value > 0 && value < 10)
|
|||
|
_secondsToWaitForSmsConfirmation = value;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Audio BitRate, default value is 16
|
|||
|
/// </summary>
|
|||
|
public int AudioBitrate
|
|||
|
{
|
|||
|
get { return _audioBitRate; }
|
|||
|
set { _audioBitRate = value; }
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets a readonly collection of sip ids that you sent Invite for
|
|||
|
/// </summary>
|
|||
|
public ReadOnlyCollection<string> SipIDsCalledByMe
|
|||
|
{
|
|||
|
get { return new ReadOnlyCollection<string>(_IDsCalledByMeList); }
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets a readonly collection of sip ids that sent you Invite's
|
|||
|
/// </summary>
|
|||
|
public ReadOnlyCollection<string> SipIDsCallingMe
|
|||
|
{
|
|||
|
get { return new ReadOnlyCollection<string>(_IDsCallingMeList); }
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets a readonly collection of sip ids that you are in dialog with
|
|||
|
/// </summary>
|
|||
|
public ReadOnlyCollection<string> SipIDsInDialogWith
|
|||
|
{
|
|||
|
get { return new ReadOnlyCollection<string>(_IDsInDialogWithList); }
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets a readonly collection of sip ids that you are registered to
|
|||
|
/// <para>This means your sip id and the group ids that you are listening to</para>
|
|||
|
/// </summary>
|
|||
|
public ReadOnlyCollection<string> SipIDsRegisteredTo
|
|||
|
{
|
|||
|
get
|
|||
|
{
|
|||
|
return new ReadOnlyCollection<string>(_IDsregisteredList);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets or sets a value indicating if the Asterisk sends sms confirmations on delivery
|
|||
|
/// <para>Default value is false</para>
|
|||
|
/// </summary>
|
|||
|
public bool SmsConfirmationFromAsterisk
|
|||
|
{
|
|||
|
get { return _smsConfirmationFromServer; }
|
|||
|
set { _smsConfirmationFromServer = true; }
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Constructor
|
|||
|
|
|||
|
// constructor
|
|||
|
/// <summary>
|
|||
|
/// Constructor for the SipClientClass
|
|||
|
/// </summary>
|
|||
|
/// <param name="sipDomain">domain name, name or IP address of sip server</param>
|
|||
|
/// <param name="sipDomainPort">port number of the sip server</param>
|
|||
|
/// <param name="localSipPort">port number of the local computer used for sip protocol</param>
|
|||
|
/// <param name="userName">user name on the sip server</param>
|
|||
|
/// <param name="password">password on the sip server</param>
|
|||
|
/// <param name="registrationInterval">interval to send sip registration requests. Value is in seconds</param>
|
|||
|
/// <param name="bufferMiliseconds">Miliseconds for the buffer that stores the received voice packets</param>
|
|||
|
/// <param name="requestTimeout">Number of ms to wait before the sip request times out</param>
|
|||
|
/// <param name="socketIOport">Port number use for socket.IO</param>
|
|||
|
/// <param name="localIPAddress">Local Ip adress. If not specified, the class will search for a local ip on the same network with the sip server ip</param>
|
|||
|
public SipClientClass3(string sipDomain, int sipDomainPort, int localSipPort, string userName, string password, int registrationInterval, int bufferMiliseconds,
|
|||
|
int requestTimeout, int socketIOport, string localIPAddress = null)
|
|||
|
{
|
|||
|
|
|||
|
if (localIPAddress == null)
|
|||
|
{
|
|||
|
_localIPaddress = BestLocalEndPoint(new IPEndPoint(IPAddress.Parse(sipDomain), sipDomainPort)).Address.ToString();
|
|||
|
#if DEBUG
|
|||
|
Console.WriteLine("Local IP selected: " + _localIPaddress);
|
|||
|
#endif
|
|||
|
}
|
|||
|
else
|
|||
|
_localIPaddress = localIPAddress;
|
|||
|
_sipDomainPort = sipDomainPort;
|
|||
|
_sipClient = CreateSipClientClass(sipDomain, _sipDomainPort, _localIPaddress, localSipPort, userName, password, requestTimeout);
|
|||
|
_bufferMiliseconds = bufferMiliseconds;
|
|||
|
// Registration Timer
|
|||
|
_registrationInterval = registrationInterval;
|
|||
|
_registrationData = new RegistrationData(userName, _registrationInterval + 2);
|
|||
|
System.Timers.Timer registrationTimer = new System.Timers.Timer();
|
|||
|
// Set up the registration timer
|
|||
|
registrationTimer.Interval = _registrationInterval * 1000;
|
|||
|
registrationTimer.Elapsed += _registrationTimer_Elapsed;
|
|||
|
_sipID_regTimer_regStatus_Dict.Add(userName,
|
|||
|
new Tuple<System.Timers.Timer, RegistrationStatus>(registrationTimer, RegistrationStatus.RegistrationNotStarted
|
|||
|
));
|
|||
|
_IDsregisteredList.Add(userName);
|
|||
|
StartRegistrationTimer();
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Public Methods
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Sends a Sip Invite command to an user
|
|||
|
/// </summary>
|
|||
|
/// <param name="idToInvite">The sip id of the user</param>
|
|||
|
public void Invite(string idToInvite)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
if (idToInvite == null)
|
|||
|
throw new ArgumentNullException("idToInvite");
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (!_IDsentInviteDict.ContainsKey(idToInvite) && !_IDreceivedInviteDict.ContainsKey(idToInvite)
|
|||
|
&& !_IDdialogTuple.ContainsKey(idToInvite))
|
|||
|
{
|
|||
|
SendSipInvite(idToInvite, false);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Sends a Sip Invite command to a Simoco Group
|
|||
|
/// </summary>
|
|||
|
/// <param name="groupIDtoInvite">The sip id of the group</param>
|
|||
|
public void InviteGroup(string groupIDtoInvite)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
if (groupIDtoInvite == null)
|
|||
|
throw new ArgumentNullException("groupIDtoInvite");
|
|||
|
// Check if registered to the group id
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (!_linxGroupSipIDsInDialogWith.Contains(groupIDtoInvite))
|
|||
|
SendSipInvite(groupIDtoInvite, true);
|
|||
|
else
|
|||
|
OnError(new ErrorEventArgs(groupIDtoInvite, UserName, $"Cannot call group {groupIDtoInvite} while I receive voice from it"));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Cancels an Invite sent to an user
|
|||
|
/// </summary>
|
|||
|
/// <param name="idToCancel">The sip id of the user</param>
|
|||
|
public void CancelInvite(string idToCancel)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
Task t = null;
|
|||
|
CancelInvite_private(idToCancel, out t);
|
|||
|
}
|
|||
|
|
|||
|
private void CancelInvite_private(string idToCancel, out Task sendingCancelTask)
|
|||
|
{
|
|||
|
sendingCancelTask = null;
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (_IDsentInviteDict.ContainsKey(idToCancel))
|
|||
|
{
|
|||
|
sendingCancelTask = SendCancelRequest(idToCancel, sendingCancelTask);
|
|||
|
}
|
|||
|
}
|
|||
|
// fire event
|
|||
|
OnInviteSentCanceled(new SipEventArgs(idToCancel));
|
|||
|
}
|
|||
|
|
|||
|
private Task SendCancelRequest(string idToCancel, Task sendingCancelTask)
|
|||
|
{
|
|||
|
// Send Cancel Request
|
|||
|
if (_IDsentInviteDict.ContainsKey(idToCancel))
|
|||
|
{
|
|||
|
|
|||
|
Dialog d = _sipClient.Dialogs.FirstOrDefault((dialog) =>
|
|||
|
{
|
|||
|
return dialog.CallID == _IDsentInviteDict[idToCancel].CallID;
|
|||
|
});
|
|||
|
if (d != null)
|
|||
|
{
|
|||
|
//Send Cancel Request
|
|||
|
sendingCancelTask = Task.Factory.StartNew((dialogObj) =>
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
_sipClient.Cancel((Dialog)dialogObj);
|
|||
|
}
|
|||
|
catch (Independentsoft.Sip.TimeoutException)
|
|||
|
{
|
|||
|
;
|
|||
|
// Don't to anything if timeout exception
|
|||
|
// the receiver did not answer;
|
|||
|
}
|
|||
|
}, d);
|
|||
|
}
|
|||
|
// Remove from dict
|
|||
|
_IDsentInviteDict.Remove(idToCancel);
|
|||
|
_IDsCalledByMeList.Remove(idToCancel);
|
|||
|
}
|
|||
|
return sendingCancelTask;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Accepts an Invite from an user
|
|||
|
/// </summary>
|
|||
|
/// <param name="callingSipID">The sip ID of the user</param>
|
|||
|
public void AcceptInvite(string callingSipID)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
bool fireEvent = false;
|
|||
|
bool success = true;
|
|||
|
bool sendBusy = true;
|
|||
|
string failedMsg = "";
|
|||
|
lock (_lockerCallingMe)
|
|||
|
{
|
|||
|
if (_IDreceivedInviteDict.ContainsKey(callingSipID))
|
|||
|
{
|
|||
|
fireEvent = true;
|
|||
|
Request receivedInvite = _IDreceivedInviteDict[callingSipID];
|
|||
|
// Answer the call
|
|||
|
OK okResponseToInvite = new OK();
|
|||
|
// TO DO - fara asta (adaugare header Contact) nu merge Simoco (Zoiper-ul e ok);
|
|||
|
okResponseToInvite.Header[StandardHeader.Contact] = "<sip:" + _sipClient.Username + "@" + _sipClient.LocalIPEndPoint.ToString() + ";transport=udp>";
|
|||
|
// Generate SDP
|
|||
|
int port = ReturnAvailablePort();
|
|||
|
okResponseToInvite.SessionDescription = CreateSDP(port);
|
|||
|
// Get linxGroupID from received invite (if is a group invite)
|
|||
|
string linxGroupID = null;
|
|||
|
if (receivedInvite.Header.Contains("toGroupSipID"))
|
|||
|
{
|
|||
|
int linxGroupIDint;
|
|||
|
if (int.TryParse(receivedInvite.Header["toGroupSipID"], out linxGroupIDint))
|
|||
|
linxGroupID = linxGroupIDint.ToString();
|
|||
|
}
|
|||
|
// Try to create the dialog
|
|||
|
try
|
|||
|
{
|
|||
|
CreatingDialog(callingSipID, receivedInvite.SessionDescription, port, false, linxGroupID);
|
|||
|
// Trimit Ok
|
|||
|
_sipClient.SendResponse(okResponseToInvite, receivedInvite);
|
|||
|
}
|
|||
|
catch (SipClassException)
|
|||
|
{
|
|||
|
// TO DO - verify this
|
|||
|
|
|||
|
// This is the strange case of "Already in dialog with....."
|
|||
|
// generated by Asterisk sending multiple invite request
|
|||
|
// I should ignore the isssue (not send busy, not fire event)
|
|||
|
sendBusy = false;
|
|||
|
fireEvent = false;
|
|||
|
success = false;
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
// Nu am reusit sa creez dialogul
|
|||
|
ClosingDialog(callingSipID, _sipClient.Username, out linxGroupID);
|
|||
|
failedMsg = ex.Message;
|
|||
|
success = false;
|
|||
|
}
|
|||
|
finally
|
|||
|
{
|
|||
|
// Remove from dictionary
|
|||
|
_IDreceivedInviteDict.Remove(callingSipID);
|
|||
|
_IDsCallingMeList.Remove(callingSipID);
|
|||
|
}
|
|||
|
|
|||
|
if (!success)
|
|||
|
{
|
|||
|
if (sendBusy)
|
|||
|
{
|
|||
|
// Trimit Busy Here
|
|||
|
Task.Factory.StartNew(() => { _sipClient.SendResponseBusyHere(receivedInvite); });
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
// Fire event
|
|||
|
if (fireEvent)
|
|||
|
{
|
|||
|
if (!success)
|
|||
|
OnError(new ErrorEventArgs(_sipClient.Username, callingSipID, failedMsg));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Closes a sip dialog (voice session) with an user
|
|||
|
/// </summary>
|
|||
|
/// <param name="idToClose">The sip id of the user</param>
|
|||
|
public bool Close(string idToClose)
|
|||
|
{
|
|||
|
bool dialogClosed = false;
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
if (idToClose != null)
|
|||
|
{
|
|||
|
Task t = null;
|
|||
|
dialogClosed = Close_private(idToClose, out t, DialogClosedReason.NormalClosing);
|
|||
|
}
|
|||
|
else throw new ArgumentNullException("idToClose");
|
|||
|
return dialogClosed;
|
|||
|
}
|
|||
|
|
|||
|
private bool Close_private(string idToClose, out Task sendingByeTask, DialogClosedReason reason)
|
|||
|
{
|
|||
|
bool fireEvent = false;
|
|||
|
sendingByeTask = null;
|
|||
|
string sipGroupID = null;
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
Dialog dialogToClose = _sipClient.GetDialogWith(idToClose);
|
|||
|
if (dialogToClose != null)
|
|||
|
{
|
|||
|
if (ClosingDialog(idToClose, _sipClient.Username, out sipGroupID))
|
|||
|
{
|
|||
|
fireEvent = true;
|
|||
|
sendingByeTask = Task.Factory.StartNew((dialogToCloseObj) =>
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
_sipClient.Bye((Dialog)dialogToCloseObj);
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
// To do
|
|||
|
// For now, do nothing
|
|||
|
;
|
|||
|
}
|
|||
|
}, dialogToClose);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
// FireEvent
|
|||
|
if (fireEvent)
|
|||
|
OnDialogClosed(new LinxDialogClosedEventArgs(
|
|||
|
idToClose, _sipClient.Username, reason, sipGroupID));
|
|||
|
return fireEvent;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Declines a received Invite from an user
|
|||
|
/// </summary>
|
|||
|
/// <param name="idToDeclineCall">The sip ID of the user</param>
|
|||
|
public void Decline(string idToDeclineCall)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
if (idToDeclineCall != null)
|
|||
|
{
|
|||
|
Task t = null;
|
|||
|
Decline_private(idToDeclineCall, out t);
|
|||
|
}
|
|||
|
else
|
|||
|
throw new ArgumentNullException("idToDeclineCall");
|
|||
|
}
|
|||
|
|
|||
|
private void Decline_private(string idToDeclineCall, out Task sendBusyTask)
|
|||
|
{
|
|||
|
bool fireEvent = false;
|
|||
|
sendBusyTask = null;
|
|||
|
lock (_lockerCallingMe)
|
|||
|
{
|
|||
|
if (_IDreceivedInviteDict.ContainsKey(idToDeclineCall))
|
|||
|
{
|
|||
|
fireEvent = true;
|
|||
|
Request inviteReq = _IDreceivedInviteDict[idToDeclineCall];
|
|||
|
// Remove from dictionary
|
|||
|
_IDreceivedInviteDict.Remove(idToDeclineCall);
|
|||
|
_IDsCallingMeList.Remove(idToDeclineCall);
|
|||
|
// Send busy here
|
|||
|
sendBusyTask = Task.Factory.StartNew(() =>
|
|||
|
{
|
|||
|
_sipClient.SendResponseBusyHere(inviteReq);
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
// Fire event
|
|||
|
if (fireEvent)
|
|||
|
OnInviteReceivedDeclined(new SipEventArgs(idToDeclineCall));
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Sends a voice buffer in a specified format to the user in dialog with
|
|||
|
/// </summary>
|
|||
|
/// <param name="idToSendVoice">The sip ID of the user</param>
|
|||
|
/// <param name="audioBuffer">The audio buffer</param>
|
|||
|
/// <param name="bufferLength">The length of the buffer</param>
|
|||
|
/// <param name="format">The audio format of the buffer</param>
|
|||
|
public virtual void SendAudio(string idToSendVoice, byte[] audioBuffer, int bufferLength, AudioFormat format)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (_IDdialogTuple.ContainsKey(idToSendVoice))
|
|||
|
{
|
|||
|
_IDdialogTuple[idToSendVoice].Item2.SendAudio(audioBuffer, bufferLength, format);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Method used to send Gps Request to a Linx device
|
|||
|
/// </summary>
|
|||
|
/// <param name="idToRequestGps">The sip id to send the gps request</param>
|
|||
|
/// <param name="seqID">The sequence id</param>
|
|||
|
protected void SendGpsRequest(string idToRequestGps, string seqID)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
if (idToRequestGps != null)
|
|||
|
{
|
|||
|
// Send Gps request for Linx
|
|||
|
// [#msgLen]#seqID#154#
|
|||
|
string cmdText = string.Format("#{0}#154#", seqID);
|
|||
|
string cmd = AddMsgLenForMBus(cmdText);
|
|||
|
Message pollRequestForLinx = GenerateSipMessage(idToRequestGps, UserName, _sipClient.Domain, _sipDomainPort, cmd);
|
|||
|
pollRequestForLinx.Header.Add("Ais-Service", "mbus");
|
|||
|
Task.Factory.StartNew(() =>
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
_sipClient.SendRequest(pollRequestForLinx);
|
|||
|
}
|
|||
|
catch (Exception)
|
|||
|
{
|
|||
|
; // Probably timeout exception, do not do anything
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
else
|
|||
|
throw new ArgumentNullException("idToRequestGps");
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Method used to acknowledge an emergency alarm sent by a Linx device
|
|||
|
/// </summary>
|
|||
|
/// <param name="linxID">The sip id of the Linx device</param>
|
|||
|
protected void AcknowledgeLinxEmergencyAlarm(string linxID)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
if (linxID != null)
|
|||
|
{
|
|||
|
// Send [#msgLen]#seqID#238#
|
|||
|
string textToSend = string.Format("#{0}#238#", _rand.Next().ToString());
|
|||
|
string cmdToSend = AddMsgLenForMBus(textToSend);
|
|||
|
Message sipMessage = GenerateSipMessage(linxID, UserName, _sipClient.Domain, _sipDomainPort, cmdToSend);
|
|||
|
Task.Factory.StartNew(() =>
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
_sipClient.SendRequest(sipMessage);
|
|||
|
}
|
|||
|
catch (Exception)
|
|||
|
{
|
|||
|
// Do nothing
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
else
|
|||
|
throw new ArgumentNullException("linxID");
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Method used to send an sms to sip id using the sip protocol
|
|||
|
/// <para>This method does not block the calling thread while waiting for the confirmation</para>
|
|||
|
/// </summary>
|
|||
|
/// <param name="idToSendSMS">The sip id where to send the sms</param>
|
|||
|
/// <param name="text">The sms text</param>
|
|||
|
/// <returns>True if the sms was received, else returns false</returns>
|
|||
|
public async Task<bool> SendSmsAsync(string idToSendSMS, string text)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
if (idToSendSMS != null && text != null)
|
|||
|
{
|
|||
|
bool sendSipMessage = true;
|
|||
|
string unconfirmedSmsKey = null;
|
|||
|
if (_smsConfirmationFromServer)
|
|||
|
{
|
|||
|
unconfirmedSmsKey = idToSendSMS + text.GetHashCode();
|
|||
|
// Add smsKey to unconfirmed sms set
|
|||
|
lock (_lockerSmsSet)
|
|||
|
{
|
|||
|
if (!_smsSentDict.ContainsKey(unconfirmedSmsKey))
|
|||
|
{
|
|||
|
_smsSentDict.Add(unconfirmedSmsKey, null);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Already the same msg to the same id in the dict
|
|||
|
// Will not send sip message, just wait for the later to be confirmed
|
|||
|
sendSipMessage = false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
if (sendSipMessage)
|
|||
|
{
|
|||
|
// Create a new thread on which:
|
|||
|
// 1. Send sip message
|
|||
|
// 2. Wait for confirmation that the mess reach the server
|
|||
|
// 3. Wait for confirmation that the mess reach the destination id
|
|||
|
return await Task.Factory.StartNew(() =>
|
|||
|
{
|
|||
|
// Send Sms using sip library
|
|||
|
try
|
|||
|
{
|
|||
|
//RequestResponse rr = _sipClient.SendMessage(
|
|||
|
// string.Format("sip:{0}@{1}", _sipClient.Username, _sipClient.Domain),
|
|||
|
// string.Format("sip:{0}@{1}", idToSendSMS, _sipClient.Domain),
|
|||
|
// text);
|
|||
|
Message sipMsg = GenerateSipMessage(idToSendSMS, _sipClient.Username, _sipClient.Domain,
|
|||
|
_sipDomainPort, text);
|
|||
|
RequestResponse rr = _sipClient.SendRequest(sipMsg);
|
|||
|
// Message got to the sip server
|
|||
|
if (rr.Response.Description == "OK" || rr.Response.Description == "Accepted")
|
|||
|
{
|
|||
|
if (unconfirmedSmsKey != null)
|
|||
|
{
|
|||
|
if (SmsConfirmedBySipServer(unconfirmedSmsKey))
|
|||
|
return true;
|
|||
|
else
|
|||
|
return false;
|
|||
|
}
|
|||
|
else
|
|||
|
return true;
|
|||
|
}
|
|||
|
else // Sip server did not accept the message
|
|||
|
return false;
|
|||
|
}
|
|||
|
catch (Exception)
|
|||
|
{
|
|||
|
// Probably timeout exception, msg didn't reach the sip server
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
});
|
|||
|
}
|
|||
|
else
|
|||
|
return await Task.Factory.StartNew<bool>(SmsConfirmedBySipServer, unconfirmedSmsKey);
|
|||
|
|
|||
|
}
|
|||
|
else if (idToSendSMS == null)
|
|||
|
throw new ArgumentNullException("idToSendSMS");
|
|||
|
else
|
|||
|
throw new ArgumentNullException("text");
|
|||
|
}
|
|||
|
|
|||
|
private Message GenerateSipMessage(string destinationID, string senderID, string sipServer,
|
|||
|
int sipServerPort, string text)
|
|||
|
{
|
|||
|
Message sipMessage = new Message();
|
|||
|
sipMessage.Uri = "sip:" + destinationID + "@" + sipServer + ":" + sipServerPort;
|
|||
|
sipMessage.From = new ContactInfo(string.Format("sip:{0}@{1}", senderID, sipServer));
|
|||
|
sipMessage.To = new ContactInfo(string.Format("sip:{0}@{1}", destinationID, sipServer));
|
|||
|
sipMessage.ContentType = "text/plain;charset=UTF-8";
|
|||
|
sipMessage.Body = text;
|
|||
|
|
|||
|
return sipMessage;
|
|||
|
}
|
|||
|
|
|||
|
private bool SmsConfirmedBySipServer(object unconfirmedSmsKey)
|
|||
|
{
|
|||
|
bool smsConfirmed = true;
|
|||
|
bool timeoutNotExpired = true;
|
|||
|
string smsKey = (string)unconfirmedSmsKey;
|
|||
|
lock (_lockerSmsSet)
|
|||
|
{
|
|||
|
while (_smsSentDict.ContainsKey(smsKey) && !_smsSentDict[smsKey].HasValue)
|
|||
|
{
|
|||
|
smsConfirmed = false;
|
|||
|
timeoutNotExpired = System.Threading.Monitor.Wait(_lockerSmsSet, SecondsToWaitForSmsConfirmation * 1000);
|
|||
|
if (!timeoutNotExpired)
|
|||
|
{
|
|||
|
// timer expired, no confiramtion received
|
|||
|
// delete the message from set
|
|||
|
_smsSentDict.Remove(smsKey);
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
if (timeoutNotExpired) // check confirmation status
|
|||
|
{
|
|||
|
lock (_lockerSmsSet)
|
|||
|
{
|
|||
|
if (_smsSentDict.ContainsKey(smsKey) && _smsSentDict[smsKey].HasValue)
|
|||
|
{
|
|||
|
// Check confirmation status
|
|||
|
smsConfirmed = _smsSentDict[smsKey].Value;
|
|||
|
// Remove from dictionary
|
|||
|
_smsSentDict.Remove(smsKey);
|
|||
|
}
|
|||
|
else
|
|||
|
throw new ApplicationException("Error on working with unconfirmed sms dict");
|
|||
|
}
|
|||
|
}
|
|||
|
return smsConfirmed;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Mehod used to check if you are in dialog with a sip id
|
|||
|
/// </summary>
|
|||
|
/// <param name="sipID">The sip id</param>
|
|||
|
/// <returns>True if you are in dialog, else returns false</returns>
|
|||
|
public bool InDialogWith(string sipID)
|
|||
|
{
|
|||
|
if (_sipClassClosed)
|
|||
|
throw new ObjectDisposedException("SipClientClass");
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
return _IDdialogTuple.ContainsKey(sipID);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Stops registration by sending unregister request to the sip server
|
|||
|
/// <para>Releases all the used resources</para>
|
|||
|
/// <param name="async">If true, method returns before all the resources are released</param>
|
|||
|
/// </summary>
|
|||
|
public virtual void Stop(bool async = true)
|
|||
|
{
|
|||
|
if (!_sipClassClosed)
|
|||
|
{
|
|||
|
_sipClassClosed = true;
|
|||
|
List<string> sipIDs = new List<string>();
|
|||
|
Task task = null;
|
|||
|
Task t = Task.Factory.StartNew(() =>
|
|||
|
{
|
|||
|
// 1. Reject all calls not answered yet
|
|||
|
sipIDs.Clear();
|
|||
|
lock (_lockerCallingMe)
|
|||
|
{
|
|||
|
foreach (string callingID in _IDreceivedInviteDict.Keys)
|
|||
|
sipIDs.Add(callingID);
|
|||
|
}
|
|||
|
foreach (string sipID in sipIDs)
|
|||
|
{
|
|||
|
Decline_private(sipID, out task);
|
|||
|
if (task != null)
|
|||
|
task.Wait();
|
|||
|
}
|
|||
|
|
|||
|
// 2. Cancel all sent calls
|
|||
|
sipIDs.Clear();
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
foreach (string calledSipID in _IDsentInviteDict.Keys)
|
|||
|
sipIDs.Add(calledSipID);
|
|||
|
}
|
|||
|
foreach (string sipID in sipIDs)
|
|||
|
{
|
|||
|
CancelInvite_private(sipID, out task);
|
|||
|
if (task != null)
|
|||
|
task.Wait();
|
|||
|
}
|
|||
|
|
|||
|
// 3. Close all curent dialogs
|
|||
|
sipIDs.Clear();
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
foreach (string idInDialog in _IDdialogTuple.Keys)
|
|||
|
sipIDs.Add(idInDialog);
|
|||
|
}
|
|||
|
foreach (string sipID in sipIDs)
|
|||
|
{
|
|||
|
Close_private(sipID, out task, DialogClosedReason.NormalClosing);
|
|||
|
if (task != null)
|
|||
|
task.Wait();
|
|||
|
}
|
|||
|
|
|||
|
// Stop registering to sip server
|
|||
|
StopRegisterToSipServer();
|
|||
|
// Unregister events
|
|||
|
_sipClient.ReceiveRequest -= ProcessSipRequest;
|
|||
|
_sipClient.ReceiveResponse -= ProcessSipResponse;
|
|||
|
// Stop sip class
|
|||
|
_sipClient.Disconnect();
|
|||
|
});
|
|||
|
if (!async)
|
|||
|
t.Wait();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Stops registration by sending unregister request to the sip server
|
|||
|
/// </summary>
|
|||
|
public void StopRegisterToSipServer()
|
|||
|
{
|
|||
|
// Stop registering to groups
|
|||
|
List<string> idsToUnregisterFrom = new List<string>(_IDsregisteredList);
|
|||
|
RegistrationData zeroRegistrationData = null;
|
|||
|
foreach (string id in idsToUnregisterFrom)
|
|||
|
{
|
|||
|
if (id != _sipClient.Username)
|
|||
|
zeroRegistrationData = new RegistrationData(id, 0, true);
|
|||
|
else
|
|||
|
zeroRegistrationData = new RegistrationData(id, 0);
|
|||
|
_sipID_regTimer_regStatus_Dict[id].Item1.Stop();
|
|||
|
SendSipRegister(zeroRegistrationData);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region RTP Listener Event Handlers
|
|||
|
|
|||
|
private void ReceivedVoice(object sender, AudioEventArgs e)
|
|||
|
{
|
|||
|
OnVoiceReceived((LinxAudioEventArgs)e);
|
|||
|
}
|
|||
|
|
|||
|
void ExceptionThrown(object sender, EventArgs e)
|
|||
|
{
|
|||
|
// Close the Sip Session with Bye
|
|||
|
RTPListener2 rtpListener = (RTPListener2)sender;
|
|||
|
Task t = null;
|
|||
|
Close_private(rtpListener.SipIDinDialogWith.ToString(), out t, DialogClosedReason.Error);
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Private Sip Functions
|
|||
|
|
|||
|
private void ProcessSipResponse(object sender, ResponseEventArgs e)
|
|||
|
{
|
|||
|
Response resp = e.Response;
|
|||
|
string responserRadioID = resp.To.Address.Split(':', '@')[1];
|
|||
|
switch (resp.StatusCode)
|
|||
|
{
|
|||
|
case 200: // OK response
|
|||
|
if (resp.CSeq.Contains("INVITE")) // OK response for an Invite
|
|||
|
{
|
|||
|
bool success = false;
|
|||
|
string errorMsg = "";
|
|||
|
string linxGroupID = null;
|
|||
|
// Gasesc Invite-ul pentru care am primit OK
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (_IDsentInviteDict.ContainsKey(responserRadioID))
|
|||
|
{
|
|||
|
Invite sentInvite = _IDsentInviteDict[responserRadioID];
|
|||
|
// Get sipGroupID from the sent invite (if is a group invite)
|
|||
|
if (sentInvite.Header.Contains("toGroupSipID"))
|
|||
|
{
|
|||
|
int linxGroupIDint;
|
|||
|
if (int.TryParse(sentInvite.Header["toGroupSipID"], out linxGroupIDint))
|
|||
|
linxGroupID = linxGroupIDint.ToString();
|
|||
|
}
|
|||
|
try
|
|||
|
{
|
|||
|
// Send sip Ack
|
|||
|
_sipClient.Ack(resp);
|
|||
|
// Creez Dialogul
|
|||
|
CreatingDialog(responserRadioID,
|
|||
|
resp.SessionDescription,
|
|||
|
sentInvite.SessionDescription.Media[0].Port,
|
|||
|
true, linxGroupID);
|
|||
|
success = true;
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
// Nu am reusit sa setez dialogul
|
|||
|
errorMsg = ex.Message;
|
|||
|
// Fac DialogClosed();
|
|||
|
if (ClosingDialog(responserRadioID, _sipClient.Username, out linxGroupID))
|
|||
|
{
|
|||
|
// Trimit Bye
|
|||
|
Task.Factory.StartNew((respObj) =>
|
|||
|
{
|
|||
|
_sipClient.Bye((Response)respObj);
|
|||
|
}, resp);
|
|||
|
}
|
|||
|
}
|
|||
|
finally
|
|||
|
{
|
|||
|
// Remove from sent invites dictionary
|
|||
|
_IDsentInviteDict.Remove(responserRadioID);
|
|||
|
_IDsCalledByMeList.Remove(responserRadioID);
|
|||
|
}
|
|||
|
}
|
|||
|
else // OK primit pentru un Invite anulat...
|
|||
|
{
|
|||
|
errorMsg = "Received OK for a cancelled Invite";
|
|||
|
// Trimit Bye sau Cancel???
|
|||
|
// Din standard se pare ca Bye
|
|||
|
//Task.Factory.StartNew((respObj) =>
|
|||
|
//{
|
|||
|
// _sipClient.Bye((Response)respObj);
|
|||
|
//}, resp);
|
|||
|
|
|||
|
// TO DO - to verify this
|
|||
|
// That weird situation with Asterisk sending multiple requests / response
|
|||
|
// Don't do anything, just return
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (success)
|
|||
|
OnDialogCreated(new LinxDialogCreatedEventArgs(responserRadioID, _sipClient.Username, TypeOfCall.FULL_DUPLEX, linxGroupID != null, linxGroupID));
|
|||
|
else
|
|||
|
OnError(new ErrorEventArgs(responserRadioID, _sipClient.Username, errorMsg));
|
|||
|
}
|
|||
|
break;
|
|||
|
case 100: // Trying
|
|||
|
break;
|
|||
|
case 180: // Ringing
|
|||
|
break;
|
|||
|
case 401: // Unauthorized (temporarely)
|
|||
|
break;
|
|||
|
case 202: // Accepted
|
|||
|
break;
|
|||
|
default: // Probabil eroare
|
|||
|
// trimit Ack
|
|||
|
// I don't send ack because the sip.NET library does not send it to the correct udp port
|
|||
|
// This happens only with Asterisk, for Linx project
|
|||
|
// It use to work for simoco
|
|||
|
//_sipClient.Ack(resp);
|
|||
|
bool fireEvent = false;
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (_IDsentInviteDict.ContainsKey(responserRadioID))
|
|||
|
{
|
|||
|
_IDsentInviteDict.Remove(responserRadioID);
|
|||
|
_IDsCalledByMeList.Remove(responserRadioID);
|
|||
|
fireEvent = true;
|
|||
|
}
|
|||
|
}
|
|||
|
if (fireEvent)
|
|||
|
OnError(new ErrorEventArgs(responserRadioID, _sipClient.Username, resp.Description));
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void ProcessSipRequest(object sender, RequestEventArgs e)
|
|||
|
{
|
|||
|
Request req = e.Request;
|
|||
|
string requesterRadioId = req.From.Address.Split(':', '@')[1];
|
|||
|
string requesterTargetId = req.To.Address.Split(':', '@')[1];
|
|||
|
switch (req.Method)
|
|||
|
{
|
|||
|
case SipMethod.Invite:
|
|||
|
bool fireEvent = false;
|
|||
|
string idInDialogWith = requesterRadioId;
|
|||
|
bool isEmergencyCall = false;
|
|||
|
lock (_lockerCallingMe)
|
|||
|
{
|
|||
|
if (!_IDreceivedInviteDict.ContainsKey(requesterRadioId))
|
|||
|
{
|
|||
|
// Check if is emergency call (Simoco)
|
|||
|
//if (req.Header.Contains(StandardHeader.Priority))
|
|||
|
// if (req.Header[StandardHeader.Priority] == "emergency")
|
|||
|
// isEmergencyCall = true;
|
|||
|
|
|||
|
if (SendResponseRinging(req, requesterRadioId))
|
|||
|
{
|
|||
|
// Sent response Ringing
|
|||
|
_sipClient.SendResponseRinging(req);
|
|||
|
// Add to dictionary
|
|||
|
_IDreceivedInviteDict.Add(idInDialogWith, req);
|
|||
|
_IDsCallingMeList.Add(idInDialogWith);
|
|||
|
// fire event
|
|||
|
fireEvent = true;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_sipClient.SendResponseBusyHere(req);
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Invite primit de doua ori de la acelasi Id, putin probabil
|
|||
|
// Trimit Busy
|
|||
|
_sipClient.SendResponseBusyHere(req);
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
if (fireEvent)
|
|||
|
{
|
|||
|
OnInviteReceived(new InviteReceivedArgs(idInDialogWith, requesterRadioId, TypeOfCall.FULL_DUPLEX, isEmergencyCall));
|
|||
|
}
|
|||
|
break;
|
|||
|
case SipMethod.Bye:
|
|||
|
// Accept the request
|
|||
|
_sipClient.AcceptRequest(req);
|
|||
|
|
|||
|
string idToClose = requesterRadioId;
|
|||
|
string linxGroupID = null;
|
|||
|
if (ClosingDialog(idToClose, requesterRadioId, out linxGroupID))
|
|||
|
{
|
|||
|
// FireEvent;
|
|||
|
OnDialogClosed(new LinxDialogClosedEventArgs(
|
|||
|
idToClose, requesterRadioId, DialogClosedReason.NormalClosing, linxGroupID));
|
|||
|
}
|
|||
|
break;
|
|||
|
case SipMethod.Cancel:
|
|||
|
// Accept the request
|
|||
|
_sipClient.AcceptRequest(req);
|
|||
|
fireEvent = false;
|
|||
|
// Check if Bye request is for an open dialog
|
|||
|
|
|||
|
lock (_lockerCallingMe)
|
|||
|
{
|
|||
|
if (_IDreceivedInviteDict.ContainsKey(requesterRadioId))
|
|||
|
{
|
|||
|
// remove from dictionary
|
|||
|
_IDreceivedInviteDict.Remove(requesterRadioId);
|
|||
|
_IDsCallingMeList.Remove(requesterRadioId);
|
|||
|
// Fire event
|
|||
|
fireEvent = true;
|
|||
|
}
|
|||
|
}
|
|||
|
if (fireEvent)
|
|||
|
OnInviteReceivedCanceled(new SipEventArgs(requesterRadioId));
|
|||
|
break;
|
|||
|
case SipMethod.Ack:
|
|||
|
if (req.Header.Contains(StandardHeader.Contact))
|
|||
|
{
|
|||
|
// We received an Invite, sent Ok and now we receive ack
|
|||
|
idInDialogWith = requesterRadioId;
|
|||
|
// Dialog confirmed
|
|||
|
if (_IDdialogTuple.ContainsKey(idInDialogWith))
|
|||
|
{
|
|||
|
linxGroupID = ((RTPListenerLinx)_IDdialogTuple[idInDialogWith].Item3).LinxGroupID;
|
|||
|
|
|||
|
OnDialogCreated(new LinxDialogCreatedEventArgs(
|
|||
|
requesterTargetId,
|
|||
|
requesterRadioId,
|
|||
|
_IDdialogTuple[idInDialogWith].Item2.TypeOfCall,
|
|||
|
linxGroupID != null,
|
|||
|
linxGroupID));
|
|||
|
}
|
|||
|
}
|
|||
|
break;
|
|||
|
case SipMethod.Message:
|
|||
|
// Accept the request
|
|||
|
_sipClient.AcceptRequest(req);
|
|||
|
ProcessReceivedSipMessage(req, requesterRadioId);
|
|||
|
break;
|
|||
|
case SipMethod.Info:
|
|||
|
case SipMethod.Notify:
|
|||
|
case SipMethod.Options:
|
|||
|
case SipMethod.Prack:
|
|||
|
case SipMethod.Publish:
|
|||
|
case SipMethod.Refer:
|
|||
|
case SipMethod.Register:
|
|||
|
case SipMethod.Subscribe:
|
|||
|
case SipMethod.Update:
|
|||
|
default:
|
|||
|
// Accept request
|
|||
|
_sipClient.AcceptRequest(req);
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Method used to decide if the app is ringing or is sending Busy response to sender
|
|||
|
/// <para>Default behaviour is to ring (sends Ringing response)</para>
|
|||
|
/// </summary>
|
|||
|
/// <param name="receivedInvite">The received invite</param>
|
|||
|
/// <param name="senderSipId">The sender of the invite</param>
|
|||
|
/// <returns>True to send Ringing response, false to send Busy Here</returns>
|
|||
|
protected virtual bool SendResponseRinging(Request receivedInvite, string senderSipId)
|
|||
|
{
|
|||
|
bool sendRinging = true;
|
|||
|
// Check that the invite is proper formatted
|
|||
|
if (receivedInvite.Header.ContainsKey("isPTT") && receivedInvite.Header.ContainsKey("toGroupSipID"))
|
|||
|
{
|
|||
|
if (receivedInvite.Header["isPTT"] == "false")
|
|||
|
{
|
|||
|
sendRinging = false;
|
|||
|
OnError(new ErrorEventArgs(UserName, senderSipId, $"Will reject invite from {senderSipId} because it is a full duplex call"));
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Call is PTT
|
|||
|
// Check if is group call
|
|||
|
string linxGroupID = null;
|
|||
|
if (string.IsNullOrEmpty(linxGroupID = receivedInvite.Header["toGroupSipID"]))
|
|||
|
{
|
|||
|
// PTT group call
|
|||
|
// Check if I am allready in group call
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (_linxGroupSipIDsInDialogWith.Contains(linxGroupID))
|
|||
|
{
|
|||
|
sendRinging = false;
|
|||
|
OnError(new ErrorEventArgs(UserName, senderSipId, $"Will reject group invite from {senderSipId} because I'm already in group call with {linxGroupID}"));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
sendRinging = false;
|
|||
|
OnError(new ErrorEventArgs(UserName, senderSipId, $"Will reject invite from {senderSipId} because it is not propely formated"));
|
|||
|
}
|
|||
|
return sendRinging;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Function that processes all the received Sip Message requests
|
|||
|
/// </summary>
|
|||
|
/// <param name="sipMessageRequest">The Sip Message request</param>
|
|||
|
/// <param name="senderID">The ID of the sender</param>
|
|||
|
protected virtual void ProcessReceivedSipMessage(Request sipMessageRequest, string senderID)
|
|||
|
{
|
|||
|
// Handle messages when working with Asterisk server (set up by Andrei)
|
|||
|
if (sipMessageRequest.ContentType == "text/plain;charset=UTF-8")
|
|||
|
{
|
|||
|
// SMS received
|
|||
|
if (senderID == "Unknown")
|
|||
|
{
|
|||
|
// SMS confirmation from Sip server
|
|||
|
if (_smsConfirmationFromServer)
|
|||
|
{
|
|||
|
// Get sms destination and message
|
|||
|
string smsKey = GetSmsKeyFromConfirmation(sipMessageRequest.Body);
|
|||
|
lock (_lockerSmsSet)
|
|||
|
{
|
|||
|
if (_smsSentDict.ContainsKey(smsKey))
|
|||
|
{
|
|||
|
// Set message as delivered or failed
|
|||
|
_smsSentDict[smsKey] = GetDeliveredStatusFromConfirmation(sipMessageRequest.Body);
|
|||
|
// Notify that we received a confirmation
|
|||
|
Monitor.PulseAll(_lockerSmsSet);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
else if (sipMessageRequest.Header.Contains("Ais-Service"))
|
|||
|
{
|
|||
|
if (sipMessageRequest.Header["Ais-Service"] == "mbus")
|
|||
|
{
|
|||
|
string messageBody = sipMessageRequest.Body;
|
|||
|
if (messageBody != null)
|
|||
|
{
|
|||
|
string[] tmp = messageBody.Split('#');
|
|||
|
if (tmp.Length >= 4 && messageBody.Length == Convert.ToInt32(tmp[1]))
|
|||
|
{
|
|||
|
switch (tmp[3])
|
|||
|
{
|
|||
|
case "138":
|
|||
|
// Emergency from Linx
|
|||
|
if (_previousEmergencySeqID != tmp[2]) // Protection against multiple message error
|
|||
|
{
|
|||
|
_previousEmergencySeqID = tmp[2];
|
|||
|
Linx.LinxEmergencyType type = Linx.LinxEmergencyType.REGULAR;
|
|||
|
if (tmp[5] != null)
|
|||
|
{
|
|||
|
int emergencyType;
|
|||
|
if (int.TryParse(tmp[5], out emergencyType))
|
|||
|
{
|
|||
|
if (Enum.IsDefined(typeof(Linx.LinxEmergencyType), emergencyType))
|
|||
|
{
|
|||
|
type = (Linx.LinxEmergencyType)emergencyType;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
OnLinxEmergencyAlarmReceived(new Linx.LinxEmergencyAlarmReceivedEventArgs(senderID, tmp[2], type));
|
|||
|
}
|
|||
|
break;
|
|||
|
case "131": // Periadically gps from Linx
|
|||
|
case "231": // Polled gps from Linx
|
|||
|
int unixTime;
|
|||
|
double speed, latitude, longitude;
|
|||
|
if (tmp.Length >= 9)
|
|||
|
{
|
|||
|
// Protection against multiple message error
|
|||
|
if (_previousLocationSeqID != tmp[2])
|
|||
|
_previousLocationSeqID = tmp[2];
|
|||
|
else
|
|||
|
break;
|
|||
|
if (double.TryParse(tmp[6], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out speed))
|
|||
|
{
|
|||
|
if (int.TryParse(tmp[5], out unixTime))
|
|||
|
{
|
|||
|
if (double.TryParse(tmp[7], NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingWhite | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out latitude))
|
|||
|
{
|
|||
|
if (double.TryParse(tmp[8], NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingWhite | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out longitude))
|
|||
|
{
|
|||
|
if (tmp[3] == "131")
|
|||
|
{
|
|||
|
// Periodically gps from Linx
|
|||
|
OnGpsPeriodicallyReportReceived(new GpsDataEventArgs(
|
|||
|
senderID, latitude, longitude, speed, UnixTimeStampToDateTime(unixTime)));
|
|||
|
}
|
|||
|
else if (tmp[3] == "231")
|
|||
|
{
|
|||
|
OnGpsReportReceived(new LinxGpsDataEventArgs(
|
|||
|
senderID, tmp[2], latitude, longitude, speed, UnixTimeStampToDateTime(unixTime)));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
break;
|
|||
|
case "130": // Ars from Linx
|
|||
|
if (tmp.Length >= 6)
|
|||
|
{
|
|||
|
if (_previousArsSeqID != tmp[2]) // Protection against multiple message error
|
|||
|
{
|
|||
|
_previousArsSeqID = tmp[2];
|
|||
|
OnLinxArsReceived(new Linx.ArsReceivedEventArgs(senderID, tmp[2], tmp[5] == "ON" ? true : false));
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Must be sms (:::seqID:::text:::)
|
|||
|
string messageBody = sipMessageRequest.Body;
|
|||
|
if (messageBody != null)
|
|||
|
{
|
|||
|
string[] tmp = messageBody.Split(new string[] { ":::" }, StringSplitOptions.None);
|
|||
|
if (tmp.Length == 4)
|
|||
|
{
|
|||
|
if (_previousSmsSeqID != tmp[1])
|
|||
|
{
|
|||
|
_previousSmsSeqID = tmp[1];
|
|||
|
OnSipSmsReceived(new SmsReceivedEventsArgs(senderID, tmp[2]));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#region Sms confirmation private helper functions
|
|||
|
|
|||
|
private string GetSmsKeyFromConfirmation(string messageBody)
|
|||
|
{
|
|||
|
string toReturn = "";
|
|||
|
int destinationSipID;
|
|||
|
string text;
|
|||
|
if (int.TryParse(GetDestinationIDFromConfirmation(messageBody), out destinationSipID))
|
|||
|
{
|
|||
|
text = GetTextFromConfirmation(messageBody);
|
|||
|
toReturn = destinationSipID.ToString() + text.GetHashCode();
|
|||
|
}
|
|||
|
return toReturn;
|
|||
|
}
|
|||
|
|
|||
|
private string GetTextFromConfirmation(string source)
|
|||
|
{
|
|||
|
string start = "Your message - ";
|
|||
|
string end = " - to ";
|
|||
|
int startIndex, endIndex;
|
|||
|
string strToReturn = "";
|
|||
|
if (source.Contains(start) && source.Contains(end))
|
|||
|
{
|
|||
|
startIndex = source.IndexOf(start) + start.Length;
|
|||
|
endIndex = source.LastIndexOf(end);
|
|||
|
strToReturn = source.Substring(startIndex, endIndex - startIndex);
|
|||
|
}
|
|||
|
return strToReturn;
|
|||
|
}
|
|||
|
|
|||
|
private string GetDestinationIDFromConfirmation(string source)
|
|||
|
{
|
|||
|
string start = " - to ";
|
|||
|
string end = " has ";
|
|||
|
int startIndex, endIndex;
|
|||
|
string strToReturn = "";
|
|||
|
if (source.Contains(start) && source.Contains(end))
|
|||
|
{
|
|||
|
startIndex = source.LastIndexOf(start) + start.Length;
|
|||
|
endIndex = source.LastIndexOf(end);
|
|||
|
strToReturn = source.Substring(startIndex, endIndex - startIndex);
|
|||
|
}
|
|||
|
return strToReturn;
|
|||
|
}
|
|||
|
|
|||
|
private bool GetDeliveredStatusFromConfirmation(string source)
|
|||
|
{
|
|||
|
string lastWord = source.Substring(source.LastIndexOf(' ') + 1);
|
|||
|
if (lastWord.Contains("delivered"))
|
|||
|
return true;
|
|||
|
else if (lastWord.Contains("failed"))
|
|||
|
return false;
|
|||
|
else
|
|||
|
throw new ApplicationException("Error on parsing the sms confirmation from sip server");
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
private void CreatingDialog(string radioInDialogWith, SessionDescription receivedSDP, int localRTPport, bool initiatedByMe, string linxGroupID = null)
|
|||
|
{
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (_IDdialogTuple.Count == MaxNbOfDialogs)
|
|||
|
throw new ApplicationException("Exceeded the maximum number of simultaneous dialogs: " + MaxNbOfDialogs);
|
|||
|
// Verific daca sunt deja in dialog cu userul
|
|||
|
if (!_IDdialogTuple.ContainsKey(radioInDialogWith))
|
|||
|
{
|
|||
|
// Extract ip and port where to send voice to simoco
|
|||
|
IPAddress ipToSendAudio = null;
|
|||
|
int portToSendAudio;
|
|||
|
if (IPAddress.TryParse(receivedSDP.Connection.Address, out ipToSendAudio))
|
|||
|
{
|
|||
|
portToSendAudio = receivedSDP.Media[0].Port;
|
|||
|
}
|
|||
|
else throw new ApplicationException("Canot determine ip where to send audio");
|
|||
|
|
|||
|
// Creez clientul de UDP conectat la portul pe care voi primi voce
|
|||
|
UdpClient udpClient = new UdpClient(localRTPport);
|
|||
|
//UdpClient udpClient = new UdpClient(new IPEndPoint(IPAddress.Parse(_localIPaddress), localRTPport));
|
|||
|
|
|||
|
// Create RTPSender (trebuie sa fie gata sa trimita mesaje catre simoco)
|
|||
|
RTPSender2 rtpSender = new RTPSender2(
|
|||
|
udpClient,
|
|||
|
AudioBitrate,
|
|||
|
new IPEndPoint(ipToSendAudio, portToSendAudio),
|
|||
|
int.Parse(_sipClient.Username),
|
|||
|
int.Parse(radioInDialogWith),
|
|||
|
initiatedByMe,
|
|||
|
TypeOfCall.FULL_DUPLEX);
|
|||
|
|
|||
|
//
|
|||
|
//_pttControlBlockingCollection = new BlockingCollection<PTTEventArgs>();
|
|||
|
//Task.Factory.StartNew(() =>
|
|||
|
//{
|
|||
|
// ProcessPTTReceived();
|
|||
|
//});
|
|||
|
|
|||
|
|
|||
|
// Incep sa ascult RTP trimis de Simoco
|
|||
|
RTPListener2 rtpListener = new RTPListenerLinx(udpClient, initiatedByMe, _bufferMiliseconds, int.Parse(radioInDialogWith), TypeOfCall.FULL_DUPLEX, linxGroupID);
|
|||
|
//_rtpListener.PTTControlReceived += PTTControlReceived;
|
|||
|
rtpListener.VoiceReceived += ReceivedVoice;
|
|||
|
rtpListener.ExceptionThrown += ExceptionThrown;
|
|||
|
rtpListener.Start();
|
|||
|
|
|||
|
// Timer hangtime
|
|||
|
TimerWithTag<string> timerHangTime = null;
|
|||
|
if (HangTimeDuration != -1)
|
|||
|
{
|
|||
|
timerHangTime = new TimerWithTag<string>(HangTimeDuration * 1000, radioInDialogWith);
|
|||
|
timerHangTime.AutoReset = false;
|
|||
|
timerHangTime.Elapsed += timerHangTime_Elapsed;
|
|||
|
timerHangTime.Start();
|
|||
|
}
|
|||
|
|
|||
|
Tuple<UdpClient, RTPSender2, RTPListener2, TimerWithTag<string>> sipDialogTuple = Tuple.Create(udpClient, rtpSender, rtpListener, timerHangTime);
|
|||
|
|
|||
|
// Sunt in dialog cu statia
|
|||
|
// Add to dictionary
|
|||
|
_IDdialogTuple.Add(radioInDialogWith, sipDialogTuple);
|
|||
|
_IDsInDialogWithList.Add(radioInDialogWith);
|
|||
|
// Add to list of group calls
|
|||
|
if (linxGroupID != null)
|
|||
|
_linxGroupSipIDsInDialogWith.Add(linxGroupID);
|
|||
|
}
|
|||
|
else
|
|||
|
throw new SipClassException(string.Format("Already in dialog with {0}",
|
|||
|
radioInDialogWith));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
void timerHangTime_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
|||
|
{
|
|||
|
|
|||
|
// End call
|
|||
|
TimerWithTag<string> timer = (TimerWithTag<string>)sender;
|
|||
|
if (_IDdialogTuple.ContainsKey(timer.Tag))
|
|||
|
{
|
|||
|
Dialog dialogToClose = _sipClient.GetDialogWith(timer.Tag);
|
|||
|
if (dialogToClose != null)
|
|||
|
{
|
|||
|
string sipIDidDialogWith = timer.Tag;
|
|||
|
string linxGroupID = null;
|
|||
|
if (ClosingDialog(sipIDidDialogWith, _sipClient.Username, out linxGroupID))
|
|||
|
{
|
|||
|
// Send Bye
|
|||
|
Task.Factory.StartNew((dialogToCloseObj) =>
|
|||
|
{
|
|||
|
_sipClient.Bye((Dialog)dialogToCloseObj);
|
|||
|
}, dialogToClose);
|
|||
|
// Fire event
|
|||
|
OnDialogClosed(new LinxDialogClosedEventArgs(
|
|||
|
sipIDidDialogWith, _sipClient.Username, DialogClosedReason.TimerHangTimeExpired, linxGroupID));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
private bool ClosingDialog(string idInDialogWith, string idWhoClosedTheDialog, out string linxGroupID)
|
|||
|
{
|
|||
|
linxGroupID = null;
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (_IDdialogTuple.ContainsKey(idInDialogWith))
|
|||
|
{
|
|||
|
Tuple<UdpClient, RTPSender2, RTPListener2, TimerWithTag<string>> dialogTuple = _IDdialogTuple[idInDialogWith];
|
|||
|
|
|||
|
|
|||
|
// Opresc timerele (send ptt hearbeat, heartbeat query, etc) din sender
|
|||
|
dialogTuple.Item2.Stop();
|
|||
|
|
|||
|
// Get the linxGroupID
|
|||
|
linxGroupID = ((RTPListenerLinx)dialogTuple.Item3).LinxGroupID;
|
|||
|
// Opresc citirea pachetelor rtp
|
|||
|
dialogTuple.Item3.Stop();
|
|||
|
// Unregister form the lister events
|
|||
|
dialogTuple.Item3.VoiceReceived -= ReceivedVoice;
|
|||
|
|
|||
|
|
|||
|
// Close the udp port
|
|||
|
dialogTuple.Item1.Close();
|
|||
|
|
|||
|
// dispose pentru _rtpListener
|
|||
|
dialogTuple.Item3.Dispose();
|
|||
|
|
|||
|
// Opresc timerul hangtime
|
|||
|
if (dialogTuple.Item4 != null)
|
|||
|
{
|
|||
|
dialogTuple.Item4.Elapsed -= timerHangTime_Elapsed;
|
|||
|
dialogTuple.Item4.Stop();
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
_IDdialogTuple.Remove(idInDialogWith);
|
|||
|
_IDsInDialogWithList.Remove(idInDialogWith);
|
|||
|
// remove from list of group dialogs
|
|||
|
if (linxGroupID != null)
|
|||
|
_linxGroupSipIDsInDialogWith.Remove(linxGroupID);
|
|||
|
return true;
|
|||
|
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
private SipClient CreateSipClientClass(string sipDomain, int sipDomainPort, string localIPaddress, int localSipPort, string userName, string password, int requestTimeout)
|
|||
|
{
|
|||
|
SipClient sipClient;
|
|||
|
// Set up the master SIP class
|
|||
|
sipClient = new SipClient(sipDomain, sipDomainPort, Independentsoft.Sip.ProtocolType.Udp, userName, password);
|
|||
|
sipClient.LocalIPEndPoint = new IPEndPoint(IPAddress.Parse(localIPaddress), localSipPort);
|
|||
|
// Turn on logging
|
|||
|
//sipClient.Logger = new Logger(AppDomain.CurrentDomain.BaseDirectory + "\\sipLog.txt");
|
|||
|
sipClient.ReceiveRequest += ProcessSipRequest;
|
|||
|
sipClient.ReceiveResponse += ProcessSipResponse;
|
|||
|
sipClient.Timeout = requestTimeout;
|
|||
|
|
|||
|
sipClient.Connect();
|
|||
|
_sipClassClosed = false;
|
|||
|
return sipClient;
|
|||
|
}
|
|||
|
|
|||
|
private void SendSipInvite(string idToCall, bool isGroup)
|
|||
|
{
|
|||
|
lock (_lockerSipDialog)
|
|||
|
{
|
|||
|
if (!_IDsentInviteDict.ContainsKey(idToCall))
|
|||
|
{
|
|||
|
// Send invite to somewone
|
|||
|
string sipServerIP = _sipClient.Domain;
|
|||
|
string sipID = _sipClient.Username;
|
|||
|
|
|||
|
int rtpPort = ReturnAvailablePort();
|
|||
|
SessionDescription sdp = CreateSDP(rtpPort);
|
|||
|
Invite inv = new Invite();
|
|||
|
inv.Uri = "sip:" + idToCall + "@" + sipServerIP;
|
|||
|
inv.From = new ContactInfo("sip:" + sipID.ToString() + "@" + sipServerIP);
|
|||
|
inv.To = new ContactInfo("sip:" + idToCall + "@" + sipServerIP);
|
|||
|
inv.Contact = new Contact("sip:" + sipID.ToString() + "@" + _sipClient.LocalIPEndPoint.ToString());
|
|||
|
inv.SessionDescription = sdp;
|
|||
|
// Add Adi headers
|
|||
|
inv.Header.Add("fromSipID", UserName);
|
|||
|
inv.Header.Add("fromUserID", "");
|
|||
|
inv.Header.Add("isPTT", "true");
|
|||
|
if (isGroup)
|
|||
|
{
|
|||
|
inv.Header.Add("toGroupSipID", idToCall);
|
|||
|
inv.Header.Add("toGroupID", "");
|
|||
|
inv.Header.Add("toSipID", "");
|
|||
|
inv.Header.Add("toUserID", "");
|
|||
|
inv.Header.Add("callType", "group");
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
inv.Header.Add("toGroupSipID", "");
|
|||
|
inv.Header.Add("toGroupID", "");
|
|||
|
inv.Header.Add("toSipID", idToCall);
|
|||
|
inv.Header.Add("toUserID", "");
|
|||
|
inv.Header.Add("callType", "private");
|
|||
|
}
|
|||
|
//
|
|||
|
|
|||
|
// Add to dictionary
|
|||
|
_IDsentInviteDict.Add(idToCall, inv);
|
|||
|
_IDsCalledByMeList.Add(idToCall);
|
|||
|
|
|||
|
// Send invite request
|
|||
|
Task.Factory.StartNew((idToCallObj) =>
|
|||
|
{
|
|||
|
string id = (string)idToCallObj;
|
|||
|
OnInviteSent(new SipEventArgs(id));
|
|||
|
try
|
|||
|
{
|
|||
|
_sipClient.SendRequest(inv);
|
|||
|
}
|
|||
|
catch (Exception tEx)
|
|||
|
{
|
|||
|
// Timout exception
|
|||
|
|
|||
|
// Cancel the invite
|
|||
|
Task t = null;
|
|||
|
SendCancelRequest(id, t);
|
|||
|
// Fire event
|
|||
|
OnError(new ErrorEventArgs(id, _sipClient.Username, tEx.Message));
|
|||
|
}
|
|||
|
|
|||
|
}, idToCall);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void SendSipRegister(object registrationDataObj)
|
|||
|
{
|
|||
|
string sipServerIP = _sipClient.Domain;
|
|||
|
RegistrationData regData = (RegistrationData)registrationDataObj;
|
|||
|
string sipIDfrom = _sipClient.Username;
|
|||
|
string sipIDto = regData.SipID;
|
|||
|
int expiresValue = regData.Expires;
|
|||
|
bool isUnregisterRequest = (expiresValue == 0);
|
|||
|
|
|||
|
Register reg = new Register();
|
|||
|
reg.Uri = "sip:" + sipServerIP;
|
|||
|
reg.From = new ContactInfo(
|
|||
|
sipIDfrom.ToString(),
|
|||
|
"sip:" + sipIDfrom + "@" + sipServerIP);
|
|||
|
reg.To = new ContactInfo(
|
|||
|
sipIDto.ToString(),
|
|||
|
"sip:" + sipIDto.ToString() + "@" + sipServerIP);
|
|||
|
reg.Contact = new Contact("sip:" + sipIDfrom.ToString() + "@" + _sipClient.LocalIPEndPoint.ToString());
|
|||
|
reg.Expires = expiresValue;
|
|||
|
|
|||
|
RegistrationStatus regStatus = _sipID_regTimer_regStatus_Dict[sipIDto].Item2;
|
|||
|
|
|||
|
try
|
|||
|
{
|
|||
|
RequestResponse reqResp = _sipClient.SendRequest(reg);
|
|||
|
if (reqResp.Response.StatusCode == 200) //OK
|
|||
|
{
|
|||
|
if (isUnregisterRequest)
|
|||
|
{
|
|||
|
regStatus = RegistrationStatus.UnregisteredAtRequest;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
regStatus = RegistrationStatus.Registered;
|
|||
|
}
|
|||
|
}
|
|||
|
else if (reqResp.Response.StatusCode == 403) // 403 = Forbidden
|
|||
|
{
|
|||
|
regStatus = RegistrationStatus.WrongCredentials;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
regStatus = RegistrationStatus.NotRegistered;
|
|||
|
// to do - delete this
|
|||
|
Console.WriteLine($"Received response: {reqResp.Response.StatusCode} to sip registration request");
|
|||
|
}
|
|||
|
}
|
|||
|
catch (SocketException socketEx)
|
|||
|
{
|
|||
|
regStatus = RegistrationStatus.SocketError;
|
|||
|
}
|
|||
|
catch (Independentsoft.Sip.TimeoutException)
|
|||
|
{
|
|||
|
regStatus = RegistrationStatus.RequestTimeout;
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
regStatus = RegistrationStatus.NotRegistered;
|
|||
|
// to do - delete this
|
|||
|
Console.WriteLine("Exception when sending registration to Sip server\n" + ex.ToString());
|
|||
|
}
|
|||
|
|
|||
|
if (regStatus != _sipID_regTimer_regStatus_Dict[sipIDto].Item2)
|
|||
|
{
|
|||
|
if (regStatus != RegistrationStatus.UnregisteredAtRequest)
|
|||
|
{
|
|||
|
// Just change the registration status
|
|||
|
System.Timers.Timer currentTimer = _sipID_regTimer_regStatus_Dict[sipIDto].Item1;
|
|||
|
_sipID_regTimer_regStatus_Dict[sipIDto] = new Tuple<System.Timers.Timer, RegistrationStatus>(currentTimer, regStatus);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Unregistered - Remove from dictionary
|
|||
|
_sipID_regTimer_regStatus_Dict.Remove(sipIDto);
|
|||
|
_IDsregisteredList.Remove(sipIDto);
|
|||
|
}
|
|||
|
//OnRegistrationStateChanged(sipIDto, regStatus);
|
|||
|
OnRegistrationStateChanged(new RegistrationStateChangedEventArgs(sipIDto, regStatus));
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private SessionDescription CreateSDP(int rtpPort)
|
|||
|
{
|
|||
|
SessionDescription sdpSession = new SessionDescription();
|
|||
|
|
|||
|
// Origin (Owner) - 0=<userName> <sessionId> <version> <network type> <address type>
|
|||
|
|
|||
|
// <sessionId> must be unique
|
|||
|
int sessionID = _rand.Next();
|
|||
|
// <version> - must be increased when a modification is made to the session data.
|
|||
|
Owner owner = new Owner(_sipClient.Username, sessionID, 18299, _sipClient.LocalIPEndPoint.Address.ToString());
|
|||
|
|
|||
|
Connection connection = new Connection(_sipClient.LocalIPEndPoint.Address.ToString());
|
|||
|
sdpSession.Owner = owner;
|
|||
|
sdpSession.Name = "_";
|
|||
|
sdpSession.Connection = connection;
|
|||
|
sdpSession.Time.Add(new Time(0, 0));
|
|||
|
// m=<media> <port> <transport> <fmt list>
|
|||
|
// <port> - 1024 to 65535 for UDP
|
|||
|
// - must be even for RTP
|
|||
|
Media media1 = new Media("audio", rtpPort, "RTP/AVP");
|
|||
|
media1.MediaFormats.Add("8");
|
|||
|
media1.Attributes.Add("rtpmap", "8 PCMA/8000");
|
|||
|
sdpSession.Media.Add(media1);
|
|||
|
|
|||
|
return sdpSession;
|
|||
|
}
|
|||
|
|
|||
|
private void StartRegistrationTimer()
|
|||
|
{
|
|||
|
// Send instant registration request
|
|||
|
Task.Factory.StartNew((state) =>
|
|||
|
{
|
|||
|
SendSipRegister(state);
|
|||
|
}, _registrationData);
|
|||
|
// Start registration timer
|
|||
|
_sipID_regTimer_regStatus_Dict[UserName].Item1.Start();
|
|||
|
}
|
|||
|
|
|||
|
private void _registrationTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
|||
|
{
|
|||
|
SendSipRegister(_registrationData);
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Private Helper Methods
|
|||
|
|
|||
|
private int ReturnAvailablePort()
|
|||
|
{
|
|||
|
lock (_lockerRtpPort)
|
|||
|
{
|
|||
|
int rtpPort = MinRtpPortNumber;
|
|||
|
while (
|
|||
|
(IsPortAllreadyInUse(rtpPort) || PortNumerProposedInSentInvites(rtpPort))
|
|||
|
&& rtpPort < MaxRtpPortNumber)
|
|||
|
{
|
|||
|
rtpPort += 2;
|
|||
|
}
|
|||
|
if (rtpPort < MaxRtpPortNumber)
|
|||
|
return rtpPort;
|
|||
|
else
|
|||
|
throw new SipClassException(
|
|||
|
string.Format("Nu gasesc port liber in range-ul {0} - {1}", MinRtpPortNumber, MaxRtpPortNumber));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private bool PortNumerProposedInSentInvites(int portNb)
|
|||
|
{
|
|||
|
Invite inv = _IDsentInviteDict.Values.FirstOrDefault((invite) =>
|
|||
|
{
|
|||
|
return invite.SessionDescription.Media[0].Port == portNb;
|
|||
|
});
|
|||
|
if (inv != null)
|
|||
|
return true;
|
|||
|
else
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
private bool IsPortAllreadyInUse(int portNumber)
|
|||
|
{
|
|||
|
|
|||
|
return (from p in System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners()
|
|||
|
where p.Port == portNumber
|
|||
|
select p).Count() == 1;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Checks if an integer can be used as a rtp port
|
|||
|
/// </summary>
|
|||
|
/// <param name="rtpPort">The integer to be cecked</param>
|
|||
|
/// <returns>True if is a valid rtp port, else false</returns>
|
|||
|
public static bool ValidRtpPort(int rtpPort)
|
|||
|
{
|
|||
|
if (rtpPort < 1024 || rtpPort > 65534)
|
|||
|
{
|
|||
|
return false;
|
|||
|
}
|
|||
|
else if (rtpPort % 2 == 1)
|
|||
|
{
|
|||
|
return false;
|
|||
|
}
|
|||
|
else
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Determines the most appropriate local end point to contact the provided remote end point.
|
|||
|
/// Testing shows this method takes on average 1.6ms to return.
|
|||
|
/// </summary>
|
|||
|
/// <param name="remoteIPEndPoint">The remote end point</param>
|
|||
|
/// <returns>The selected local end point</returns>
|
|||
|
private static IPEndPoint BestLocalEndPoint(IPEndPoint remoteIPEndPoint)
|
|||
|
{
|
|||
|
Socket testSocket = new Socket(remoteIPEndPoint.AddressFamily, SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
|
|||
|
testSocket.Connect(remoteIPEndPoint);
|
|||
|
return (IPEndPoint)testSocket.LocalEndPoint;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
private bool IsInSameSubnet(IPAddress address2, IPAddress address, IPAddress subnetMask)
|
|||
|
{
|
|||
|
IPAddress network1 = GetNetworkAddress(address, subnetMask);
|
|||
|
IPAddress network2 = GetNetworkAddress(address2, subnetMask);
|
|||
|
|
|||
|
return network1.Equals(network2);
|
|||
|
}
|
|||
|
|
|||
|
private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
|
|||
|
{
|
|||
|
byte[] ipAdressBytes = address.GetAddressBytes();
|
|||
|
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
|
|||
|
|
|||
|
if (ipAdressBytes.Length != subnetMaskBytes.Length)
|
|||
|
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
|
|||
|
|
|||
|
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
|
|||
|
for (int i = 0; i < broadcastAddress.Length; i++)
|
|||
|
{
|
|||
|
broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
|
|||
|
}
|
|||
|
return new IPAddress(broadcastAddress);
|
|||
|
}
|
|||
|
|
|||
|
private string AddMsgLenForMBus(string textToSend)
|
|||
|
{
|
|||
|
String cmdok = textToSend;
|
|||
|
Int32 tmp = cmdok.Length + 1;
|
|||
|
tmp += tmp.ToString().Length;
|
|||
|
String TMPcmdok = "#" + tmp.ToString() + cmdok;
|
|||
|
if (tmp != TMPcmdok.Length) cmdok = "#" + TMPcmdok.Length + cmdok;
|
|||
|
else cmdok = TMPcmdok;
|
|||
|
return cmdok;
|
|||
|
}
|
|||
|
|
|||
|
private DateTime UnixTimeStampToDateTime(double unixTimeStamp)
|
|||
|
{
|
|||
|
// Unix timestamp is seconds past epoch
|
|||
|
System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
|
|||
|
dtDateTime = dtDateTime.AddSeconds(unixTimeStamp);
|
|||
|
return dtDateTime;
|
|||
|
}
|
|||
|
|
|||
|
private double DateTimeToUnixTime(DateTime utcTime)
|
|||
|
{
|
|||
|
System.DateTime baseTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
|
|||
|
return (utcTime - baseTime).TotalSeconds;
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Events
|
|||
|
/// <summary>
|
|||
|
/// Occurs when you send an invite to a sip id
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<SipEventArgs> InviteSent;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when you cancel a sent invite
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<SipEventArgs> InviteSentCanceled;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when you receive an invite
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<InviteReceivedArgs> InviteReceived;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when an invite is cancelled before you had time to accept or reject it.
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<SipEventArgs> InviteReceivedCanceled;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when you decline an invite from another user
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<SipEventArgs> InviteReceivedDeclined;
|
|||
|
/// <summary>
|
|||
|
/// An error occured while attempting to establish a voice session
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<ErrorEventArgs> ErrorOnCreatingDialog;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when a voice session (a dialog) has been established
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<LinxDialogCreatedEventArgs> DialogCreated;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when a voice session (a dialog) is closed
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<LinxDialogClosedEventArgs> DialogClosed;
|
|||
|
/// <summary>
|
|||
|
/// Ocurs when a voice buffer is received from a sip id
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<LinxAudioEventArgs> VoiceReceived;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when you receive an sms as a Sip message
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<SmsReceivedEventsArgs> SipSmsReceived;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when the sip registration status changes
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<RegistrationStateChangedEventArgs> RegistrationStateChanged;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when a GPS report is received from a Linx device
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<LinxGpsDataEventArgs> GpsReportReceived;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when a periodically GPS report is received
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<GpsDataEventArgs> GpsPeriodicallyReportReceived;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when an emergency alarm is received from a Linx device
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<Linx.LinxEmergencyAlarmReceivedEventArgs> EmergencyAlarmReceived;
|
|||
|
/// <summary>
|
|||
|
/// Occurs when an Ars command is received from a Linx device
|
|||
|
/// </summary>
|
|||
|
protected event EventHandler<Linx.ArsReceivedEventArgs> LinxArsReceivedOnSip;
|
|||
|
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Occurs when hangtime is ended
|
|||
|
/// </summary>
|
|||
|
public event EventHandler<HangtimeEndedEventArgs> HangtimeEnded;
|
|||
|
|
|||
|
|
|||
|
private void OnInviteSent(SipEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<SipEventArgs> handler = InviteSent;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnInviteSentCanceled(SipEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<SipEventArgs> handler = InviteSentCanceled;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// This is the method that raises the ErrorOnCreatingDialog event
|
|||
|
/// </summary>
|
|||
|
/// <param name="e">Data for the ErrorOnCreatingDialog event</param>
|
|||
|
/// <remarks>Do not forget to call the base version when overrinding,
|
|||
|
/// <para>otherwize the ErrorOnCreatingDialog event will not fire</para>
|
|||
|
/// </remarks>
|
|||
|
protected virtual void OnError(ErrorEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<ErrorEventArgs> handler = ErrorOnCreatingDialog;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnInviteReceived(InviteReceivedArgs e)
|
|||
|
{
|
|||
|
EventHandler<InviteReceivedArgs> handler = InviteReceived;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnInviteReceivedCanceled(SipEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<SipEventArgs> handler = InviteReceivedCanceled;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnInviteReceivedDeclined(SipEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<SipEventArgs> handler = InviteReceivedDeclined;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// This is the method that raises the DialogCreated event
|
|||
|
/// </summary>
|
|||
|
/// <param name="e">Data for the DialogCreatedEvent</param>
|
|||
|
/// <remarks>Do not forget to call the base version when overrinding,
|
|||
|
/// <para>otherwize the DialogCreated Event will not fire</para>
|
|||
|
/// </remarks>
|
|||
|
protected virtual void OnDialogCreated(LinxDialogCreatedEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<LinxDialogCreatedEventArgs> handler = DialogCreated;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// This is the method that raises the DialogClosed event
|
|||
|
/// </summary>
|
|||
|
/// <param name="e">Data for the DialogClosed event</param>
|
|||
|
/// <remarks>Do not forget to call the base version when overrinding,
|
|||
|
/// <para>otherwize the DialogClosed Event will not fire</para>
|
|||
|
/// </remarks>
|
|||
|
protected virtual void OnDialogClosed(LinxDialogClosedEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<LinxDialogClosedEventArgs> handler = DialogClosed;
|
|||
|
if (handler != null)
|
|||
|
{
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// This is the method that raises the VoiceReceived event
|
|||
|
/// </summary>
|
|||
|
/// <param name="e">Data for the VoiceReceived event</param>
|
|||
|
/// <remarks>Do not forget to call the base version when overrinding,
|
|||
|
/// <para>otherwise the VoiceReceived Event will not fire</para>
|
|||
|
/// </remarks>
|
|||
|
protected virtual void OnVoiceReceived(LinxAudioEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<LinxAudioEventArgs> handler = VoiceReceived;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnRegistrationStateChanged(RegistrationStateChangedEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<RegistrationStateChangedEventArgs> handler = RegistrationStateChanged;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnSipSmsReceived(SmsReceivedEventsArgs e)
|
|||
|
{
|
|||
|
EventHandler<SmsReceivedEventsArgs> handler = SipSmsReceived;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnGpsPeriodicallyReportReceived(GpsDataEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<GpsDataEventArgs> handler = GpsPeriodicallyReportReceived;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnLinxEmergencyAlarmReceived(Linx.LinxEmergencyAlarmReceivedEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<Linx.LinxEmergencyAlarmReceivedEventArgs> handler = EmergencyAlarmReceived;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnGpsReportReceived(LinxGpsDataEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<LinxGpsDataEventArgs> handler = GpsReportReceived;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
private void OnLinxArsReceived(Linx.ArsReceivedEventArgs e)
|
|||
|
{
|
|||
|
EventHandler<Linx.ArsReceivedEventArgs> handler = LinxArsReceivedOnSip;
|
|||
|
if (handler != null)
|
|||
|
handler(this, e);
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
}
|
|||
|
}
|