frnsharp/sharpFRN/FRNClient.cs

676 lines
22 KiB
C#
Raw Permalink Normal View History

2024-11-22 23:55:57 +08:00
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;
}
}
}