add of changes
All checks were successful
/ mirror (push) Successful in 5s

This commit is contained in:
Bartosz 2025-07-07 22:07:02 +01:00
parent 4cc9e8b2d2
commit 10b44cdf72
5 changed files with 238 additions and 131 deletions

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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"]

View File

@ -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 <<EOF > 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."