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"; } /// /// Represents a Free Radio Network client. /// 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 _inputBuffer = new List(); 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 OnMessageReceived; public event Action OnVoiceDataReceived; public event Action> 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(); /// /// Initializes a new instance of the FRNClient class. /// 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; } /// /// Starts the client and connects to the server. /// public async Task RunAsync() { await ConnectAsync(); if (_isConnected) { await ProcessAsync(_cancellationTokenSource.Token); } } /// /// Connects to the FRN server. /// 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(); } } /// /// Processes incoming and outgoing data. /// 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}"); } } /// /// Reads data from the network stream. /// 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); } } } /// /// Parses incoming data from the server. /// 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; } } } } /// /// Handles the 'DT_VOICE_BUFFER' data type. /// 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 } } /// /// Handles the 'DT_DO_TX' data type. /// 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 } } /// /// Handles the 'DT_IDLE' data type. /// 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); } } } /// /// Parses incoming data for login response. /// 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(); 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($"", 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(); } } /// /// Writes data to the network stream. /// 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); } } } /// /// Sends periodic keep-alive messages to the server. /// private async Task KeepAliveAsync(CancellationToken cancellationToken) { while (_isConnected && !cancellationToken.IsCancellationRequested) { if ((DateTime.Now - _lastKeepAlive).TotalSeconds > Constants.KeepAliveTimeout) { await SendKeepAliveAsync(); } await Task.Delay(500, cancellationToken); } } /// /// Sends the initial login message to the server. /// private async Task SendLoginAsync() { string loginMessage = BuildLoginMessage(); await SendAsync(loginMessage); _state = ClientState.LoginPhase1; } /// /// Builds the login message. /// private string BuildLoginMessage() { var sb = new StringBuilder(); sb.Append("CT:"); sb.Append($"{Constants.FRNProtocolVersion}"); sb.Append($"{_email}"); sb.Append($"{_password}"); sb.Append($"{_callSign}"); sb.Append($"0"); sb.Append($"{_type}"); // Adjusted as per documentation sb.Append($"{_description}"); sb.Append($"{_country}"); sb.Append($"{_city} - {_locator}"); sb.Append($"{_network}"); sb.Append("\r\n"); #if DEBUG Console.WriteLine($"Built login message: {sb}"); #endif return sb.ToString(); } /// /// Sends a keep-alive message to the server. /// private async Task SendKeepAliveAsync() { await SendAsync("P\r\n"); _lastKeepAlive = DateTime.Now; #if DEBUG Console.WriteLine("Sent keep-alive to server."); #endif } /// /// Sends data to the server. /// private async Task SendAsync(string data) { lock (_outputBuffer) { _outputBuffer.Append(data); } } /// /// Disconnects the client. /// 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. /// /// Sends a text message to the server. /// public async Task SendMessageAsync(string recipientId, string message) { string messageToSend = $"TM:{recipientId}{message}\r\n"; await SendAsync(messageToSend); Console.WriteLine($"Sent message to {recipientId}: {message}"); } /// /// Sends a status update to the server. /// public async Task SendStatusAsync(int status) { string statusMessage = $"ST:{status}\r\n"; await SendAsync(statusMessage); Console.WriteLine($"Sent status update: {status}"); } /// /// Requests to start transmitting voice data. /// public async Task RequestTransmitAsync() { await SendAsync("TX0\r\n"); Console.WriteLine("Requested to start transmitting voice data."); } } /// /// Represents client information received from the server. /// 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 public static class Extensions { public static int IndexOf(this List buffer, byte value) { for (int i = 0; i < buffer.Count; i++) { if (buffer[i] == value) return i; } return -1; } } }