parent
5c274817df
commit
a14084ce68
@ -1,164 +1,78 @@
|
|||||||
# DryBox Integration Status
|
# DryBox Protocol Integration Status
|
||||||
|
|
||||||
## Overview
|
## Current Issues
|
||||||
Successfully integrated the complete protocol stack with DryBox, combining:
|
|
||||||
- **Noise XK**: End-to-end encrypted handshake and session establishment
|
|
||||||
- **Codec2**: Voice compression at 1200 bps (48 bits per 40ms frame)
|
|
||||||
- **4FSK Modulation**: Robust modulation for GSM voice channels (600 baud)
|
|
||||||
- **ChaCha20**: Additional voice frame encryption layer
|
|
||||||
|
|
||||||
**Latest Update**: Fixed UI integration to use existing DryBox UI with enhanced debugging
|
1. **UI Integration Problems:**
|
||||||
|
- The UI is trying to use the old `NoiseXKSession` handshake mechanism
|
||||||
|
- The `protocol_phone_client.py` was calling non-existent `get_messages()` method
|
||||||
|
- Error handling was passing error messages through the UI state system
|
||||||
|
|
||||||
## Components Integrated
|
2. **Protocol Mismatch:**
|
||||||
|
- The original DryBox UI uses a simple handshake exchange
|
||||||
|
- The Protocol uses a more complex Noise XK pattern with:
|
||||||
|
- PING REQUEST/RESPONSE
|
||||||
|
- HANDSHAKE messages
|
||||||
|
- Key derivation
|
||||||
|
- These two approaches are incompatible without significant refactoring
|
||||||
|
|
||||||
### 1. Voice Codec (`voice_codec.py`)
|
3. **Test Failures:**
|
||||||
- **Codec2Wrapper**: Simulates Codec2 voice compression
|
- FSK demodulation is returning empty data
|
||||||
- Default mode: 1200 bps (optimal for GSM)
|
- Voice protocol tests are failing due to API mismatches
|
||||||
- Frame size: 48 bits (6 bytes) per 40ms
|
- Encryption tests have parameter issues
|
||||||
- Sample rate: 8kHz mono
|
|
||||||
- **FSKModem**: 4-FSK modulation/demodulation
|
|
||||||
- Frequencies: 600, 1200, 1800, 2400 Hz
|
|
||||||
- Symbol rate: 600 baud
|
|
||||||
- Preamble detection and synchronization
|
|
||||||
- Confidence scoring for demodulation
|
|
||||||
|
|
||||||
### 2. Encryption (`encryption.py`)
|
## What Works
|
||||||
- **ChaCha20-CTR**: Stream cipher for voice frames
|
|
||||||
- 256-bit keys
|
|
||||||
- 16-byte nonces
|
|
||||||
- Low latency (no authentication for voice)
|
|
||||||
- **ChaCha20-Poly1305**: Authenticated encryption for control messages
|
|
||||||
- **AES-256-GCM**: Alternative authenticated encryption
|
|
||||||
|
|
||||||
### 3. Protocol Phone Client (`protocol_phone_client.py`)
|
1. **Individual Components:**
|
||||||
- Extends base phone client with protocol support
|
- IcingProtocol works standalone
|
||||||
- Integrates Noise XK session management
|
- FSK modulation works (but demodulation has issues)
|
||||||
- Handles voice frame encoding/decoding pipeline:
|
- Codec2 wrapper works
|
||||||
1. PCM → Codec2 → 4FSK → ChaCha20 → Noise XK → Network
|
- Encryption/decryption works with correct parameters
|
||||||
2. Network → Noise XK → ChaCha20 → 4FSK → Codec2 → PCM
|
|
||||||
- Voice session management (start/stop)
|
|
||||||
- Automatic key derivation from Noise session
|
|
||||||
|
|
||||||
### 4. Protocol Client State (`protocol_client_state.py`)
|
2. **Simple Integration:**
|
||||||
- Enhanced state management for protocol operations
|
- See `simple_integrated_ui.py` for a working example
|
||||||
- Voice session state tracking
|
- Shows proper protocol flow step-by-step
|
||||||
- Automatic voice start after handshake completion
|
- Demonstrates successful key exchange and encryption
|
||||||
- Command queue for async operations
|
|
||||||
|
|
||||||
### 5. Enhanced DryBox UI (`main.py`)
|
## Recommended Approach
|
||||||
- Uses existing DryBox UI (not a new UI)
|
|
||||||
- Added debug console with timestamped messages
|
|
||||||
- Added automatic test button with 11-step sequence
|
|
||||||
- Visual waveform display for transmitted/received audio
|
|
||||||
- Real-time status updates
|
|
||||||
- Support for test audio file transmission (wav/input_8k_mono.wav)
|
|
||||||
|
|
||||||
## Protocol Flow
|
Instead of trying to retrofit the complex Protocol into the existing DryBox UI, I recommend:
|
||||||
|
|
||||||
1. **Connection**: Phones connect to GSM simulator
|
1. **Start Fresh:**
|
||||||
2. **Call Setup**: Phone 1 calls Phone 2
|
- Use `simple_integrated_ui.py` as a base
|
||||||
3. **Noise XK Handshake**:
|
- Build up the phone UI features gradually
|
||||||
- 3-message handshake pattern
|
- Ensure each step works before adding complexity
|
||||||
- Establishes encrypted channel
|
|
||||||
- Derives voice encryption keys
|
|
||||||
4. **Voice Transmission**:
|
|
||||||
- Audio → Codec2 (1200bps) → 4FSK modulation
|
|
||||||
- Encrypt with ChaCha20 (per-frame key stream)
|
|
||||||
- Wrap in Noise XK encrypted channel
|
|
||||||
- Transmit over GSM voice channel
|
|
||||||
5. **Voice Reception**:
|
|
||||||
- Reverse of transmission process
|
|
||||||
- Confidence-based demodulation
|
|
||||||
- Frame reconstruction
|
|
||||||
|
|
||||||
## Testing
|
2. **Fix Protocol Flow:**
|
||||||
|
- Remove old handshake code from UI
|
||||||
|
- Implement proper state machine for protocol phases:
|
||||||
|
- IDLE → CONNECTING → PING_SENT → HANDSHAKE_SENT → KEYS_DERIVED → READY
|
||||||
|
- Handle auto-responder mode properly
|
||||||
|
|
||||||
All components tested and verified:
|
3. **Simplify Audio Integration:**
|
||||||
- ✓ Codec2 compression/decompression
|
- Get basic encrypted messaging working first
|
||||||
- ✓ 4FSK modulation/demodulation (>92% confidence)
|
- Add voice/FSK modulation as a separate phase
|
||||||
- ✓ ChaCha20 encryption/decryption
|
- Test with GSM simulator separately
|
||||||
- ✓ Full pipeline integration
|
|
||||||
- ✓ GSM simulator compatibility
|
|
||||||
|
|
||||||
## Usage
|
## Quick Start
|
||||||
|
|
||||||
1. Start the GSM simulator:
|
To see the protocol working:
|
||||||
```bash
|
```bash
|
||||||
cd simulator
|
cd DryBox/UI
|
||||||
./launch_gsm_simulator.sh
|
python3 simple_integrated_ui.py
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run the DryBox UI:
|
Then click through the buttons in order:
|
||||||
```bash
|
1. Connect
|
||||||
python3 UI/main.py
|
2. Send PING
|
||||||
```
|
3. Send Handshake
|
||||||
|
4. Derive Keys
|
||||||
3. Click "Run Automatic Test" button for full protocol testing
|
5. Send Encrypted Message
|
||||||
|
|
||||||
3. Or use the protocol phone client directly in your application:
|
This demonstrates the full protocol flow without the complexity of the phone UI.
|
||||||
```python
|
|
||||||
from protocol_phone_client import ProtocolPhoneClient
|
|
||||||
client = ProtocolPhoneClient(client_id=1)
|
|
||||||
client.start()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Features
|
## Next Steps
|
||||||
|
|
||||||
- **End-to-end encryption**: Noise XK + ChaCha20 dual layer
|
1. Fix the FSK demodulation issue
|
||||||
- **GSM compatible**: Works over standard voice channels
|
2. Create a new phone UI based on the working protocol flow
|
||||||
- **Low bitrate**: 1200 bps voice codec
|
3. Integrate voice/audio after basic encryption works
|
||||||
- **Robust modulation**: 4FSK survives GSM compression
|
4. Add GSM simulator support once everything else works
|
||||||
- **Real-time**: 40ms frame latency
|
|
||||||
- **Confidence scoring**: Quality metrics for demodulation
|
|
||||||
|
|
||||||
## Current Issues & Debugging
|
|
||||||
|
|
||||||
### 1. **Handshake Timing**
|
|
||||||
- Issue: Encrypted data sometimes arrives before handshake completes
|
|
||||||
- Debug: Added extensive logging to track handshake state
|
|
||||||
- Status: Needs timing synchronization fix
|
|
||||||
|
|
||||||
### 2. **State Management**
|
|
||||||
- Fixed: PhoneState now uses proper Python Enum
|
|
||||||
- Fixed: Added proper initiator/responder role tracking
|
|
||||||
|
|
||||||
### 3. **Decryption Errors**
|
|
||||||
- Symptom: "Decryption error" with ciphertext in logs
|
|
||||||
- Cause: Data received before secure channel established
|
|
||||||
- Mitigation: Added checks for handshake_complete before processing
|
|
||||||
|
|
||||||
## Debug Features Added
|
|
||||||
|
|
||||||
1. **Debug Console in UI**
|
|
||||||
- Real-time protocol message display
|
|
||||||
- Timestamped messages
|
|
||||||
- Clear button for cleanup
|
|
||||||
- Auto-scroll to latest messages
|
|
||||||
|
|
||||||
2. **Automatic Test Sequence**
|
|
||||||
- Step 1: Check initial state
|
|
||||||
- Step 2: Make call
|
|
||||||
- Step 3: Answer call
|
|
||||||
- Step 4: Check handshake progress
|
|
||||||
- Step 5: Check handshake status
|
|
||||||
- Step 6: Check voice status
|
|
||||||
- Step 7: Check audio transmission
|
|
||||||
- Step 8: Protocol details
|
|
||||||
- Step 9: Let transmission run
|
|
||||||
- Step 10: Final statistics
|
|
||||||
- Step 11: Hang up
|
|
||||||
|
|
||||||
3. **Enhanced Logging**
|
|
||||||
- All components use debug() method
|
|
||||||
- Verbose handshake state tracking
|
|
||||||
- Voice frame logging (every 25 frames)
|
|
||||||
- Disabled Noise session verbose logging
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- Fix handshake timing to ensure completion before voice
|
|
||||||
- Add FEC (Forward Error Correction) for improved robustness
|
|
||||||
- Implement voice activity detection (VAD)
|
|
||||||
- Add adaptive bitrate selection
|
|
||||||
- Integrate with real Codec2 library (not simulation)
|
|
||||||
- Add DTMF signaling for out-of-band control
|
|
@ -1,67 +1,323 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
import select
|
import select
|
||||||
|
import threading
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal
|
from PyQt5.QtCore import QThread, pyqtSignal
|
||||||
|
|
||||||
|
# Add Protocol directory to Python path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'Protocol'))
|
||||||
|
|
||||||
|
from protocol import IcingProtocol
|
||||||
|
from voice_codec import VoiceProtocol, Codec2Mode
|
||||||
|
from messages import VoiceStart, VoiceAck, VoiceEnd
|
||||||
|
from encryption import EncryptedMessage
|
||||||
|
|
||||||
class ProtocolPhoneClient(QThread):
|
class ProtocolPhoneClient(QThread):
|
||||||
|
"""Phone client that integrates the full Icing Protocol with 4FSK and ChaCha20."""
|
||||||
|
|
||||||
data_received = pyqtSignal(bytes, int)
|
data_received = pyqtSignal(bytes, int)
|
||||||
state_changed = pyqtSignal(str, str, int)
|
state_changed = pyqtSignal(str, str, int)
|
||||||
|
audio_received = pyqtSignal(bytes, int) # For decoded audio
|
||||||
|
|
||||||
|
def __init__(self, client_id, identity_keys=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
|
# Initialize Icing Protocol
|
||||||
|
self.protocol = IcingProtocol()
|
||||||
|
|
||||||
|
# Override identity keys if provided
|
||||||
|
if identity_keys:
|
||||||
|
self.protocol.identity_privkey = identity_keys[0]
|
||||||
|
self.protocol.identity_pubkey = identity_keys[1]
|
||||||
|
|
||||||
|
# Connection state
|
||||||
|
self.connected = False
|
||||||
self.handshake_complete = False
|
self.handshake_complete = False
|
||||||
|
|
||||||
# Voice state
|
# Voice state
|
||||||
self.voice_active = False
|
self.voice_active = False
|
||||||
|
self.voice_protocol = None
|
||||||
|
|
||||||
|
# Peer information
|
||||||
|
self.peer_identity_hex = None
|
||||||
|
self.peer_port = None
|
||||||
|
|
||||||
|
# For GSM simulator compatibility
|
||||||
|
self.gsm_host = "localhost"
|
||||||
|
self.gsm_port = 12345
|
||||||
|
self.gsm_socket = None
|
||||||
|
|
||||||
try:
|
# Track processed messages
|
||||||
return True
|
self.processed_message_count = 0
|
||||||
except Exception as e:
|
|
||||||
return False
|
def set_peer_identity(self, peer_identity_hex):
|
||||||
|
"""Set the peer's identity public key (hex string)."""
|
||||||
|
self.peer_identity_hex = peer_identity_hex
|
||||||
|
self.protocol.set_peer_identity(peer_identity_hex)
|
||||||
|
|
||||||
|
def set_peer_port(self, port):
|
||||||
|
"""Set the peer's port for direct connection."""
|
||||||
|
self.peer_port = port
|
||||||
|
|
||||||
|
def connect_to_gsm_simulator(self):
|
||||||
|
"""Connect to the GSM simulator for voice channel simulation."""
|
||||||
|
try:
|
||||||
|
self.gsm_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.gsm_socket.settimeout(5)
|
||||||
|
self.gsm_socket.connect((self.gsm_host, self.gsm_port))
|
||||||
|
print(f"Client {self.client_id} connected to GSM simulator")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Client {self.client_id} failed to connect to GSM simulator: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
"""Main thread loop."""
|
||||||
|
# Protocol listener already started in __init__ of IcingProtocol
|
||||||
|
|
||||||
|
# Connect to GSM simulator if available
|
||||||
|
self.connect_to_gsm_simulator()
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
|
# Process protocol messages
|
||||||
|
self._process_protocol_messages()
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
except Exception as e:
|
|
||||||
|
|
||||||
try:
|
|
||||||
except:
|
|
||||||
self.data_received.emit(plaintext, self.client_id)
|
|
||||||
|
|
||||||
self.voice_active = True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Process GSM simulator data if connected
|
||||||
|
if self.gsm_socket:
|
||||||
|
self._process_gsm_data()
|
||||||
|
|
||||||
|
self.msleep(10)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Client {self.client_id} error in main loop: {e}")
|
||||||
|
# Only emit state change if it's a real connection error
|
||||||
|
if "get_messages" not in str(e):
|
||||||
|
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||||
|
break
|
||||||
|
|
||||||
|
def _process_protocol_messages(self):
|
||||||
|
"""Process messages from the Icing Protocol."""
|
||||||
|
# Process new messages in the inbound queue
|
||||||
|
if not hasattr(self.protocol, 'inbound_messages'):
|
||||||
|
return
|
||||||
|
|
||||||
|
new_messages = self.protocol.inbound_messages[self.processed_message_count:]
|
||||||
|
|
||||||
|
for msg in new_messages:
|
||||||
|
self.processed_message_count += 1
|
||||||
|
msg_type = msg.get('type', '')
|
||||||
|
|
||||||
|
if msg_type == 'PING_REQUEST':
|
||||||
|
# Received ping request, we're the responder
|
||||||
|
if not self.protocol.state['ping_sent']:
|
||||||
|
# Enable auto responder to handle protocol flow
|
||||||
|
self.protocol.auto_responder = True
|
||||||
|
# Send ping response
|
||||||
|
index = self.protocol.inbound_messages.index(msg)
|
||||||
|
self.protocol.respond_to_ping(index, 1) # Accept with ChaCha20
|
||||||
|
|
||||||
|
elif msg_type == 'PING_RESPONSE':
|
||||||
|
# Ping response received, continue with handshake
|
||||||
|
if not self.protocol.state['handshake_sent']:
|
||||||
|
self.protocol.send_handshake()
|
||||||
|
|
||||||
|
elif msg_type == 'HANDSHAKE':
|
||||||
|
# Handshake received
|
||||||
|
if self.protocol.state['ping_sent'] and not self.protocol.state['handshake_sent']:
|
||||||
|
# We're initiator, send our handshake
|
||||||
|
self.protocol.send_handshake()
|
||||||
|
# Derive keys if we have peer's handshake
|
||||||
|
if self.protocol.state['handshake_received'] and not self.protocol.state['key_exchange_complete']:
|
||||||
|
self.protocol.derive_hkdf()
|
||||||
|
self.handshake_complete = True
|
||||||
|
self.state_changed.emit("HANDSHAKE_DONE", "", self.client_id)
|
||||||
|
|
||||||
|
elif msg_type == 'ENCRYPTED':
|
||||||
|
# Decrypt and process encrypted message
|
||||||
|
parsed = msg.get('parsed')
|
||||||
|
if parsed and hasattr(parsed, 'plaintext'):
|
||||||
|
self._handle_encrypted_message(parsed.plaintext)
|
||||||
|
|
||||||
|
elif msg_type == 'voice_start':
|
||||||
|
# Voice session started by peer
|
||||||
|
self._handle_voice_start(msg.get('parsed'))
|
||||||
|
|
||||||
|
elif msg_type == 'voice_ack':
|
||||||
|
# Voice session acknowledged
|
||||||
|
self.voice_active = True
|
||||||
|
|
||||||
|
elif msg_type == 'voice_end':
|
||||||
|
# Voice session ended
|
||||||
|
self.voice_active = False
|
||||||
|
|
||||||
|
def _process_gsm_data(self):
|
||||||
|
"""Process audio data from GSM simulator."""
|
||||||
|
try:
|
||||||
|
readable, _, _ = select.select([self.gsm_socket], [], [], 0)
|
||||||
|
if readable:
|
||||||
|
data = self.gsm_socket.recv(4096)
|
||||||
|
if data and self.voice_active and self.voice_protocol:
|
||||||
|
# Process received FSK-modulated audio
|
||||||
|
encrypted_frames = self.voice_protocol.demodulate_audio(data)
|
||||||
|
for frame in encrypted_frames:
|
||||||
|
# Decrypt and decode
|
||||||
|
audio_samples = self.voice_protocol.decrypt_and_decode(frame)
|
||||||
|
if audio_samples:
|
||||||
|
self.audio_received.emit(audio_samples, self.client_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Client {self.client_id} GSM data processing error: {e}")
|
||||||
|
|
||||||
self.voice_active = False
|
def _handle_encrypted_message(self, plaintext):
|
||||||
|
"""Handle decrypted message content."""
|
||||||
|
# Check if it's audio data or control message
|
||||||
|
if plaintext.startswith(b'AUDIO:'):
|
||||||
|
audio_data = plaintext[6:]
|
||||||
|
self.data_received.emit(audio_data, self.client_id)
|
||||||
|
else:
|
||||||
|
# Control message
|
||||||
|
try:
|
||||||
|
message = plaintext.decode('utf-8')
|
||||||
|
self._handle_control_message(message)
|
||||||
|
except:
|
||||||
|
# Binary data
|
||||||
|
self.data_received.emit(plaintext, self.client_id)
|
||||||
|
|
||||||
|
def _handle_control_message(self, message):
|
||||||
|
"""Handle control messages."""
|
||||||
|
if message == "RINGING":
|
||||||
|
self.state_changed.emit("RINGING", "", self.client_id)
|
||||||
|
elif message == "IN_CALL":
|
||||||
|
self.state_changed.emit("IN_CALL", "", self.client_id)
|
||||||
|
elif message == "CALL_END":
|
||||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||||
|
|
||||||
|
def _handle_voice_start(self, voice_start_msg):
|
||||||
|
"""Handle voice session start."""
|
||||||
|
if voice_start_msg:
|
||||||
|
# Initialize voice protocol with negotiated parameters
|
||||||
|
self.protocol.voice_session_active = True
|
||||||
|
self.protocol.voice_session_id = voice_start_msg.session_id
|
||||||
|
|
||||||
|
# Send acknowledgment
|
||||||
|
self.protocol.send_voice_ack(voice_start_msg.session_id)
|
||||||
|
|
||||||
|
# Initialize voice codec
|
||||||
|
self._initialize_voice_protocol(voice_start_msg.codec_mode)
|
||||||
|
self.voice_active = True
|
||||||
|
|
||||||
try:
|
def _initialize_voice_protocol(self, codec_mode=Codec2Mode.MODE_1200):
|
||||||
except Exception as e:
|
"""Initialize voice protocol with codec and encryption."""
|
||||||
|
if self.protocol.hkdf_key:
|
||||||
|
self.voice_protocol = VoiceProtocol(
|
||||||
|
shared_key=bytes.fromhex(self.protocol.hkdf_key),
|
||||||
|
codec_mode=codec_mode,
|
||||||
|
cipher_type=self.protocol.cipher_type
|
||||||
|
)
|
||||||
|
print(f"Client {self.client_id} initialized voice protocol")
|
||||||
|
|
||||||
if isinstance(message, str):
|
def initiate_call(self):
|
||||||
message = message.encode('utf-8')
|
"""Initiate a call to the peer."""
|
||||||
|
if not self.peer_port:
|
||||||
|
print(f"Client {self.client_id}: No peer port set")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Connect to peer
|
||||||
|
self.protocol.connect_to_peer(self.peer_port)
|
||||||
|
self.state_changed.emit("CALLING", "", self.client_id)
|
||||||
|
|
||||||
|
# Start key exchange
|
||||||
|
self.protocol.generate_ephemeral_keys()
|
||||||
|
self.protocol.send_ping_request(cipher_type=1) # Request ChaCha20
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def answer_call(self):
|
||||||
|
"""Answer an incoming call."""
|
||||||
|
self.state_changed.emit("IN_CALL", "", self.client_id)
|
||||||
|
|
||||||
|
# Enable auto-responder for handling protocol flow
|
||||||
|
self.protocol.auto_responder = True
|
||||||
|
|
||||||
|
# If we already have a ping request, respond to it
|
||||||
|
for i, msg in enumerate(self.protocol.inbound_messages):
|
||||||
|
if msg.get('type') == 'PING_REQUEST' and not self.protocol.state['ping_sent']:
|
||||||
|
self.protocol.respond_to_ping(i, 1) # Accept with ChaCha20
|
||||||
|
break
|
||||||
|
|
||||||
|
def end_call(self):
|
||||||
|
"""End the current call."""
|
||||||
|
if self.voice_active:
|
||||||
|
self.protocol.end_voice_call()
|
||||||
|
|
||||||
|
self.voice_active = False
|
||||||
|
self.handshake_complete = False
|
||||||
|
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||||
|
|
||||||
|
# Close connections
|
||||||
|
for conn in self.protocol.connections:
|
||||||
|
conn.close()
|
||||||
|
self.protocol.connections.clear()
|
||||||
|
|
||||||
|
def send_audio(self, audio_samples):
|
||||||
|
"""Send audio samples through the voice protocol."""
|
||||||
|
if self.voice_active and self.voice_protocol and self.gsm_socket:
|
||||||
|
# Encode and encrypt audio
|
||||||
|
fsk_audio = self.voice_protocol.encode_and_encrypt(audio_samples)
|
||||||
|
if fsk_audio and self.gsm_socket:
|
||||||
|
try:
|
||||||
|
# Send FSK-modulated audio through GSM simulator
|
||||||
|
self.gsm_socket.send(fsk_audio)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Client {self.client_id} failed to send audio: {e}")
|
||||||
|
|
||||||
|
def send_message(self, message):
|
||||||
|
"""Send an encrypted message."""
|
||||||
|
if self.handshake_complete:
|
||||||
|
if isinstance(message, str):
|
||||||
|
message = message.encode('utf-8')
|
||||||
|
self.protocol.send_encrypted_message(message)
|
||||||
|
|
||||||
|
def start_voice_session(self):
|
||||||
|
"""Start a voice session."""
|
||||||
|
if self.handshake_complete and not self.voice_active:
|
||||||
|
self._initialize_voice_protocol()
|
||||||
|
self.protocol.start_voice_call(codec_mode=5) # 1200 bps mode
|
||||||
|
|
||||||
|
def get_identity_key(self):
|
||||||
|
"""Get this client's identity public key."""
|
||||||
|
if hasattr(self.protocol, 'identity_pubkey') and self.protocol.identity_pubkey:
|
||||||
|
return self.protocol.identity_pubkey.hex()
|
||||||
|
return "test_identity_key_" + str(self.client_id)
|
||||||
|
|
||||||
|
def get_local_port(self):
|
||||||
|
"""Get the local listening port."""
|
||||||
|
if hasattr(self.protocol, 'local_port'):
|
||||||
|
return self.protocol.local_port
|
||||||
|
return 12345 + self.client_id
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""Stop the client."""
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
# End voice session if active
|
||||||
|
if self.voice_active:
|
||||||
|
self.protocol.end_voice_call()
|
||||||
|
|
||||||
|
# Stop protocol server listener
|
||||||
|
if hasattr(self.protocol, 'server_listener') and self.protocol.server_listener:
|
||||||
|
self.protocol.server_listener.stop()
|
||||||
|
|
||||||
|
# Close GSM socket
|
||||||
|
if self.gsm_socket:
|
||||||
try:
|
try:
|
||||||
|
self.gsm_socket.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
self.quit()
|
self.quit()
|
||||||
self.wait(1000)
|
self.wait(1000)
|
Loading…
Reference in New Issue
Block a user