This commit is contained in:
parent
4cc9e8b2d2
commit
10b44cdf72
81
protocol_prototype/DryBox/README.md
Normal file
81
protocol_prototype/DryBox/README.md
Normal 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
|
@ -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()
|
||||
|
||||
|
@ -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
|
@ -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"]
|
@ -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."
|
Loading…
Reference in New Issue
Block a user