This commit is contained in:
parent
9e2daa7f53
commit
41aff9848a
415
protocol_prototype/DryBox/UI/python_ui.py
Normal file
415
protocol_prototype/DryBox/UI/python_ui.py
Normal file
@ -0,0 +1,415 @@
|
||||
import sys
|
||||
import random
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QPushButton, QLabel, QFrame, QSizePolicy, QStyle
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QTimer, QSize, QPointF, pyqtSignal, QThread
|
||||
from PyQt5.QtGui import QPainter, QColor, QPen, QLinearGradient, QBrush, QIcon, QFont
|
||||
|
||||
# --- Phone Client Thread ---
|
||||
class PhoneClient(QThread):
|
||||
data_received = pyqtSignal(bytes, int) # Include client_id
|
||||
state_changed = pyqtSignal(str, str, int) # Include client_id
|
||||
|
||||
def __init__(self, host, port, client_id):
|
||||
super().__init__()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.client_id = client_id
|
||||
self.sock = None
|
||||
self.running = True
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.sock.settimeout(15)
|
||||
self.sock.connect((self.host, self.port))
|
||||
print(f"Client {self.client_id} connected to {self.host}:{self.port}")
|
||||
while self.running:
|
||||
try:
|
||||
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
|
||||
decoded_data = data.decode('utf-8', errors='ignore').strip()
|
||||
print(f"Client {self.client_id} received raw: {decoded_data}")
|
||||
if decoded_data in ["RINGING", "CALL_END", "CALL_DROPPED", "IN_CALL"]:
|
||||
self.state_changed.emit(decoded_data, "", self.client_id)
|
||||
else:
|
||||
self.data_received.emit(data, self.client_id)
|
||||
print(f"Client {self.client_id} received audio: {decoded_data}")
|
||||
except socket.timeout:
|
||||
print(f"Client {self.client_id} timed out waiting for data")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} error: {e}")
|
||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} connection failed: {e}")
|
||||
finally:
|
||||
if self.sock:
|
||||
self.sock.close()
|
||||
|
||||
def send(self, message):
|
||||
if self.sock and self.running:
|
||||
try:
|
||||
self.sock.send(message.encode())
|
||||
print(f"Client {self.client_id} sent: {message}")
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} send error: {e}")
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
if self.sock:
|
||||
self.sock.close()
|
||||
|
||||
# --- Custom Waveform Widget ---
|
||||
class WaveformWidget(QWidget):
|
||||
def __init__(self, parent=None, dynamic=False):
|
||||
super().__init__(parent)
|
||||
self.dynamic = dynamic
|
||||
self.setMinimumSize(200, 80)
|
||||
self.setMaximumHeight(100)
|
||||
self.waveform_data = [random.randint(10, 90) for _ in range(50)]
|
||||
if self.dynamic:
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.update_waveform)
|
||||
self.timer.start(100)
|
||||
|
||||
def update_waveform(self):
|
||||
self.waveform_data = self.waveform_data[1:] + [random.randint(10, 90)]
|
||||
self.update()
|
||||
|
||||
def set_data(self, data):
|
||||
amplitude = sum(byte for byte in data) % 90 + 10
|
||||
self.waveform_data = self.waveform_data[1:] + [amplitude]
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
painter.fillRect(self.rect(), QColor("#2D2D2D"))
|
||||
gradient = QLinearGradient(0, 0, 0, self.height())
|
||||
gradient.setColorAt(0.0, QColor("#0078D4"))
|
||||
gradient.setColorAt(1.0, QColor("#50E6A4"))
|
||||
pen = QPen(QBrush(gradient), 2)
|
||||
painter.setPen(pen)
|
||||
bar_width = self.width() / len(self.waveform_data)
|
||||
max_h = self.height() - 10
|
||||
for i, val in enumerate(self.waveform_data):
|
||||
bar_height = (val / 100.0) * max_h
|
||||
x = i * bar_width
|
||||
y = (self.height() - bar_height) / 2
|
||||
painter.drawLine(QPointF(x + bar_width / 2, y), QPointF(x + bar_width / 2, y + bar_height))
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self.update()
|
||||
|
||||
# --- Phone State ---
|
||||
class PhoneState:
|
||||
IDLE = 0
|
||||
CALLING = 1
|
||||
IN_CALL = 2
|
||||
RINGING = 3
|
||||
|
||||
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;
|
||||
}
|
||||
""")
|
||||
|
||||
# Phone states
|
||||
self.phone1_state = PhoneState.IDLE
|
||||
self.phone2_state = PhoneState.IDLE
|
||||
|
||||
# Phone clients
|
||||
self.phone1_client = PhoneClient("localhost", 12345, 0)
|
||||
self.phone2_client = PhoneClient("localhost", 12345, 1)
|
||||
self.phone1_client.data_received.connect(lambda data, cid: self.update_waveform(cid, data))
|
||||
self.phone2_client.data_received.connect(lambda data, cid: self.update_waveform(cid, data))
|
||||
self.phone1_client.state_changed.connect(lambda state, num, cid: self.set_phone_state(cid, self.map_state(state), num))
|
||||
self.phone2_client.state_changed.connect(lambda state, num, cid: self.set_phone_state(cid, self.map_state(state), num))
|
||||
self.phone1_client.start()
|
||||
self.phone2_client.start()
|
||||
|
||||
# Main widget and layout
|
||||
main_widget = QWidget()
|
||||
self.setCentralWidget(main_widget)
|
||||
main_layout = QVBoxLayout()
|
||||
main_layout.setSpacing(20)
|
||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||
main_layout.setAlignment(Qt.AlignCenter)
|
||||
main_widget.setLayout(main_layout)
|
||||
|
||||
# App Title
|
||||
app_title_label = QLabel("Dual Phone Control Panel")
|
||||
app_title_label.setObjectName("mainTitleLabel")
|
||||
app_title_label.setAlignment(Qt.AlignCenter)
|
||||
main_layout.addWidget(app_title_label)
|
||||
|
||||
# Phone displays layout
|
||||
phone_controls_layout = QHBoxLayout()
|
||||
phone_controls_layout.setSpacing(50)
|
||||
phone_controls_layout.setAlignment(Qt.AlignCenter)
|
||||
main_layout.addLayout(phone_controls_layout)
|
||||
|
||||
# Phone 1
|
||||
phone1_widget_container, self.phone1_display, self.phone1_button, self.phone1_waveform = self._create_phone_ui("Phone 1", self.phone1_action)
|
||||
phone_controls_layout.addWidget(phone1_widget_container)
|
||||
|
||||
# Phone 2
|
||||
phone2_widget_container, self.phone2_display, self.phone2_button, self.phone2_waveform = self._create_phone_ui("Phone 2", self.phone2_action)
|
||||
phone_controls_layout.addWidget(phone2_widget_container)
|
||||
|
||||
# Spacer
|
||||
main_layout.addStretch(1)
|
||||
|
||||
# Settings Button
|
||||
self.settings_button = QPushButton("Settings")
|
||||
self.settings_button.setObjectName("settingsButton")
|
||||
self.settings_button.setFixedWidth(180)
|
||||
self.settings_button.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView))
|
||||
self.settings_button.setIconSize(QSize(20, 20))
|
||||
self.settings_button.clicked.connect(self.settings_action)
|
||||
settings_layout = QHBoxLayout()
|
||||
settings_layout.addStretch()
|
||||
settings_layout.addWidget(self.settings_button)
|
||||
settings_layout.addStretch()
|
||||
main_layout.addLayout(settings_layout)
|
||||
|
||||
# Initialize button states
|
||||
self._update_phone_button_ui(self.phone1_button, self.phone1_state)
|
||||
self._update_phone_button_ui(self.phone2_button, self.phone2_state)
|
||||
|
||||
def _create_phone_ui(self, title, action_slot):
|
||||
phone_container_widget = QWidget()
|
||||
phone_container_widget.setObjectName("phoneWidget")
|
||||
phone_layout = QVBoxLayout()
|
||||
phone_layout.setAlignment(Qt.AlignCenter)
|
||||
phone_layout.setSpacing(15)
|
||||
phone_container_widget.setLayout(phone_layout)
|
||||
|
||||
phone_title_label = QLabel(title)
|
||||
phone_title_label.setObjectName("phoneTitleLabel")
|
||||
phone_title_label.setAlignment(Qt.AlignCenter)
|
||||
phone_layout.addWidget(phone_title_label)
|
||||
|
||||
phone_display_frame = QFrame()
|
||||
phone_display_frame.setObjectName("phoneDisplay")
|
||||
phone_display_frame.setFixedSize(250, 350)
|
||||
phone_display_frame.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
|
||||
display_content_layout = QVBoxLayout(phone_display_frame)
|
||||
display_content_layout.setAlignment(Qt.AlignCenter)
|
||||
phone_status_label = QLabel("Idle")
|
||||
phone_status_label.setAlignment(Qt.AlignCenter)
|
||||
phone_status_label.setFont(QFont("Arial", 16))
|
||||
display_content_layout.addWidget(phone_status_label)
|
||||
phone_layout.addWidget(phone_display_frame, alignment=Qt.AlignCenter)
|
||||
|
||||
phone_button = QPushButton()
|
||||
phone_button.setFixedWidth(120)
|
||||
phone_button.setIconSize(QSize(20, 20))
|
||||
phone_button.clicked.connect(action_slot)
|
||||
phone_layout.addWidget(phone_button, alignment=Qt.AlignCenter)
|
||||
|
||||
waveform_label = QLabel(f"{title} Audio")
|
||||
waveform_label.setAlignment(Qt.AlignCenter)
|
||||
waveform_label.setStyleSheet("font-size: 14px; color: #E0E0E0;")
|
||||
phone_layout.addWidget(waveform_label)
|
||||
waveform_widget = WaveformWidget(dynamic=False)
|
||||
phone_layout.addWidget(waveform_widget, alignment=Qt.AlignCenter)
|
||||
|
||||
phone_display_frame.setProperty("statusLabel", phone_status_label)
|
||||
return phone_container_widget, phone_display_frame, phone_button, waveform_widget
|
||||
|
||||
def _update_phone_button_ui(self, button, state, phone_number=""):
|
||||
parent_widget = button.parentWidget()
|
||||
if parent_widget:
|
||||
frame = parent_widget.findChild(QFrame, "phoneDisplay")
|
||||
if frame:
|
||||
status_label = frame.property("statusLabel")
|
||||
if status_label:
|
||||
if state == PhoneState.IDLE:
|
||||
button.setText("Call")
|
||||
button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
|
||||
status_label.setText("Idle")
|
||||
button.setStyleSheet("background-color: #0078D4;")
|
||||
elif state == PhoneState.CALLING:
|
||||
button.setText("Cancel")
|
||||
button.setIcon(self.style().standardIcon(QStyle.SP_MediaStop))
|
||||
status_label.setText(f"Calling {phone_number}...")
|
||||
button.setStyleSheet("background-color: #E81123;")
|
||||
elif state == PhoneState.IN_CALL:
|
||||
button.setText("Hang Up")
|
||||
button.setIcon(self.style().standardIcon(QStyle.SP_DialogCancelButton))
|
||||
status_label.setText(f"In Call with {phone_number}")
|
||||
button.setStyleSheet("background-color: #E81123;")
|
||||
elif state == PhoneState.RINGING:
|
||||
button.setText("Answer")
|
||||
button.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton))
|
||||
status_label.setText(f"Incoming Call from {phone_number}")
|
||||
button.setStyleSheet("background-color: #107C10;")
|
||||
else:
|
||||
print("Warning: statusLabel property not found")
|
||||
else:
|
||||
print("Warning: QFrame not found")
|
||||
else:
|
||||
print("Warning: Parent widget not found")
|
||||
|
||||
def update_waveform(self, client_id, data):
|
||||
print(f"Updating waveform for client_id {client_id}")
|
||||
waveform = self.phone1_waveform if client_id == 0 else self.phone2_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
|
||||
return PhoneState.IDLE # Default to IDLE
|
||||
|
||||
def set_phone_state(self, client_id, state, number=""):
|
||||
if client_id == 0:
|
||||
self.phone1_state = state
|
||||
self._update_phone_button_ui(self.phone1_button, self.phone1_state, number if number else "123-4567")
|
||||
if state == PhoneState.IDLE and hasattr(self, 'phone1_audio_timer'):
|
||||
self.phone1_audio_timer.stop()
|
||||
elif state == PhoneState.IN_CALL and (not hasattr(self, 'phone1_audio_timer') or not self.phone1_audio_timer.isActive()):
|
||||
self.phone1_audio_timer = QTimer(self)
|
||||
self.phone1_audio_timer.timeout.connect(self.send_phone1_audio)
|
||||
self.phone1_audio_timer.start(1000)
|
||||
else:
|
||||
self.phone2_state = state
|
||||
self._update_phone_button_ui(self.phone2_button, self.phone2_state, number if number else "987-6543")
|
||||
if state == PhoneState.IDLE and hasattr(self, 'phone2_audio_timer'):
|
||||
self.phone2_audio_timer.stop()
|
||||
elif state == PhoneState.IN_CALL and (not hasattr(self, 'phone2_audio_timer') or not self.phone2_audio_timer.isActive()):
|
||||
self.phone2_audio_timer = QTimer(self)
|
||||
self.phone2_audio_timer.timeout.connect(self.send_phone2_audio)
|
||||
self.phone2_audio_timer.start(1000)
|
||||
|
||||
def phone1_action(self):
|
||||
print("Phone 1 Action")
|
||||
if self.phone1_state == PhoneState.IDLE:
|
||||
self.phone1_state = PhoneState.CALLING
|
||||
self.phone1_client.send("RINGING")
|
||||
self._update_phone_button_ui(self.phone1_button, self.phone1_state, "123-4567")
|
||||
elif self.phone1_state == PhoneState.CALLING:
|
||||
self.phone1_state = PhoneState.IDLE
|
||||
self.phone1_client.send("CALL_END")
|
||||
self._update_phone_button_ui(self.phone1_button, self.phone1_state)
|
||||
if hasattr(self, 'phone1_audio_timer'):
|
||||
self.phone1_audio_timer.stop()
|
||||
elif self.phone1_state == PhoneState.RINGING:
|
||||
self.phone1_state = PhoneState.IN_CALL
|
||||
self.phone2_state = PhoneState.IN_CALL # Sync both phones
|
||||
self.phone1_client.send("IN_CALL")
|
||||
self._update_phone_button_ui(self.phone1_button, self.phone1_state, "123-4567")
|
||||
self._update_phone_button_ui(self.phone2_button, self.phone2_state, "987-6543")
|
||||
# Start audio timer
|
||||
self.phone1_audio_timer = QTimer(self)
|
||||
self.phone1_audio_timer.timeout.connect(self.send_phone1_audio)
|
||||
self.phone1_audio_timer.start(1000)
|
||||
elif self.phone1_state == PhoneState.IN_CALL:
|
||||
self.phone1_state = PhoneState.IDLE
|
||||
self.phone2_state = PhoneState.IDLE # Sync both phones
|
||||
self.phone1_client.send("CALL_END")
|
||||
self._update_phone_button_ui(self.phone1_button, self.phone1_state)
|
||||
self._update_phone_button_ui(self.phone2_button, self.phone2_state)
|
||||
if hasattr(self, 'phone1_audio_timer'):
|
||||
self.phone1_audio_timer.stop()
|
||||
|
||||
def send_phone1_audio(self):
|
||||
if self.phone1_state == PhoneState.IN_CALL:
|
||||
message = f"Audio packet {random.randint(1, 1000)}"
|
||||
self.phone1_client.send(message)
|
||||
|
||||
def phone2_action(self):
|
||||
print("Phone 2 Action")
|
||||
if self.phone2_state == PhoneState.IDLE:
|
||||
self.phone2_state = PhoneState.CALLING
|
||||
self.phone2_client.send("RINGING")
|
||||
self._update_phone_button_ui(self.phone2_button, self.phone2_state, "987-6543")
|
||||
elif self.phone2_state == PhoneState.CALLING:
|
||||
self.phone2_state = PhoneState.IDLE
|
||||
self.phone2_client.send("CALL_END")
|
||||
self._update_phone_button_ui(self.phone2_button, self.phone2_state)
|
||||
if hasattr(self, 'phone2_audio_timer'):
|
||||
self.phone2_audio_timer.stop()
|
||||
elif self.phone2_state == PhoneState.RINGING:
|
||||
self.phone2_state = PhoneState.IN_CALL
|
||||
self.phone1_state = PhoneState.IN_CALL # Sync both phones
|
||||
self.phone2_client.send("IN_CALL")
|
||||
self._update_phone_button_ui(self.phone2_button, self.phone2_state, "987-6543")
|
||||
self._update_phone_button_ui(self.phone1_button, self.phone1_state, "123-4567")
|
||||
# Start audio timer
|
||||
self.phone2_audio_timer = QTimer(self)
|
||||
self.phone2_audio_timer.timeout.connect(self.send_phone2_audio)
|
||||
self.phone2_audio_timer.start(1000)
|
||||
elif self.phone2_state == PhoneState.IN_CALL:
|
||||
self.phone2_state = PhoneState.IDLE
|
||||
self.phone1_state = PhoneState.IDLE # Sync both phones
|
||||
self.phone2_client.send("CALL_END")
|
||||
self._update_phone_button_ui(self.phone2_button, self.phone2_state)
|
||||
self._update_phone_button_ui(self.phone1_button, self.phone1_state)
|
||||
if hasattr(self, 'phone2_audio_timer'):
|
||||
self.phone2_audio_timer.stop()
|
||||
|
||||
def send_phone2_audio(self):
|
||||
if self.phone2_state == PhoneState.IN_CALL:
|
||||
message = f"Audio packet {random.randint(1, 1000)}"
|
||||
self.phone2_client.send(message)
|
||||
|
||||
def settings_action(self):
|
||||
print("Settings clicked")
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.phone1_client.stop()
|
||||
self.phone2_client.stop()
|
||||
event.accept()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
window = PhoneUI()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
@ -1,5 +1,4 @@
|
||||
import socket
|
||||
|
||||
#external_caller.py
|
||||
import socket
|
||||
import time
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
#external_receiver.py
|
||||
import socket
|
||||
|
||||
def connect():
|
||||
|
@ -1,3 +1,4 @@
|
||||
#gsm_simulator.py
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
Loading…
Reference in New Issue
Block a user