676 lines
22 KiB
C#
676 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Monoculus
|
|
{
|
|
// Enums for different states
|
|
public enum ClientState
|
|
{
|
|
None = 0x00,
|
|
Connecting = 0x01,
|
|
ProtoHandshake = 0x02,
|
|
LoginPhase1 = 0x03,
|
|
LoginPhase2 = 0x04,
|
|
MessageHeader = 0x05,
|
|
Message = 0x06,
|
|
Tx = 0x07,
|
|
Rx = 0x08,
|
|
ClientsHeader = 0x09,
|
|
Clients = 0x0A,
|
|
NetworksHeader = 0x0B,
|
|
Networks = 0x0C,
|
|
SndFrameIn = 0x0D,
|
|
KeepAlive = 0x0E,
|
|
Disconnected = 0x0F,
|
|
TxRequest = 0x10,
|
|
TxWaiting = 0x11,
|
|
TxApproved = 0x12,
|
|
TxRejected = 0x13,
|
|
TxComplete = 0x14,
|
|
Ping = 0x15,
|
|
BanlistHeader = 0x16,
|
|
Banlist = 0x17,
|
|
MutelistHeader = 0x18,
|
|
Mutelist = 0x19,
|
|
PttDown = 0x1A,
|
|
PttUp = 0x1B,
|
|
MessageInput = 0x1C,
|
|
MessageSend = 0x1D,
|
|
Abort = 0xFE,
|
|
Idle = 0xFF
|
|
}
|
|
|
|
// Constants for markers
|
|
public static class Markers
|
|
{
|
|
public const byte KeepAlive = 0x00;
|
|
public const byte TxApprove = 0x01;
|
|
public const byte Sound = 0x02;
|
|
public const byte Clients = 0x03;
|
|
public const byte Message = 0x04;
|
|
public const byte Networks = 0x05;
|
|
public const byte Ban = 0x08;
|
|
public const byte Mute = 0x09;
|
|
// Additional markers based on documentation
|
|
public const byte DT_IDLE = 0x00;
|
|
public const byte DT_DO_TX = 0x01;
|
|
public const byte DT_VOICE_BUFFER = 0x02;
|
|
public const byte DT_CLIENT_LIST = 0x03;
|
|
public const byte DT_TEXT_MESSAGE = 0x04;
|
|
public const byte DT_NET_NAMES = 0x05;
|
|
public const byte DT_ADMIN_LIST = 0x06;
|
|
public const byte DT_ACCESS_LIST = 0x07;
|
|
public const byte DT_BLOCK_LIST = 0x08;
|
|
public const byte DT_MUTE_LIST = 0x09;
|
|
public const byte DT_ACCESS_MODE = 0x0A;
|
|
}
|
|
|
|
// Other constants
|
|
public static class Constants
|
|
{
|
|
public const int KeepAliveTimeout = 1;
|
|
public const int FRNProtocolVersion = 2022001;
|
|
|
|
public const string FRNTypePCOnly = "PC Only";
|
|
public const string FRNTypeCrosslink = "Crosslink";
|
|
public const string FRNTypeParrot = "Parrot";
|
|
|
|
public const string FRNMessageBroadcast = "A";
|
|
public const string FRNMessagePrivate = "P";
|
|
|
|
public const int FRNStatusOnline = 0;
|
|
public const int FRNStatusAway = 1;
|
|
public const int FRNStatusNA = 2;
|
|
|
|
public const int FRNMuteOff = 0;
|
|
public const int FRNMuteOn = 1;
|
|
|
|
public const string FRNResultOK = "OK";
|
|
public const string FRNResultNOK = "NOK";
|
|
public const string FRNResultWrong = "WRONG";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a Free Radio Network client.
|
|
/// </summary>
|
|
public class FRNClient
|
|
{
|
|
private TcpClient _tcpClient;
|
|
private NetworkStream _networkStream;
|
|
private readonly string _host;
|
|
private readonly int _port;
|
|
private ClientState _state;
|
|
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
|
|
|
// Client parameters
|
|
private readonly string _callSign;
|
|
private readonly string _name;
|
|
private readonly string _email;
|
|
private readonly string _password;
|
|
private readonly string _network;
|
|
private readonly string _country;
|
|
private readonly string _city;
|
|
private readonly string _locator;
|
|
private readonly string _type;
|
|
private readonly string _description;
|
|
|
|
// Buffers
|
|
private readonly List<byte> _inputBuffer = new List<byte>();
|
|
private readonly StringBuilder _outputBuffer = new StringBuilder();
|
|
|
|
// Last KeepAlive timestamp
|
|
private DateTime _lastKeepAlive;
|
|
|
|
// Event handlers (callbacks)
|
|
public event Action OnConnect;
|
|
public event Action OnDisconnect;
|
|
public event Action OnLogin;
|
|
public event Action<string> OnMessageReceived;
|
|
public event Action<byte[], int> OnVoiceDataReceived;
|
|
public event Action<List<ClientInfo>> OnClientListReceived;
|
|
|
|
// Additional properties and flags
|
|
private bool _isConnected;
|
|
|
|
// Variables to track voice data collection
|
|
private bool _isReceivingVoiceData = false;
|
|
private MemoryStream _voiceDataStream = new MemoryStream();
|
|
private int _currentActiveClientIndex = -1;
|
|
private readonly object _voiceDataLock = new object();
|
|
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the FRNClient class.
|
|
/// </summary>
|
|
public FRNClient(string host, int port, string callSign, string name, string email, string password, string network, string country, string city, string locator, string type, string description)
|
|
{
|
|
_host = host;
|
|
_port = port;
|
|
_callSign = callSign;
|
|
_name = name;
|
|
_email = email;
|
|
_password = password;
|
|
_network = network;
|
|
_country = country ?? "N/A";
|
|
_city = city ?? "N/A";
|
|
_locator = locator ?? "N/A";
|
|
_type = type ?? Constants.FRNTypePCOnly;
|
|
_description = description ?? string.Empty;
|
|
_state = ClientState.None;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the client and connects to the server.
|
|
/// </summary>
|
|
public async Task RunAsync()
|
|
{
|
|
await ConnectAsync();
|
|
|
|
if (_isConnected)
|
|
{
|
|
await ProcessAsync(_cancellationTokenSource.Token);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Connects to the FRN server.
|
|
/// </summary>
|
|
private async Task ConnectAsync()
|
|
{
|
|
try
|
|
{
|
|
_tcpClient = new TcpClient();
|
|
await _tcpClient.ConnectAsync(_host, _port);
|
|
_networkStream = _tcpClient.GetStream();
|
|
_state = ClientState.Connecting;
|
|
_isConnected = true;
|
|
|
|
OnConnect?.Invoke();
|
|
#if DEBUG
|
|
Console.WriteLine("Connected to server.");
|
|
#endif
|
|
// Start the login process
|
|
await SendLoginAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Connection failed: {ex.Message}");
|
|
_isConnected = false;
|
|
_state = ClientState.Disconnected;
|
|
|
|
OnDisconnect?.Invoke();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes incoming and outgoing data.
|
|
/// </summary>
|
|
private async Task ProcessAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var readTask = ReadAsync(cancellationToken);
|
|
var writeTask = WriteAsync(cancellationToken);
|
|
var keepAliveTask = KeepAliveAsync(cancellationToken);
|
|
|
|
await Task.WhenAll(readTask, writeTask, keepAliveTask);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Processing error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads data from the network stream.
|
|
/// </summary>
|
|
private async Task ReadAsync(CancellationToken cancellationToken)
|
|
{
|
|
var buffer = new byte[8192];
|
|
|
|
while (_isConnected && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
if (_networkStream.DataAvailable)
|
|
{
|
|
int bytesRead = await _networkStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
|
|
if (bytesRead == 0)
|
|
{
|
|
// Connection closed
|
|
_isConnected = false;
|
|
_state = ClientState.Disconnected;
|
|
|
|
OnDisconnect?.Invoke();
|
|
#if DEBUG
|
|
Console.WriteLine("Disconnected from server.");
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
// Process incoming data
|
|
byte[] dataReceived = new byte[bytesRead];
|
|
Array.Copy(buffer, dataReceived, bytesRead);
|
|
#if DEBUG
|
|
Console.WriteLine($"Received raw data ({bytesRead} bytes): {BitConverter.ToString(dataReceived)}");
|
|
#endif
|
|
_inputBuffer.AddRange(dataReceived);
|
|
ParseIncomingData();
|
|
}
|
|
else
|
|
{
|
|
await Task.Delay(100, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses incoming data from the server.
|
|
/// </summary>
|
|
private void ParseIncomingData()
|
|
{
|
|
while (true)
|
|
{
|
|
if (_inputBuffer.Count < 1)
|
|
{
|
|
// Not enough data
|
|
break;
|
|
}
|
|
|
|
if (_state == ClientState.LoginPhase1)
|
|
{
|
|
// We're expecting a login response
|
|
int newlineIndex = _inputBuffer.LastIndexOf((byte)'\r');
|
|
if (newlineIndex != -1)
|
|
{
|
|
byte[] lineBytes = _inputBuffer.GetRange(0, newlineIndex + 1).ToArray();
|
|
_inputBuffer.RemoveRange(0, newlineIndex + 1);
|
|
string response = Encoding.ASCII.GetString(lineBytes).Trim();
|
|
|
|
HandleLoginPhase1(response);
|
|
}
|
|
else
|
|
{
|
|
// Wait for more data
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
byte dataType = _inputBuffer[0];
|
|
|
|
switch (dataType)
|
|
{
|
|
case Markers.DT_IDLE:
|
|
_inputBuffer.RemoveAt(0); // Remove DataType byte
|
|
HandleIdleState();
|
|
break;
|
|
case Markers.DT_DO_TX:
|
|
if (_inputBuffer.Count >= 1 + 2)
|
|
{
|
|
_inputBuffer.RemoveAt(0); // Remove DataType byte
|
|
HandleDoTx();
|
|
}
|
|
else
|
|
{
|
|
// Not enough data
|
|
return;
|
|
}
|
|
break;
|
|
case Markers.DT_VOICE_BUFFER:
|
|
if (_inputBuffer.Count >= 1 + 2 + 325)
|
|
{
|
|
_inputBuffer.RemoveAt(0); // Remove DataType byte
|
|
HandleVoiceBuffer();
|
|
}
|
|
else
|
|
{
|
|
// Not enough data
|
|
return;
|
|
}
|
|
break;
|
|
// Handle other data types similarly
|
|
default:
|
|
Console.WriteLine($"Unknown data type received: {dataType}");
|
|
_inputBuffer.RemoveAt(0); // Remove unknown DataType byte
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'DT_VOICE_BUFFER' data type.
|
|
/// </summary>
|
|
private void HandleVoiceBuffer()
|
|
{
|
|
if (_inputBuffer.Count >= 2 + 325)
|
|
{
|
|
ushort clientIndex = (ushort)((_inputBuffer[0] << 8) | _inputBuffer[1]);
|
|
_inputBuffer.RemoveRange(0, 2);
|
|
|
|
byte[] voiceData = _inputBuffer.GetRange(0, 325).ToArray();
|
|
_inputBuffer.RemoveRange(0, 325);
|
|
#if DEBUG
|
|
Console.WriteLine($"Received voice data from client index: {clientIndex}");
|
|
#endif
|
|
// Invoke the event handler to process the voice data
|
|
OnVoiceDataReceived?.Invoke(voiceData, clientIndex);
|
|
}
|
|
else
|
|
{
|
|
// Not enough data
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'DT_DO_TX' data type.
|
|
/// </summary>
|
|
private void HandleDoTx()
|
|
{
|
|
if (_inputBuffer.Count >= 2)
|
|
{
|
|
ushort clientIndex = (ushort)((_inputBuffer[0] << 8) | _inputBuffer[1]);
|
|
_inputBuffer.RemoveRange(0, 2);
|
|
Console.WriteLine($"Server indicates you may start sending voice data. Active client index: {clientIndex}");
|
|
// Implement logic to start transmitting voice data
|
|
}
|
|
else
|
|
{
|
|
// Wait for more data
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the 'DT_IDLE' data type.
|
|
/// </summary>
|
|
private void HandleIdleState()
|
|
{
|
|
#if DEBUG
|
|
Console.WriteLine("Server is idle. No new data.");
|
|
#endif
|
|
|
|
lock (_voiceDataLock)
|
|
{
|
|
if (_isReceivingVoiceData)
|
|
{
|
|
// Transmission ended
|
|
_isReceivingVoiceData = false;
|
|
|
|
// Reset active client index
|
|
_currentActiveClientIndex = -1;
|
|
_voiceDataStream.SetLength(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses incoming data for login response.
|
|
/// </summary>
|
|
private void HandleLoginPhase1(string response)
|
|
{
|
|
// Extract the server version number
|
|
int index = 0;
|
|
while (index < response.Length && char.IsDigit(response[index]))
|
|
{
|
|
index++;
|
|
}
|
|
|
|
string versionString = response.Substring(0, index);
|
|
string xmlData = response.Substring(index);
|
|
#if DEBUG
|
|
Console.WriteLine($"Server version: {versionString}");
|
|
#endif
|
|
// Parse the XML-like data
|
|
var xmlTags = new Dictionary<string, string>();
|
|
|
|
int pos = 0;
|
|
while (pos < xmlData.Length)
|
|
{
|
|
if (xmlData[pos] == '<')
|
|
{
|
|
int tagStart = pos + 1;
|
|
int tagEnd = xmlData.IndexOf('>', tagStart);
|
|
if (tagEnd == -1)
|
|
break; // incomplete tag
|
|
|
|
string tagName = xmlData.Substring(tagStart, tagEnd - tagStart);
|
|
|
|
int closingTagStart = xmlData.IndexOf($"</{tagName}>", tagEnd + 1);
|
|
if (closingTagStart == -1)
|
|
{
|
|
// Some tags might be self-closing or empty; adjust as needed
|
|
closingTagStart = tagEnd;
|
|
// break; // incomplete data
|
|
}
|
|
|
|
string tagValue = xmlData.Substring(tagEnd + 1, closingTagStart - tagEnd - 1);
|
|
|
|
xmlTags[tagName] = tagValue;
|
|
|
|
pos = closingTagStart + tagName.Length + 3; // Move past the closing tag
|
|
}
|
|
else
|
|
{
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
// Process the extracted tags
|
|
if (xmlTags.TryGetValue("AL", out string accessLevel))
|
|
{
|
|
Console.WriteLine($"Access Level: {accessLevel}");
|
|
|
|
if (accessLevel == "OK" || accessLevel == "OWNER" || accessLevel == "NETOWNER" || accessLevel == "ADMIN")
|
|
{
|
|
_state = ClientState.LoginPhase2;
|
|
OnLogin?.Invoke();
|
|
#if DEBUG
|
|
Console.WriteLine("Login successful.");
|
|
#endif
|
|
_state = ClientState.Idle;
|
|
|
|
// Send 'RX0' to server as per documentation
|
|
SendAsync("RX0\r\n").Wait();
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Login failed or access blocked.");
|
|
_state = ClientState.Abort;
|
|
_isConnected = false;
|
|
OnDisconnect?.Invoke();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Failed to parse access level.");
|
|
_state = ClientState.Abort;
|
|
_isConnected = false;
|
|
OnDisconnect?.Invoke();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes data to the network stream.
|
|
/// </summary>
|
|
private async Task WriteAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (_isConnected && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
if (_outputBuffer.Length > 0)
|
|
{
|
|
string dataToSend;
|
|
lock (_outputBuffer)
|
|
{
|
|
dataToSend = _outputBuffer.ToString();
|
|
_outputBuffer.Clear();
|
|
}
|
|
|
|
byte[] data = Encoding.ASCII.GetBytes(dataToSend);
|
|
#if DEBUG
|
|
Console.WriteLine($"Sending raw data ({data.Length} bytes): {BitConverter.ToString(data)}");
|
|
#endif
|
|
await _networkStream.WriteAsync(data, 0, data.Length, cancellationToken);
|
|
await _networkStream.FlushAsync(cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
await Task.Delay(100, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Sends periodic keep-alive messages to the server.
|
|
/// </summary>
|
|
private async Task KeepAliveAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (_isConnected && !cancellationToken.IsCancellationRequested)
|
|
{
|
|
if ((DateTime.Now - _lastKeepAlive).TotalSeconds > Constants.KeepAliveTimeout)
|
|
{
|
|
await SendKeepAliveAsync();
|
|
}
|
|
|
|
await Task.Delay(500, cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends the initial login message to the server.
|
|
/// </summary>
|
|
private async Task SendLoginAsync()
|
|
{
|
|
string loginMessage = BuildLoginMessage();
|
|
await SendAsync(loginMessage);
|
|
|
|
_state = ClientState.LoginPhase1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the login message.
|
|
/// </summary>
|
|
private string BuildLoginMessage()
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append("CT:");
|
|
sb.Append($"<VX>{Constants.FRNProtocolVersion}</VX>");
|
|
sb.Append($"<EA>{_email}</EA>");
|
|
sb.Append($"<PW>{_password}</PW>");
|
|
sb.Append($"<ON>{_callSign}</ON>");
|
|
sb.Append($"<CL>0</CL>");
|
|
sb.Append($"<BC>{_type}</BC>"); // Adjusted as per documentation
|
|
sb.Append($"<DS>{_description}</DS>");
|
|
sb.Append($"<NN>{_country}</NN>");
|
|
sb.Append($"<CT>{_city} - {_locator}</CT>");
|
|
sb.Append($"<NT>{_network}</NT>");
|
|
sb.Append("\r\n");
|
|
#if DEBUG
|
|
Console.WriteLine($"Built login message: {sb}");
|
|
#endif
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a keep-alive message to the server.
|
|
/// </summary>
|
|
private async Task SendKeepAliveAsync()
|
|
{
|
|
await SendAsync("P\r\n");
|
|
_lastKeepAlive = DateTime.Now;
|
|
#if DEBUG
|
|
Console.WriteLine("Sent keep-alive to server.");
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends data to the server.
|
|
/// </summary>
|
|
private async Task SendAsync(string data)
|
|
{
|
|
lock (_outputBuffer)
|
|
{
|
|
_outputBuffer.Append(data);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disconnects the client.
|
|
/// </summary>
|
|
public void Disconnect()
|
|
{
|
|
_isConnected = false;
|
|
_cancellationTokenSource.Cancel();
|
|
_networkStream?.Close();
|
|
_tcpClient?.Close();
|
|
_state = ClientState.Disconnected;
|
|
|
|
OnDisconnect?.Invoke();
|
|
Console.WriteLine("Client disconnected.");
|
|
}
|
|
|
|
// Other methods to handle sending messages, changing status, etc.
|
|
|
|
/// <summary>
|
|
/// Sends a text message to the server.
|
|
/// </summary>
|
|
public async Task SendMessageAsync(string recipientId, string message)
|
|
{
|
|
string messageToSend = $"TM:<ID>{recipientId}</ID><MS>{message}</MS>\r\n";
|
|
await SendAsync(messageToSend);
|
|
Console.WriteLine($"Sent message to {recipientId}: {message}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a status update to the server.
|
|
/// </summary>
|
|
public async Task SendStatusAsync(int status)
|
|
{
|
|
string statusMessage = $"ST:{status}\r\n";
|
|
await SendAsync(statusMessage);
|
|
Console.WriteLine($"Sent status update: {status}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Requests to start transmitting voice data.
|
|
/// </summary>
|
|
public async Task RequestTransmitAsync()
|
|
{
|
|
await SendAsync("TX0\r\n");
|
|
Console.WriteLine("Requested to start transmitting voice data.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents client information received from the server.
|
|
/// </summary>
|
|
public class ClientInfo
|
|
{
|
|
public string Status { get; set; }
|
|
public string Muted { get; set; }
|
|
public string Country { get; set; }
|
|
public string City { get; set; }
|
|
public string BandAndChannel { get; set; }
|
|
public string ClientType { get; set; }
|
|
public string CallsignAndUser { get; set; }
|
|
public string ID { get; set; }
|
|
public string Description { get; set; }
|
|
}
|
|
|
|
// Extension method to find index of a byte in a List<byte>
|
|
public static class Extensions
|
|
{
|
|
public static int IndexOf(this List<byte> buffer, byte value)
|
|
{
|
|
for (int i = 0; i < buffer.Count; i++)
|
|
{
|
|
if (buffer[i] == value)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
}
|