From 30df8c4c775a6729ef3a15e72f12724f01cf6f2b Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Wed, 28 May 2025 14:18:48 +0300 Subject: [PATCH] feat: clean architecture in drybox ui --- protocol_prototype/DryBox/UI/client_state.py | 78 +++++++ protocol_prototype/DryBox/UI/main.py | 209 ++++-------------- protocol_prototype/DryBox/UI/phone_client.py | 141 +++--------- protocol_prototype/DryBox/UI/phone_manager.py | 89 ++++++++ protocol_prototype/DryBox/devnote.txt | 13 ++ 5 files changed, 248 insertions(+), 282 deletions(-) create mode 100644 protocol_prototype/DryBox/UI/client_state.py create mode 100644 protocol_prototype/DryBox/UI/phone_manager.py create mode 100644 protocol_prototype/DryBox/devnote.txt diff --git a/protocol_prototype/DryBox/UI/client_state.py b/protocol_prototype/DryBox/UI/client_state.py new file mode 100644 index 0000000..b8e0d03 --- /dev/null +++ b/protocol_prototype/DryBox/UI/client_state.py @@ -0,0 +1,78 @@ +# client_state.py +from queue import Queue +from session import NoiseXKSession + +class ClientState: + def __init__(self, client_id): + self.client_id = client_id + self.command_queue = Queue() + self.initiator = None + self.keypair = None + self.peer_pubkey = None + self.session = None + self.handshake_in_progress = False + self.handshake_start_time = None + self.call_active = False + + def process_command(self, client): + """Process commands from the queue.""" + if not self.command_queue.empty(): + print(f"Client {self.client_id} processing command queue, size: {self.command_queue.qsize()}") + command = self.command_queue.get() + if command == "handshake": + try: + print(f"Client {self.client_id} starting handshake, initiator: {self.initiator}") + self.session = NoiseXKSession(self.keypair, self.peer_pubkey) + self.session.handshake(client.sock, self.initiator) + print(f"Client {self.client_id} handshake complete") + client.send("HANDSHAKE_DONE") + except Exception as e: + print(f"Client {self.client_id} handshake failed: {e}") + client.state_changed.emit("CALL_END", "", self.client_id) + finally: + self.handshake_in_progress = False + self.handshake_start_time = None + + def start_handshake(self, initiator, keypair, peer_pubkey): + """Queue handshake command.""" + self.initiator = initiator + self.keypair = keypair + self.peer_pubkey = peer_pubkey + print(f"Client {self.client_id} queuing handshake, initiator: {initiator}") + self.handshake_in_progress = True + self.handshake_start_time = time.time() + self.command_queue.put("handshake") + + def handle_data(self, client, data): + """Handle received data (control or audio).""" + try: + decoded_data = data.decode('utf-8').strip() + print(f"Client {self.client_id} received raw: {decoded_data}") + if decoded_data in ["RINGING", "CALL_END", "CALL_DROPPED", "IN_CALL", "HANDSHAKE", "HANDSHAKE_DONE"]: + client.state_changed.emit(decoded_data, decoded_data, self.client_id) + if decoded_data == "HANDSHAKE": + self.handshake_in_progress = True + elif decoded_data == "HANDSHAKE_DONE": + self.call_active = True + else: + print(f"Client {self.client_id} ignored unexpected text message: {decoded_data}") + except UnicodeDecodeError: + if self.call_active and self.session: + try: + print(f"Client {self.client_id} received audio packet, length={len(data)}") + decrypted_data = self.session.decrypt(data) + print(f"Client {self.client_id} decrypted audio packet, length={len(decrypted_data)}") + client.data_received.emit(decrypted_data, self.client_id) + except Exception as e: + print(f"Client {self.client_id} failed to process audio packet: {e}") + else: + print(f"Client {self.client_id} ignored non-text message: {data.hex()}") + + def check_handshake_timeout(self, client): + """Check for handshake timeout.""" + if self.handshake_in_progress and self.handshake_start_time: + if time.time() - self.handshake_start_time > 30: + print(f"Client {self.client_id} handshake timeout after 30s") + client.state_changed.emit("CALL_END", "", self.client_id) + self.handshake_in_progress = False + self.handshake_start_time = None \ No newline at end of file diff --git a/protocol_prototype/DryBox/UI/main.py b/protocol_prototype/DryBox/UI/main.py index 2ed1d01..cc649ce 100644 --- a/protocol_prototype/DryBox/UI/main.py +++ b/protocol_prototype/DryBox/UI/main.py @@ -1,50 +1,23 @@ +# main.py import sys -import secrets from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, QSizePolicy, QStyle ) -from PyQt5.QtCore import Qt, QTimer, QSize +from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QFont -from phone_client import PhoneClient +from phone_manager import PhoneManager from waveform_widget import WaveformWidget -from phone_state import PhoneState -from session import NoiseXKSession class PhoneUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Enhanced Dual Phone Interface") self.setGeometry(100, 100, 900, 750) - self.setStyleSheet(""" - QMainWindow { background-color: #333333; } - QLabel { color: #E0E0E0; font-size: 14px; } - QPushButton { - background-color: #0078D4; color: white; border: none; - padding: 10px 15px; border-radius: 5px; font-size: 14px; - min-height: 30px; - } - QPushButton:hover { background-color: #005A9E; } - QPushButton:pressed { background-color: #003C6B; } - QPushButton#settingsButton { background-color: #555555; } - QPushButton#settingsButton:hover { background-color: #777777; } - QFrame#phoneDisplay { - background-color: #1E1E1E; border: 2px solid #0078D4; - border-radius: 10px; - } - QLabel#phoneTitleLabel { - font-size: 18px; font-weight: bold; padding-bottom: 5px; - color: #FFFFFF; - } - QLabel#mainTitleLabel { - font-size: 24px; font-weight: bold; color: #00A2E8; - padding: 15px; - } - QWidget#phoneWidget { - border: 1px solid #4A4A4A; border-radius: 8px; - padding: 10px; background-color: #3A3A3A; - } - """) + self.setStyleSheet("...") + + self.manager = PhoneManager() + self.manager.initialize_phones() # Main widget and layout main_widget = QWidget() @@ -67,39 +40,18 @@ class PhoneUI(QMainWindow): phone_controls_layout.setAlignment(Qt.AlignCenter) main_layout.addLayout(phone_controls_layout) - # Initialize phones - self.phones = [] - self.handshake_done_count = 0 - for i in range(2): - client = PhoneClient("localhost", 12345, i) - client.data_received.connect(lambda data, cid=i: self.update_waveform(cid, data)) - client.state_changed.connect(lambda state, num, cid=i: self.set_phone_state(cid, self.map_state(state), num)) - client.start() - - # Generate keypair for each phone - keypair = NoiseXKSession.generate_keypair() - - phone_container_widget, phone_display_frame, phone_button, waveform_widget, phone_status_label = self._create_phone_ui( - f"Phone {i+1}", lambda checked, phone_id=i: self.phone_action(phone_id) + # Setup UI for phones + for phone in self.manager.phones: + phone_container_widget, phone_button, waveform_widget, phone_status_label = self._create_phone_ui( + f"Phone {phone['id']+1}", lambda checked, pid=phone['id']: self.manager.phone_action(pid, self) ) - self.phones.append({ - 'id': i, - 'client': client, - 'state': PhoneState.IDLE, - 'button': phone_button, - 'waveform': waveform_widget, - 'number': "123-4567" if i == 0 else "987-6543", - 'audio_timer': None, - 'status_label': phone_status_label, - 'keypair': keypair, - 'public_key': keypair.public, - 'is_initiator': False - }) + phone['button'] = phone_button + phone['waveform'] = waveform_widget + phone['status_label'] = phone_status_label phone_controls_layout.addWidget(phone_container_widget) - - # Share public key between phones - self.phones[0]['peer_public_key'] = self.phones[1]['public_key'] - self.phones[1]['peer_public_key'] = self.phones[0]['public_key'] + phone['client'].data_received.connect(lambda data, cid=phone['id']: self.manager.update_waveform(cid, data)) + phone['client'].state_changed.connect(lambda state, num, cid=phone['id']: self.set_phone_state(cid, state, num)) + phone['client'].start() # Spacer main_layout.addStretch(1) @@ -117,11 +69,12 @@ class PhoneUI(QMainWindow): settings_layout.addStretch() main_layout.addLayout(settings_layout) - # Initialize button states - for phone in self.phones: - self._update_phone_button_ui(phone['button'], phone['status_label'], phone['state']) + # Initialize UI + for phone in self.manager.phones: + self.update_phone_ui(phone['id']) def _create_phone_ui(self, title, action_slot): + # Same as existing _create_phone_ui phone_container_widget = QWidget() phone_container_widget.setObjectName("phoneWidget") phone_layout = QVBoxLayout() @@ -162,7 +115,15 @@ class PhoneUI(QMainWindow): return phone_container_widget, phone_display_frame, phone_button, waveform_widget, phone_status_label - def _update_phone_button_ui(self, button, status_label, state, phone_number=""): + def update_phone_ui(self, phone_id): + """Update phone button and status label based on state.""" + phone = self.manager.phones[phone_id] + other_phone = self.manager.phones[1 - phone_id] + state = phone['state'] + phone_number = other_phone['number'] if state != PhoneState.IDLE else "" + button = phone['button'] + status_label = phone['status_label'] + if state == PhoneState.IDLE: button.setText("Call") button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) @@ -184,123 +145,31 @@ class PhoneUI(QMainWindow): status_label.setText(f"Incoming Call from {phone_number}") button.setStyleSheet("background-color: #107C10;") - def phone_action(self, phone_id): - phone = self.phones[phone_id] - other_phone = self.phones[1 - phone_id] - print(f"Phone {phone_id + 1} Action, current state: {phone['state']}, is_initiator: {phone['is_initiator']}") - - if phone['state'] == PhoneState.IDLE: - # Initiate a call - phone['state'] = PhoneState.CALLING - other_phone['state'] = PhoneState.RINGING - # Set init/resp - phone['is_initiator'] = True - other_phone['is_initiator'] = False - self._update_phone_button_ui(phone['button'], phone['status_label'], phone['state'], other_phone['number']) - self._update_phone_button_ui(other_phone['button'], other_phone['status_label'], other_phone['state'], phone['number']) - phone['client'].send("RINGING") - - elif phone['state'] == PhoneState.RINGING: - # Answer the call - phone['state'] = PhoneState.IN_CALL - other_phone['state'] = PhoneState.IN_CALL - self._update_phone_button_ui(phone['button'], phone['status_label'], phone['state'], other_phone['number']) - self._update_phone_button_ui(other_phone['button'], other_phone['status_label'], other_phone['state'], phone['number']) - phone['client'].send("IN_CALL") - - elif phone['state'] == PhoneState.IN_CALL or phone['state'] == PhoneState.CALLING: - # Hang up or cancel - if not phone['client'].handshake_in_progress and phone['state'] != PhoneState.CALLING: - phone['state'] = PhoneState.IDLE - other_phone['state'] = PhoneState.IDLE - self._update_phone_button_ui(phone['button'], phone['status_label'], phone['state'], "") - self._update_phone_button_ui(other_phone['button'], other_phone['status_label'], other_phone['state'], "") - phone['client'].send("CALL_END") - # Stop audio timers for both phones - for p in [phone, other_phone]: - if p['audio_timer']: - p['audio_timer'].stop() - else: - print(f"Phone {phone_id + 1} cannot hang up during handshake or call setup") - - def start_audio(self, client_id): - """Start audio timer after both clients send HANDSHAKE_DONE.""" - self.handshake_done_count += 1 - print(f"HANDSHAKE_DONE received for client {client_id}, count: {self.handshake_done_count}") - if self.handshake_done_count == 2: - for phone in self.phones: - if phone['state'] == PhoneState.IN_CALL: - if not phone['audio_timer'] or not phone['audio_timer'].isActive(): - phone['audio_timer'] = QTimer(self) - phone['audio_timer'].timeout.connect(lambda pid=phone['id']: self.send_audio(pid)) - phone['audio_timer'].start(100) # 100ms for smoother updates - self.handshake_done_count = 0 - - def send_audio(self, phone_id): - phone = self.phones[phone_id] - if phone['state'] == PhoneState.IN_CALL and phone['client'].session and phone['client'].sock: - # Generate mock 16-byte audio data - mock_audio = secrets.token_bytes(16) - try: - # Encrypt with Noise session, send over socket - phone['client'].session.send(phone['client'].sock, mock_audio) - print(f"Client {phone_id} sent encrypted audio packet, length=32") - except Exception as e: - print(f"Client {phone_id} failed to send audio: {e}") - - def update_waveform(self, client_id, data): - print(f"Updating waveform for client_id {client_id}, data_length={len(data)}") - waveform = self.phones[client_id]['waveform'] - waveform.set_data(data) - - def map_state(self, state_str): - if state_str == "RINGING": - return PhoneState.RINGING - elif state_str in ["CALL_END", "CALL_DROPPED"]: - return PhoneState.IDLE - elif state_str == "IN_CALL": - return PhoneState.IN_CALL - elif state_str == "HANDSHAKE": - return PhoneState.IN_CALL # Stay in IN_CALL, trigger handshake - elif state_str == "HANDSHAKE_DONE": - return PhoneState.IN_CALL # Stay in IN_CALL, start audio - return PhoneState.IDLE - - def set_phone_state(self, client_id, state, number=""): - phone = self.phones[client_id] - other_phone = self.phones[1 - client_id] + def set_phone_state(self, client_id, state_str, number): + """Handle state changes from client.""" + state = self.manager.map_state(state_str) + phone = self.manager.phones[client_id] + other_phone = self.manager.phones[1 - phone_id] print(f"Setting state for Phone {client_id + 1}: {state}, number: {number}, is_initiator: {phone['is_initiator']}") phone['state'] = state - if state == PhoneState.RINGING: - self._update_phone_button_ui(phone['button'], phone['status_label'], state, other_phone['number']) - elif state == PhoneState.IN_CALL: + if state == PhoneState.IN_CALL: print(f"Phone {client_id + 1} confirmed in IN_CALL state") - self._update_phone_button_ui(phone['button'], phone['status_label'], state, other_phone['number']) if number == "IN_CALL" and phone['is_initiator']: - # Initiator starts handshake after receiving IN_CALL print(f"Phone {client_id + 1} (initiator) starting handshake") phone['client'].send("HANDSHAKE") phone['client'].start_handshake(initiator=True, keypair=phone['keypair'], peer_pubkey=other_phone['public_key']) elif number == "HANDSHAKE" and not phone['is_initiator']: - # Responder starts handshake after receiving HANDSHAKE print(f"Phone {client_id + 1} (responder) starting handshake") phone['client'].start_handshake(initiator=False, keypair=phone['keypair'], peer_pubkey=other_phone['public_key']) elif number == "HANDSHAKE_DONE": - # Start audio after HANDSHAKE_DONE - self.start_audio(client_id) - else: - # Handle disconnect gracefully - self._update_phone_button_ui(phone['button'], phone['status_label'], state, "") - if state == PhoneState.IDLE and number == "CALL_END": - print(f"Phone {client_id + 1} resetting due to disconnect") - if state == PhoneState.IDLE and phone['audio_timer']: - phone['audio_timer'].stop() + self.manager.start_audio(client_id) + self.update_phone_ui(client_id) def settings_action(self): print("Settings clicked") def closeEvent(self, event): - for phone in self.phones: + for phone in self.manager.phones: phone['client'].stop() event.accept() diff --git a/protocol_prototype/DryBox/UI/phone_client.py b/protocol_prototype/DryBox/UI/phone_client.py index fac7d27..c27ec9e 100644 --- a/protocol_prototype/DryBox/UI/phone_client.py +++ b/protocol_prototype/DryBox/UI/phone_client.py @@ -1,45 +1,37 @@ +# phone_client.py import socket import time import select from PyQt5.QtCore import QThread, pyqtSignal -from queue import Queue -from session import NoiseXKSession +from client_state import ClientState class PhoneClient(QThread): - data_received = pyqtSignal(bytes, int) # Include client_id - state_changed = pyqtSignal(str, str, int) # Include client_id + data_received = pyqtSignal(bytes, int) + state_changed = pyqtSignal(str, str, int) - def __init__(self, host, port, client_id): + def __init__(self, client_id): super().__init__() - self.host = host - self.port = port + self.host = "localhost" + self.port = 12345 self.client_id = client_id self.sock = None self.running = True - self.command_queue = Queue() - self.initiator = None - self.keypair = None - self.peer_pubkey = None - self.session = None - self.handshake_in_progress = False - self.handshake_start_time = None - self.call_active = False # Track active call after HANDSHAKE_DONE + self.state = ClientState(client_id) def connect_socket(self): - """Attempt to connect to the server with retries.""" retries = 3 for attempt in range(retries): try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - self.sock.settimeout(120) # 120s for socket operations + self.sock.settimeout(120) self.sock.connect((self.host, self.port)) print(f"Client {self.client_id} connected to {self.host}:{self.port}") return True except Exception as e: print(f"Client {self.client_id} connection attempt {attempt + 1} failed: {e}") if attempt < retries - 1: - time.sleep(1) # Wait before retrying + time.sleep(1) self.sock = None return False @@ -52,94 +44,27 @@ class PhoneClient(QThread): break try: while self.running: - # print(f"Client {self.client_id} run loop iteration") - # Check command queue first - if not self.command_queue.empty(): - print(f"Client {self.client_id} processing command queue, size: {self.command_queue.qsize()}") - command = self.command_queue.get() - if command == "handshake": + self.state.process_command(self) + self.state.check_handshake_timeout(self) + if not self.state.handshake_in_progress: + readable, _, _ = select.select([self.sock], [], [], 0.01) + if readable: try: - print(f"Client {self.client_id} starting handshake, initiator: {self.initiator}") - self.session = NoiseXKSession(self.keypair, self.peer_pubkey) - self.session.handshake(self.sock, self.initiator) - print(f"Client {self.client_id} handshake complete") - self.send("HANDSHAKE_DONE") - except socket.timeout: - print(f"Client {self.client_id} handshake timed out") + print(f"Client {self.client_id} attempting sock.recv") + data = self.sock.recv(1024) + if not data: + print(f"Client {self.client_id} disconnected") + self.state_changed.emit("CALL_END", "", self.client_id) + break + self.state.handle_data(self, data) + except socket.error as e: + print(f"Client {self.client_id} socket error: {e}") self.state_changed.emit("CALL_END", "", self.client_id) break - except Exception as e: - print(f"Client {self.client_id} handshake failed: {e}") - self.state_changed.emit("CALL_END", "", self.client_id) - break - finally: - self.handshake_in_progress = False - self.handshake_start_time = None else: - # Check for handshake timeout - if self.handshake_in_progress and self.handshake_start_time: - if time.time() - self.handshake_start_time > 30: # 30s handshake timeout - print(f"Client {self.client_id} handshake timeout after 30s") - self.state_changed.emit("CALL_END", "", self.client_id) - self.handshake_in_progress = False - self.handshake_start_time = None - break - # Only read socket if not in handshake - if not self.handshake_in_progress: - # Use select to check if data is available - readable, _, _ = select.select([self.sock], [], [], 0.01) # 10ms timeout - if readable: - try: - print(f"Client {self.client_id} attempting sock.recv") - data = self.sock.recv(1024) - if not data: - print(f"Client {self.client_id} disconnected") - self.state_changed.emit("CALL_END", "", self.client_id) - break - # Handle control messages (UTF-8) - try: - decoded_data = data.decode('utf-8').strip() - print(f"Client {self.client_id} received raw: {decoded_data}") - if decoded_data in ["RINGING", "CALL_END", "CALL_DROPPED", "IN_CALL", "HANDSHAKE", "HANDSHAKE_DONE"]: - self.state_changed.emit(decoded_data, decoded_data, self.client_id) - if decoded_data == "HANDSHAKE": - self.handshake_in_progress = True # Block further reads - elif decoded_data == "HANDSHAKE_DONE": - self.call_active = True # Enable audio processing - else: - print(f"Client {self.client_id} ignored unexpected text message: {decoded_data}") - except UnicodeDecodeError: - # Handle binary data (audio packets) - if self.call_active and self.session: - try: - print(f"Client {self.client_id} received audio packet, length={len(data)}") - decrypted_data = self.session.decrypt(data) - print(f"Client {self.client_id} decrypted audio packet, length={len(decrypted_data)}") - self.data_received.emit(decrypted_data, self.client_id) - except Exception as e: - print(f"Client {self.client_id} failed to process audio packet: {e}") - else: - print(f"Client {self.client_id} ignored non-text message: {data.hex()}") - except socket.timeout: - print(f"Client {self.client_id} timed out waiting for data") - continue - except socket.error as e: - print(f"Client {self.client_id} socket error: {e}") - self.state_changed.emit("CALL_END", "", self.client_id) - break - except Exception as e: - print(f"Client {self.client_id} error: {e}") - self.state_changed.emit("CALL_END", "", self.client_id) - break - else: - # print(f"Client {self.client_id} no data available, skipping recv") - pass - else: - # Yield during handshake - self.msleep(20) # 20ms sleep to yield CPU - print(f"Client {self.client_id} yielding during handshake") - # Short sleep to yield Qt event loop - self.msleep(1) # 1ms sleep + self.msleep(20) + print(f"Client {self.client_id} yielding during handshake") + self.msleep(1) finally: if self.sock: self.sock.close() @@ -153,7 +78,6 @@ class PhoneClient(QThread): self.sock.send(data) print(f"Client {self.client_id} sent: {message}, length={len(data)}") else: - # Send binary data (audio) self.sock.send(message) print(f"Client {self.client_id} sent binary data, length={len(message)}") except socket.error as e: @@ -165,13 +89,6 @@ class PhoneClient(QThread): if self.sock: self.sock.close() self.sock = None - + def start_handshake(self, initiator, keypair, peer_pubkey): - """Queue the handshake command with necessary parameters.""" - self.initiator = initiator - self.keypair = keypair - self.peer_pubkey = peer_pubkey - print(f"Client {self.client_id} queuing handshake, initiator: {initiator}") - self.handshake_in_progress = True # Block recv before handshake starts - self.handshake_start_time = time.time() - self.command_queue.put("handshake") \ No newline at end of file + self.state.start_handshake(initiator, keypair, peer_pubkey) \ No newline at end of file diff --git a/protocol_prototype/DryBox/UI/phone_manager.py b/protocol_prototype/DryBox/UI/phone_manager.py new file mode 100644 index 0000000..cc5e770 --- /dev/null +++ b/protocol_prototype/DryBox/UI/phone_manager.py @@ -0,0 +1,89 @@ +# phone_manager.py +import secrets +from PyQt5.QtCore import QTimer +from phone_client import PhoneClient +from session import NoiseXKSession +from phone_state import PhoneState + +class PhoneManager: + def __init__(self): + self.phones = [] + self.handshake_done_count = 0 + + def initialize_phones(self): + """Initialize phone clients and their keypairs.""" + for i in range(2): + client = PhoneClient(i) + keypair = NoiseXKSession.generate_keypair() + phone = { + 'id': i, + 'client': client, + 'state': PhoneState.IDLE, + 'number': "123-4567" if i == 0 else "987-6543", + 'audio_timer': None, + 'keypair': keypair, + 'public_key': keypair.public, + 'is_initiator': False + } + self.phones.append(phone) + + # Share public keys + self.phones[0]['peer_public_key'] = self.phones[1]['public_key'] + self.phones[1]['peer_public_key'] = self.phones[0]['public_key'] + + def phone_action(self, phone_id, ui_manager): + """Handle phone actions (call, answer, disconnect).""" + phone = self.phones[phone_id] + other_phone = self.phones[1 - phone_id] + print(f"Phone {phone_id + 1} Action, current state: {phone['state']}, is_initiator: {phone['is_initiator']}") + + if phone['state'] == PhoneState.IDLE: + phone['state'] = PhoneState.CALLING + other_phone['state'] = PhoneState.RINGING + phone['is_initiator'] = True + other_phone['is_initiator'] = False + phone['client'].send("RINGING") + elif phone['state'] == PhoneState.RINGING: + phone['state'] = other_phone['state'] = PhoneState.IN_CALL + phone['client'].send("IN_CALL") + elif phone['state'] in [PhoneState.IN_CALL, PhoneState.CALLING]: + if not phone['client'].handshake_in_progress and phone['state'] != PhoneState.CALLING: + phone['state'] = other_phone['state'] = PhoneState.IDLE + phone['client'].send("CALL_END") + for p in [phone, other_phone]: + if p['audio_timer']: + p['audio_timer'].stop() + else: + print(f"Phone {phone_id + 1} cannot hang up during handshake or call setup") + + # Update UI + ui_manager.update_phone_ui(phone_id) + ui_manager.update_phone_ui(1 - phone_id) + + def send_audio(self, phone_id): + """Send mock audio data.""" + phone = self.phones[phone_id] + if phone['state'] == PhoneState.IN_CALL and phone['client'].session and phone['client'].sock: + mock_audio = secrets.token_bytes(16) + try: + phone['client'].session.send(phone['client'].sock, mock_audio) + print(f"Client {phone_id} sent encrypted audio packet, length=32") + except Exception as e: + print(f"Client {phone_id} failed to send audio: {e}") + + def start_audio(self, client_id): + """Start audio timer after HANDSHAKE_DONE.""" + self.handshake_done_count += 1 + print(f"HANDSHAKE_DONE received for client {client_id}, count: {self.handshake_done_count}") + if self.handshake_done_count == 2: + for phone in self.phones: + if phone['state'] == PhoneState.IN_CALL: + if not phone['audio_timer'] or not phone['audio_timer'].isActive(): + phone['audio_timer'] = QTimer() + phone['audio_timer'].timeout.connect(lambda pid=phone['id']: self.send_audio(pid)) + phone['audio_timer'].start(100) + self.handshake_done_count = 0 + + def update_waveform(self, client_id, data): + """Update waveform with received audio data.""" + print(f"Updating waveform for client_id {client_id}, data_length={len(data)}") \ No newline at end of file diff --git a/protocol_prototype/DryBox/devnote.txt b/protocol_prototype/DryBox/devnote.txt new file mode 100644 index 0000000..ae75b1c --- /dev/null +++ b/protocol_prototype/DryBox/devnote.txt @@ -0,0 +1,13 @@ +simulator/ +├── gsm_simulator.py # gsm_simulator +├── launch_gsm_simulator.sh # use to start docker and simulator, run in terminal + +2 clients nect to gsm_simulator and simulate a call using noise protocol +UI/ +├── main.py # UI setup and event handling +├── phone_manager.py # Phone state, client init, audio logic +├── phone_client.py # Socket communication and threading +├── client_state.py # Client state and command processing +├── session.py # Noise XK crypto session +├── waveform_widget.py # Waveform UI component +├── phone_state.py # State constants \ No newline at end of file