From 10b44cdf728a4a09eb99ca2de24fa5c4e9e3121b Mon Sep 17 00:00:00 2001 From: Bartosz Date: Mon, 7 Jul 2025 22:07:02 +0100 Subject: [PATCH] add of changes --- protocol_prototype/DryBox/README.md | 81 ++++++++ protocol_prototype/DryBox/UI/main.py | 195 ++++++++++++++---- protocol_prototype/DryBox/run_ui.sh | 11 - .../DryBox/simulator/Dockerfile | 14 -- .../DryBox/simulator/launch_gsm_simulator.sh | 68 ------ 5 files changed, 238 insertions(+), 131 deletions(-) create mode 100644 protocol_prototype/DryBox/README.md delete mode 100755 protocol_prototype/DryBox/run_ui.sh delete mode 100644 protocol_prototype/DryBox/simulator/Dockerfile delete mode 100755 protocol_prototype/DryBox/simulator/launch_gsm_simulator.sh diff --git a/protocol_prototype/DryBox/README.md b/protocol_prototype/DryBox/README.md new file mode 100644 index 0000000..fb94422 --- /dev/null +++ b/protocol_prototype/DryBox/README.md @@ -0,0 +1,81 @@ +# DryBox - Secure Voice Communication System + +A PyQt5-based application demonstrating secure voice communication using the Noise XK protocol, Codec2 audio compression, and 4FSK modulation. + +## Features + +- **Secure Communication**: End-to-end encryption using Noise XK protocol +- **Audio Compression**: Codec2 (3200bps) for efficient voice transmission +- **Modulation**: 4FSK (4-level Frequency Shift Keying) for robust transmission +- **GSM Network Simulation**: Simulates realistic GSM network conditions +- **Real-time Audio**: Playback and recording capabilities +- **Visual Feedback**: Waveform displays and signal strength indicators + +## Requirements + +- Python 3.7+ +- PyQt5 +- NumPy +- pycodec2 +- Additional dependencies in `requirements.txt` + +## Installation + +1. Install system dependencies: + ```bash + ./install_audio_deps.sh + ``` + +2. Install Python dependencies: + ```bash + pip install -r requirements.txt + ``` + +## Running the Application + +Simply run: +```bash +python3 UI/main.py +``` + +The application will automatically: +- Start the GSM network simulator +- Initialize two phone clients +- Display the main UI with GSM status panel + +## Usage + +### Phone Controls +- **Click "Call" button** or press `1`/`2` to initiate/answer calls +- **Ctrl+1/2**: Toggle audio playback for each phone +- **Alt+1/2**: Toggle audio recording for each phone + +### GSM Settings +- **Click "Settings" button** or press `Ctrl+G` to open GSM settings dialog +- Adjust signal strength, quality, noise, and network parameters +- Use presets for quick configuration (Excellent/Good/Fair/Poor) + +### Other Controls +- **Space**: Run automatic test sequence +- **Ctrl+L**: Clear debug console +- **Ctrl+A**: Audio processing options menu + +## Architecture + +- **main.py**: Main UI application +- **phone_manager.py**: Manages phone instances and audio +- **protocol_phone_client.py**: Implements the secure protocol stack +- **noise_wrapper.py**: Noise XK protocol implementation +- **gsm_simulator.py**: Network simulation relay +- **gsm_status_widget.py**: Real-time GSM status display + +## Testing + +The automatic test feature (`Space` key) runs through a complete call sequence: +1. Initial state verification +2. Call initiation +3. Call answering +4. Noise XK handshake +5. Voice session establishment +6. Audio transmission +7. Call termination \ No newline at end of file diff --git a/protocol_prototype/DryBox/UI/main.py b/protocol_prototype/DryBox/UI/main.py index 4092554..03b550d 100644 --- a/protocol_prototype/DryBox/UI/main.py +++ b/protocol_prototype/DryBox/UI/main.py @@ -1,4 +1,7 @@ import sys +import os +import subprocess +import atexit from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, QSizePolicy, QStyle, QTextEdit, QSplitter, @@ -36,6 +39,11 @@ class PhoneUI(QMainWindow): # GSM simulation timer self.gsm_simulation_timer = None + + # GSM simulator process + self.gsm_simulator_process = None + self.start_gsm_simulator() + self.setStyleSheet(""" QMainWindow { background-color: #1a1a1a; @@ -134,9 +142,8 @@ class PhoneUI(QMainWindow): # Setup debug signal early self.debug_signal.connect(self.append_debug) - self.manager = PhoneManager() - self.manager.ui = self # Set UI reference for debug logging - self.manager.initialize_phones() + # Initialize phone manager after simulator starts + QTimer.singleShot(1000, self.initialize_phone_manager) # Main widget with splitter main_widget = QWidget() @@ -179,31 +186,10 @@ class PhoneUI(QMainWindow): phones_layout.addWidget(protocol_info) # Phone displays layout - phone_controls_layout = QHBoxLayout() - phone_controls_layout.setSpacing(20) - phone_controls_layout.setContentsMargins(10, 0, 10, 0) - phones_layout.addLayout(phone_controls_layout) - - # Setup UI for phones - for phone in self.manager.phones: - phone_container_widget, phone_display_frame, phone_button, waveform_widget, sent_waveform_widget, phone_status_label, playback_button, record_button = self._create_phone_ui( - f"Phone {phone['id']+1}", lambda checked, pid=phone['id']: self.manager.phone_action(pid, self) - ) - phone['button'] = phone_button - phone['waveform'] = waveform_widget - phone['sent_waveform'] = sent_waveform_widget - phone['status_label'] = phone_status_label - phone['playback_button'] = playback_button - phone['record_button'] = record_button - - # Connect audio control buttons with proper closure - playback_button.clicked.connect(lambda checked, pid=phone['id']: self.toggle_playback(pid)) - record_button.clicked.connect(lambda checked, pid=phone['id']: self.toggle_recording(pid)) - phone_controls_layout.addWidget(phone_container_widget) - # Connect data_received signal - it emits (data, client_id) - phone['client'].data_received.connect(lambda data, cid: 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() + self.phone_controls_layout = QHBoxLayout() + self.phone_controls_layout.setSpacing(20) + self.phone_controls_layout.setContentsMargins(10, 0, 10, 0) + phones_layout.addLayout(self.phone_controls_layout) # Control buttons layout control_layout = QHBoxLayout() @@ -264,16 +250,124 @@ class PhoneUI(QMainWindow): # Set splitter sizes self.splitter.setSizes([600, 300]) # 70% phones, 30% debug self.main_h_splitter.setSizes([300, 1100]) # GSM panel: 300px, rest: 1100px - - # Initialize UI - for phone in self.manager.phones: - self.update_phone_ui(phone['id']) # Initial debug message QTimer.singleShot(100, lambda: self.debug("DryBox UI initialized with integrated protocol")) # Setup keyboard shortcuts self.setup_shortcuts() + + # Placeholder for manager (will be initialized after simulator starts) + self.manager = None + + def initialize_phone_manager(self): + """Initialize phone manager after GSM simulator is ready""" + self.debug("Initializing phone manager...") + self.manager = PhoneManager() + self.manager.ui = self # Set UI reference for debug logging + self.manager.initialize_phones() + + # Now setup phone UIs + self.setup_phone_uis() + + # Initialize UI + for phone in self.manager.phones: + self.update_phone_ui(phone['id']) + + def setup_phone_uis(self): + """Setup UI for phones after manager is initialized""" + # Find the phone controls layout + phone_controls_layout = self.phone_controls_layout + + # Setup UI for phones + for phone in self.manager.phones: + phone_container_widget, phone_display_frame, phone_button, waveform_widget, sent_waveform_widget, phone_status_label, playback_button, record_button = self._create_phone_ui( + f"Phone {phone['id']+1}", lambda checked, pid=phone['id']: self.manager.phone_action(pid, self) + ) + phone['button'] = phone_button + phone['waveform'] = waveform_widget + phone['sent_waveform'] = sent_waveform_widget + phone['status_label'] = phone_status_label + phone['playback_button'] = playback_button + phone['record_button'] = record_button + + # Connect audio control buttons with proper closure + playback_button.clicked.connect(lambda checked, pid=phone['id']: self.toggle_playback(pid)) + record_button.clicked.connect(lambda checked, pid=phone['id']: self.toggle_recording(pid)) + phone_controls_layout.addWidget(phone_container_widget) + # Connect data_received signal - it emits (data, client_id) + phone['client'].data_received.connect(lambda data, cid: 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() + + def start_gsm_simulator(self): + """Start the GSM simulator as a subprocess""" + try: + # First, try to kill any existing GSM simulator + try: + subprocess.run(['pkill', '-f', 'gsm_simulator.py'], capture_output=True) + time.sleep(0.5) # Give it time to shut down + except: + pass # Ignore if pkill fails + + # Get the path to the simulator script + current_dir = os.path.dirname(os.path.abspath(__file__)) + simulator_path = os.path.join(os.path.dirname(current_dir), 'simulator', 'gsm_simulator.py') + + if not os.path.exists(simulator_path): + self.debug(f"ERROR: GSM simulator not found at {simulator_path}") + return + + # Start the simulator process + self.gsm_simulator_process = subprocess.Popen( + [sys.executable, simulator_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + bufsize=1 + ) + + # Start thread to read simulator output + simulator_thread = threading.Thread( + target=self.read_simulator_output, + daemon=True + ) + simulator_thread.start() + + # Give simulator time to start + time.sleep(0.5) + self.debug("GSM simulator started successfully") + + except Exception as e: + self.debug(f"ERROR: Failed to start GSM simulator: {e}") + + def read_simulator_output(self): + """Read output from GSM simulator subprocess""" + if self.gsm_simulator_process: + while True: + line = self.gsm_simulator_process.stdout.readline() + if not line: + break + line = line.strip() + if line: + self.debug(f"[GSM Simulator] {line}") + + # Check for errors + stderr = self.gsm_simulator_process.stderr.read() + if stderr: + self.debug(f"[GSM Simulator ERROR] {stderr}") + + def stop_gsm_simulator(self): + """Stop the GSM simulator subprocess""" + if self.gsm_simulator_process: + self.debug("Stopping GSM simulator...") + self.gsm_simulator_process.terminate() + try: + self.gsm_simulator_process.wait(timeout=2) + except subprocess.TimeoutExpired: + self.gsm_simulator_process.kill() + self.gsm_simulator_process = None + self.debug("GSM simulator stopped") def _create_phone_ui(self, title, action_slot): phone_container_widget = QWidget() @@ -409,6 +503,8 @@ class PhoneUI(QMainWindow): return phone_container_widget, phone_display_frame, phone_button, waveform_widget, sent_waveform_widget, phone_status_label, playback_button, record_button def update_phone_ui(self, phone_id): + if not self.manager: + return phone = self.manager.phones[phone_id] other_phone = self.manager.phones[1 - phone_id] state = phone['state'] @@ -484,6 +580,9 @@ class PhoneUI(QMainWindow): def set_phone_state(self, client_id, state_str, number): self.debug(f"Phone {client_id + 1} state change: {state_str}") + if not self.manager: + return + # Handle protocol-specific states if state_str == "HANDSHAKE_COMPLETE": phone = self.manager.phones[client_id] @@ -571,7 +670,7 @@ class PhoneUI(QMainWindow): def apply_gsm_settings(self): """Apply GSM settings to the phone simulation""" - if self.gsm_settings: + if self.gsm_settings and self.manager: # Here you can apply the settings to your phone manager or simulation # For example, update signal quality indicators, adjust codec parameters, etc. self.debug("Applying GSM settings to simulation...") @@ -654,6 +753,10 @@ class PhoneUI(QMainWindow): def execute_test_step(self): """Execute next step in test sequence""" + if not self.manager: + self.debug("Test step skipped - manager not initialized") + return + phone1 = self.manager.phones[0] phone2 = self.manager.phones[1] @@ -775,6 +878,8 @@ class PhoneUI(QMainWindow): def toggle_playback(self, phone_id): """Toggle audio playback for a phone""" + if not self.manager: + return is_enabled = self.manager.toggle_playback(phone_id) phone = self.manager.phones[phone_id] phone['playback_button'].setChecked(is_enabled) @@ -786,6 +891,8 @@ class PhoneUI(QMainWindow): def toggle_recording(self, phone_id): """Toggle audio recording for a phone""" + if not self.manager: + return is_recording, save_path = self.manager.toggle_recording(phone_id) phone = self.manager.phones[phone_id] phone['record_button'].setChecked(is_recording) @@ -848,6 +955,8 @@ class PhoneUI(QMainWindow): def export_audio_buffer(self, phone_id): """Export audio buffer for a phone""" + if not self.manager: + return save_path = self.manager.export_buffered_audio(phone_id) if save_path: self.debug(f"Phone {phone_id + 1}: Audio buffer exported to {save_path}") @@ -856,10 +965,14 @@ class PhoneUI(QMainWindow): def clear_audio_buffer(self, phone_id): """Clear audio buffer for a phone""" + if not self.manager: + return self.manager.clear_audio_buffer(phone_id) def process_audio(self, phone_id, processing_type): """Process audio with specified type""" + if not self.manager: + return save_path = self.manager.process_audio(phone_id, processing_type) if save_path: self.debug(f"Phone {phone_id + 1}: Processed audio saved to {save_path}") @@ -868,6 +981,8 @@ class PhoneUI(QMainWindow): def apply_gain_dialog(self, phone_id): """Show dialog to get gain value""" + if not self.manager: + return gain, ok = QInputDialog.getDouble( self, "Apply Gain", "Enter gain in dB:", 0.0, -20.0, 20.0, 1 @@ -880,12 +995,12 @@ class PhoneUI(QMainWindow): def setup_shortcuts(self): """Setup keyboard shortcuts""" # Phone 1 shortcuts - QShortcut(QKeySequence("1"), self, lambda: self.manager.phone_action(0, self)) + QShortcut(QKeySequence("1"), self, lambda: self.manager.phone_action(0, self) if self.manager else None) QShortcut(QKeySequence("Ctrl+1"), self, lambda: self.toggle_playback(0)) QShortcut(QKeySequence("Alt+1"), self, lambda: self.toggle_recording(0)) # Phone 2 shortcuts - QShortcut(QKeySequence("2"), self, lambda: self.manager.phone_action(1, self)) + QShortcut(QKeySequence("2"), self, lambda: self.manager.phone_action(1, self) if self.manager else None) QShortcut(QKeySequence("Ctrl+2"), self, lambda: self.toggle_playback(1)) QShortcut(QKeySequence("Alt+2"), self, lambda: self.toggle_recording(1)) @@ -966,6 +1081,9 @@ class PhoneUI(QMainWindow): # Stop GSM simulation self.stop_gsm_simulation() + # Stop GSM simulator process + self.stop_gsm_simulator() + # Clean up GSM settings dialog if self.gsm_settings_dialog is not None: self.gsm_settings_dialog.close() @@ -973,12 +1091,13 @@ class PhoneUI(QMainWindow): self.gsm_settings_dialog = None # Clean up audio player - if hasattr(self.manager, 'audio_player'): + if self.manager and hasattr(self.manager, 'audio_player'): self.manager.audio_player.cleanup() # Stop all phone clients - for phone in self.manager.phones: - phone['client'].stop() + if self.manager: + for phone in self.manager.phones: + phone['client'].stop() event.accept() diff --git a/protocol_prototype/DryBox/run_ui.sh b/protocol_prototype/DryBox/run_ui.sh deleted file mode 100755 index 2b3beb1..0000000 --- a/protocol_prototype/DryBox/run_ui.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Run DryBox UI with proper Wayland support on Fedora - -cd "$(dirname "$0")" - -# Use native Wayland if available -export QT_QPA_PLATFORM=wayland - -# Run the UI -cd UI -python3 main.py \ No newline at end of file diff --git a/protocol_prototype/DryBox/simulator/Dockerfile b/protocol_prototype/DryBox/simulator/Dockerfile deleted file mode 100644 index ff26191..0000000 --- a/protocol_prototype/DryBox/simulator/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -# Use official Python image -FROM python:3.9-slim - -# Set working directory -WORKDIR /app - -# Copy the simulator script -COPY gsm_simulator.py . - -# Expose the port -EXPOSE 12345 - -# Run the simulator -CMD ["python", "gsm_simulator.py"] \ No newline at end of file diff --git a/protocol_prototype/DryBox/simulator/launch_gsm_simulator.sh b/protocol_prototype/DryBox/simulator/launch_gsm_simulator.sh deleted file mode 100755 index 74971d1..0000000 --- a/protocol_prototype/DryBox/simulator/launch_gsm_simulator.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# Script to launch the GSM Simulator in Docker - -# Variables -IMAGE_NAME="gsm-simulator" -CONTAINER_NAME="gsm-sim" -PORT="12345" -LOG_FILE="gsm_simulator.log" - -# Check if Docker is installed -if ! command -v docker &> /dev/null; then - echo "Error: Docker is not installed. Please install Docker and try again." - exit 1 -fi - -# Check if gsm_simulator.py exists -if [ ! -f "gsm_simulator.py" ]; then - echo "Error: gsm_simulator.py not found in the current directory." - echo "Please ensure gsm_simulator.py is present and try again." - exit 1 -fi - -# Create Dockerfile if it doesn't exist -if [ ! -f "Dockerfile" ]; then - echo "Creating Dockerfile..." - cat < Dockerfile -FROM python:3.9-slim -WORKDIR /app -COPY gsm_simulator.py . -EXPOSE 12345 -CMD ["python", "gsm_simulator.py"] -EOF -fi - -# Ensure log file is writable -touch $LOG_FILE -chmod 666 $LOG_FILE - -# Build the Docker image -echo "Building Docker image: $IMAGE_NAME..." -docker build -t $IMAGE_NAME . - -# Check if the build was successful -if [ $? -ne 0 ]; then - echo "Error: Failed to build Docker image." - exit 1 -fi - -# Stop and remove any existing container -if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then - echo "Stopping existing container: $CONTAINER_NAME..." - docker stop $CONTAINER_NAME -fi -if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then - echo "Removing existing container: $CONTAINER_NAME..." - docker rm $CONTAINER_NAME -fi - -# Clean up dangling images -docker image prune -f - -# Run the Docker container interactively -echo "Launching GSM Simulator in Docker container: $CONTAINER_NAME..." -docker run -it --rm -p $PORT:$PORT --name $CONTAINER_NAME $IMAGE_NAME | tee $LOG_FILE - -# Note: Script will block here until container exits -echo "GSM Simulator stopped. Logs saved to $LOG_FILE." \ No newline at end of file