chore: initial project setup
- Added base project structure (core, ui, proto, assets, logs, utils) - Added requirements.txt for dependencies - Added main.py entry point - Configured .gitignore to exclude __pycache__, build, dist, venv, logs, and .spec filesmain
|
|
@ -0,0 +1,27 @@
|
|||
# Python cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
# Build / distribution
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
# PyInstaller spec files
|
||||
*.spec
|
||||
|
||||
# Virtual environment
|
||||
venv/
|
||||
.env/
|
||||
.venv/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
|
||||
# IDE / editor specific
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
|
@ -0,0 +1,159 @@
|
|||
# In core/csv_logger.py
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
from datetime import datetime
|
||||
from PyQt6.QtCore import QObject, pyqtSlot, QTimer
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
from proto.vec_payload_chgSt_pb2 import eventPayload, rpcRequest
|
||||
|
||||
class CsvLogger(QObject):
|
||||
def __init__(self, base_log_directory, session_name):
|
||||
super().__init__()
|
||||
self.base_log_directory = base_log_directory
|
||||
self.session_name = session_name
|
||||
os.makedirs(self.base_log_directory, exist_ok=True)
|
||||
|
||||
self.files = {}
|
||||
self.writers = {}
|
||||
self.queue = queue.Queue()
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self._process_queue)
|
||||
self.num_slots = 9
|
||||
|
||||
def start_logging(self):
|
||||
self.timer.start(100)
|
||||
print(f"✅ CSV logging service started for session: {self.session_name}")
|
||||
|
||||
def _get_writer(self, device_id, file_group):
|
||||
writer_key = (device_id, file_group)
|
||||
if writer_key in self.writers:
|
||||
return self.writers[writer_key]
|
||||
|
||||
try:
|
||||
session_dir = os.path.join(self.base_log_directory, device_id, self.session_name)
|
||||
os.makedirs(session_dir, exist_ok=True)
|
||||
filepath = os.path.join(session_dir, f"{file_group}.csv")
|
||||
|
||||
self.files[writer_key] = open(filepath, 'w', newline='', encoding='utf-8')
|
||||
writer = csv.writer(self.files[writer_key])
|
||||
self.writers[writer_key] = writer
|
||||
|
||||
if file_group == 'PERIODIC':
|
||||
# --- Programmatically build the WIDE header ---
|
||||
base_header = ["Timestamp", "DeviceID", "StationDiagnosticCode"]
|
||||
slot_fields = [
|
||||
"BatteryID", "BatteryPresent", "ChargerPresent", "DoorStatus", "DoorLockStatus",
|
||||
"Voltage_V", "Current_A", "SOC_Percent", "BatteryTemp_C", "SlotTemp_C",
|
||||
"BatteryFaultCode", "ChargerFaultCode", "BatteryMode", "ChargerMode"
|
||||
]
|
||||
slot_header = [f"Slot{i}_{field}" for i in range(1, self.num_slots + 1) for field in slot_fields]
|
||||
header = base_header + slot_header + ["RawHexPayload"]
|
||||
else: # Header for EVENTS_RPC
|
||||
header = ["Timestamp", "Topic", "Payload_JSON", "RawHexPayload"]
|
||||
|
||||
writer.writerow(header)
|
||||
print(f"---> New log file created: {filepath}")
|
||||
return writer
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to create CSV writer for {writer_key}: {e}")
|
||||
return None
|
||||
|
||||
@pyqtSlot(list)
|
||||
def log_data(self, data_list):
|
||||
self.queue.put(data_list)
|
||||
|
||||
def _process_queue(self):
|
||||
while not self.queue.empty():
|
||||
# ==========================================================
|
||||
# ===== EDITED SECTION STARTS HERE =========================
|
||||
# ==========================================================
|
||||
item = None # Define item outside the try block for better error reporting
|
||||
try:
|
||||
# First, get the whole item from the queue.
|
||||
item = self.queue.get()
|
||||
|
||||
# Now, try to unpack it. This is where the ValueError can happen.
|
||||
timestamp_obj, topic, data, raw_payload = item
|
||||
|
||||
# The rest of your logic remains the same
|
||||
parts = topic.split('/')
|
||||
if len(parts) < 5: continue
|
||||
device_id = parts[3]
|
||||
file_group = 'PERIODIC' if topic.endswith('/PERIODIC') else 'EVENTS_RPC'
|
||||
|
||||
writer = self._get_writer(device_id, file_group)
|
||||
if not writer: continue
|
||||
|
||||
if file_group == 'PERIODIC':
|
||||
# --- Build one single WIDE row ---
|
||||
row_data = [
|
||||
datetime.fromtimestamp(data.get("ts")).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
device_id,
|
||||
data.get("stationDiagnosticCode", "N/A")
|
||||
]
|
||||
|
||||
all_slots_data = []
|
||||
slots = data.get("slotLevelPayload", [])
|
||||
num_slot_fields = 14
|
||||
|
||||
for i in range(self.num_slots):
|
||||
if i < len(slots):
|
||||
slot = slots[i]
|
||||
all_slots_data.extend([
|
||||
slot.get('batteryIdentification', ''),
|
||||
"TRUE" if slot.get("batteryPresent") == 1 else "FALSE",
|
||||
"TRUE" if slot.get("chargerPresent") == 1 else "FALSE",
|
||||
"OPEN" if slot.get("doorStatus") == 1 else "CLOSED",
|
||||
"LOCKED" if slot.get("doorLockStatus") == 1 else "UNLOCKED",
|
||||
slot.get('voltage', 0) / 1000.0,
|
||||
slot.get('current', 0) / 1000.0,
|
||||
slot.get('soc', 0),
|
||||
slot.get('batteryMaxTemp', 0) / 10.0,
|
||||
slot.get('slotTemperature', 0) / 10.0,
|
||||
slot.get('batteryFaultCode', 0),
|
||||
slot.get('chargerFaultCode', 0),
|
||||
slot.get('batteryMode', 0),
|
||||
slot.get('chargerMode', 0)
|
||||
])
|
||||
else:
|
||||
all_slots_data.extend([''] * num_slot_fields)
|
||||
|
||||
final_row = row_data + all_slots_data + [raw_payload.hex()]
|
||||
writer.writerow(final_row)
|
||||
else:
|
||||
# Logic for EVENTS and RPC remains the same
|
||||
payload_json_string = json.dumps(data)
|
||||
row = [
|
||||
timestamp_obj.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
||||
topic,
|
||||
payload_json_string,
|
||||
raw_payload.hex()
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
writer_key = (device_id, file_group)
|
||||
if file_handle := self.files.get(writer_key):
|
||||
file_handle.flush()
|
||||
|
||||
except ValueError:
|
||||
# This specifically catches the unpacking error and prints a helpful message.
|
||||
print(f"❌ Error: Malformed item in log queue. Expected 4 values, but got {len(item)}. Item: {item}")
|
||||
continue # Continue to the next item in the queue
|
||||
|
||||
except Exception as e:
|
||||
# A general catch-all for any other unexpected errors.
|
||||
# This message is safe because it doesn't use variables from the try block.
|
||||
print(f"❌ An unexpected error occurred in the logger thread: {e}")
|
||||
continue
|
||||
|
||||
def stop_logging(self):
|
||||
self.timer.stop()
|
||||
self._process_queue()
|
||||
for file in self.files.values():
|
||||
file.close()
|
||||
self.files.clear()
|
||||
self.writers.clear()
|
||||
print(f"🛑 CSV logging stopped for session: {self.session_name}")
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# In core/mqtt_client.py
|
||||
import socket
|
||||
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
class MqttClient(QObject):
|
||||
# --- MODIFIED SIGNAL: Now sends a bool and a string ---
|
||||
connection_status_changed = pyqtSignal(bool, str)
|
||||
message_received = pyqtSignal(str, bytes)
|
||||
connection_error = pyqtSignal(str)
|
||||
stop_logging_signal = pyqtSignal()
|
||||
connected = pyqtSignal()
|
||||
disconnected = pyqtSignal()
|
||||
|
||||
def __init__(self, broker, port, user, password, client_id):
|
||||
super().__init__()
|
||||
self.broker = broker
|
||||
self.port = port
|
||||
|
||||
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id)
|
||||
if user and password:
|
||||
self.client.username_pw_set(user, password)
|
||||
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
self.client.on_message = self.on_message
|
||||
# self.client.on_subscribe = self.on_subscribe
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc, properties):
|
||||
if rc == 0:
|
||||
print("Connection to MQTT Broker successful!")
|
||||
# --- MODIFIED EMIT: Send a success message ---
|
||||
self.connection_status_changed.emit(True, "✅ Connected")
|
||||
else:
|
||||
print(f"Failed to connect, return code {rc}\n")
|
||||
# --- MODIFIED EMIT: Send a failure message ---
|
||||
self.connection_status_changed.emit(False, f"❌ Connection failed (Code: {rc})")
|
||||
self.stop_logging_signal.emit() # Emit the signal
|
||||
self.disconnected.emit()
|
||||
|
||||
def on_message(self, client, userdata, msg):
|
||||
# print(f"Received {len(msg.payload)} bytes of binary data from topic `{msg.topic}`")
|
||||
self.message_received.emit(msg.topic, msg.payload)
|
||||
|
||||
def on_disconnect(self, client, userdata, flags, rc, properties):
|
||||
print("Disconnected from MQTT Broker.")
|
||||
# Change the icon in the line below from 🔌 to 🔴 ❌ 🚫 💔
|
||||
self.connection_status_changed.emit(False, "💔 Disconnected")
|
||||
self.disconnected.emit()
|
||||
|
||||
# --- MODIFIED connect_to_broker METHOD ---
|
||||
def connect_to_broker(self):
|
||||
print(f"Attempting to connect to {self.broker}:{self.port}...")
|
||||
try:
|
||||
self.client.connect(self.broker, self.port, 60)
|
||||
self.client.loop_start()
|
||||
except socket.gaierror:
|
||||
msg = "Host not found. Check internet."
|
||||
print(f"❌ Connection Error: {msg}")
|
||||
self.connection_status_changed.emit(False, f"❌ {msg}")
|
||||
except (socket.error, TimeoutError):
|
||||
msg = "Connection failed. Server offline?"
|
||||
print(f"❌ Connection Error: {msg}")
|
||||
self.connection_status_changed.emit(False, f"❌ {msg}")
|
||||
except Exception as e:
|
||||
msg = f"An unexpected error occurred: {e}"
|
||||
print(f"❌ {msg}")
|
||||
self.connection_status_changed.emit(False, f"❌ Error")
|
||||
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Connects to the broker and starts the network loop.
|
||||
Handles all common connection errors gracefully.
|
||||
"""
|
||||
print(f"Attempting to connect to {self.broker}:{self.port}...")
|
||||
try:
|
||||
# 1. Attempt to connect
|
||||
self.client.connect(self.broker, self.port, 60)
|
||||
|
||||
# 2. Run the blocking network loop
|
||||
# This will run until self.client.disconnect() is called
|
||||
self.client.loop_forever()
|
||||
|
||||
except socket.gaierror:
|
||||
msg = "Host not found. Check the broker address or your internet connection."
|
||||
print(f"❌ {msg}")
|
||||
self.connection_error.emit(msg) # Report error to the main window
|
||||
|
||||
except (socket.error, ConnectionRefusedError):
|
||||
msg = "Connection refused. Is the server offline or the port incorrect?"
|
||||
print(f"❌ {msg}")
|
||||
self.connection_error.emit(msg)
|
||||
|
||||
except TimeoutError:
|
||||
msg = "Connection timed out. The server is not responding."
|
||||
print(f"❌ {msg}")
|
||||
self.connection_error.emit(msg)
|
||||
|
||||
except Exception as e:
|
||||
# Catch any other unexpected errors during connection or loop
|
||||
msg = f"An unexpected error occurred: {e}"
|
||||
print(f"❌ {msg}")
|
||||
self.connection_error.emit(msg)
|
||||
|
||||
# def on_subscribe(self, client, userdata, mid, reason_code_list, properties):
|
||||
# """Callback function for when the broker responds to a subscription request."""
|
||||
# if reason_code_list[0].is_failure:
|
||||
# print(f"❌ Broker rejected subscription: {reason_code_list[0]}")
|
||||
# else:
|
||||
# print(f"✅ Broker accepted subscription with QoS: {reason_code_list[0].value}")
|
||||
|
||||
# --- (The rest of the file remains the same) ---
|
||||
@pyqtSlot()
|
||||
def disconnect_from_broker(self):
|
||||
"""Stops the MQTT client's network loop."""
|
||||
if self.client:
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
print("Stopping MQTT network loop.")
|
||||
|
||||
def subscribe_to_topic(self, topic): # Add qos parameter
|
||||
print(f"Subscribing to topic: {topic}")
|
||||
self.client.subscribe(topic)
|
||||
|
||||
def publish_message(self, topic, payload):
|
||||
self.client.publish(topic, payload)
|
||||
|
||||
def cleanup(self):
|
||||
print("Stopping MQTT network loop.")
|
||||
self.client.loop_stop()
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
|
@ -0,0 +1,16 @@
|
|||
import sys
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from ui.main_window import MainWindow
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# --- DYNAMIC SCALING LOGIC ---
|
||||
BASE_HEIGHT = 1080.0
|
||||
screen = app.primaryScreen()
|
||||
available_height = screen.availableGeometry().height()
|
||||
scale_factor = max(0.7, available_height / BASE_HEIGHT)
|
||||
|
||||
window = MainWindow(scale_factor=scale_factor)
|
||||
window.showMaximized()
|
||||
sys.exit(app.exec())
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
slotLevelPayload.batteryIdentification max_length: 32
|
||||
|
||||
mainPayload.switchStatus max_count: 9 fixed_count:true
|
||||
mainPayload.sessionId max_length: 64
|
||||
mainPayload.slotLevelPayload max_count:9 fixed_count:true
|
||||
mainPayload.deviceId max_length: 30
|
||||
|
||||
eventPayload.deviceId max_length: 30
|
||||
eventPayload.sessionId max_length: 64
|
||||
|
||||
eventData_s.nfcUuid max_length: 64
|
||||
eventData_s.batteryIdentification max_length: 32
|
||||
|
||||
|
||||
|
||||
rpcResponse.deviceId max_length:30
|
||||
rpcResponse.jobId max_length:64
|
||||
|
||||
|
||||
rpcRequest.jobId max_length:64
|
||||
|
||||
rpcData_s.slotsData max_count: 18 fixed_count:true
|
||||
rpcData_s.sessionId max_length: 64
|
||||
|
||||
|
||||
nfcPayload_s.manufacturingData max_length: 16
|
||||
nfcPayload_s.customData max_length: 128
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
syntax = "proto2";
|
||||
|
||||
enum eventType_e {
|
||||
EVENT_SWAP_START = 0x200;
|
||||
EVENT_BATTERY_ENTRY = 0x201;
|
||||
EVENT_BATTERY_EXIT = 0x202;
|
||||
EVENT_ACTIVITY_FAILED = 0x203;
|
||||
EVENT_SWAP_ABORTED = 0x204;
|
||||
EVENT_BATFAULT_ALARM = 0x205;
|
||||
EVENT_SLOT_LOCK_ENEGAGED = 0x206;
|
||||
EVENT_SWAP_ENDED = 0x207;
|
||||
EVENT_CHGFAULT_ALARM = 0x208;
|
||||
EVENT_NFC_SCAN = 0x209;
|
||||
EVENT_SLOT_LOCK_DISENEGAGED = 0x20A;
|
||||
EVENT_REVERSE_SWAP = 0x20B;
|
||||
}
|
||||
|
||||
enum jobType_e {
|
||||
JOBTYPE_NONE = 0;
|
||||
JOBTYPE_GET_STATUS_OF_A_JOB = 0x01;
|
||||
JOBTYPE_SWAP_START = 0x100;
|
||||
JOBTYPE_CHARGER_ENABLE_DISABLE = 0x101;
|
||||
JOBTYPE_GATE_OPEN_CLOSE = 0x102;
|
||||
JOBTYPE_TRANSACTION_ABORT = 0x103;
|
||||
JOBTYPE_REBOOT = 0x104;
|
||||
JOBTYPE_SWAP_DENY = 0x105;
|
||||
JOBTYPE_LANGUAGE_UPDATE = 0x106;
|
||||
}
|
||||
|
||||
enum jobResult_e {
|
||||
JOB_RESULT_UNKNOWN = 0;
|
||||
JOB_RESULT_SUCCESS = 1;
|
||||
JOB_RESULT_REJECTED = 2;
|
||||
JOB_RESULT_TIMEOUT = 3;
|
||||
}
|
||||
|
||||
enum jobStatus_e {
|
||||
JOB_STATUS_IDLE = 0;
|
||||
JOB_STATUS_PENDING = 1;
|
||||
JOB_STATUS_EXECUTING = 2;
|
||||
JOB_STATUS_EXECUTED = 3;
|
||||
}
|
||||
|
||||
message slotLevelPayload{
|
||||
optional uint32 batteryPresent = 1;
|
||||
optional uint32 chargerPresent = 2;
|
||||
optional uint32 doorLockStatus = 3;
|
||||
optional uint32 doorStatus = 4;
|
||||
optional uint32 voltage = 5;
|
||||
optional int32 current = 6;
|
||||
optional uint32 batteryFaultCode = 7;
|
||||
optional uint32 chargerFaultCode = 8;
|
||||
optional int32 batteryMaxTemp = 9;
|
||||
optional int32 chargerMaxTemp= 10;
|
||||
optional string batteryIdentification = 11;
|
||||
optional uint32 batteryMode = 12;
|
||||
optional uint32 chargerMode = 13;
|
||||
optional int32 slotTemperature = 14;
|
||||
optional uint32 gasSensor = 15;
|
||||
optional uint32 soc=16;
|
||||
optional uint32 ts = 17;
|
||||
}
|
||||
|
||||
message mainPayload{
|
||||
required uint32 ts = 1;
|
||||
required string deviceId = 2;
|
||||
required string sessionId = 3;
|
||||
repeated slotLevelPayload slotLevelPayload = 4;
|
||||
optional uint32 backupSupplyStatus = 5;
|
||||
repeated uint32 switchStatus = 6;
|
||||
optional uint32 stationStatus = 7;
|
||||
optional uint32 stationDiagnosticCode = 8;
|
||||
repeated float coordinates = 9;
|
||||
}
|
||||
|
||||
enum swapAbortReason_e{
|
||||
ABORT_UNKNOWN=0;
|
||||
ABORT_BAT_EXIT_TIMEOUT=1;
|
||||
ABORT_BAT_ENTRY_TIMEOUT=2;
|
||||
ABORT_DOOR_CLOSE_TIMEOUT=3;
|
||||
ABORT_DOOR_OPEN_TIMEOUT=4;
|
||||
ABORT_INVALID_PARAM=5;
|
||||
ABORT_REMOTE_REQUESTED=6;
|
||||
ABORT_INVALID_BATTERY=7;
|
||||
}
|
||||
|
||||
enum swapDenyReason_e{
|
||||
SWAP_DENY_INSUFFICIENT_BAL=1;
|
||||
SWAP_DENY_INVALID_NFC=2;
|
||||
SWAP_DENY_BATTERY_UNAVAILABLE=3;
|
||||
}
|
||||
|
||||
enum languageType_e{
|
||||
LANGUAGE_TYPE_ENGLISH = 1;
|
||||
LANGUAGE_TYPE_HINDI = 2;
|
||||
LANGUAGE_TYPE_KANNADA = 3;
|
||||
LANGUAGE_TYPE_TELUGU = 4;
|
||||
}
|
||||
|
||||
message nfcPayload_s{
|
||||
required string manufacturingData = 1;
|
||||
required string customData = 2;
|
||||
}
|
||||
|
||||
message eventData_s {
|
||||
optional nfcPayload_s nfcData = 1;
|
||||
optional string batteryIdentification = 2;
|
||||
optional uint32 activityFailureReason = 3;
|
||||
optional swapAbortReason_e swapAbortReason = 4;
|
||||
optional uint32 swapTime = 5;
|
||||
optional uint32 faultCode = 6;
|
||||
optional uint32 doorStatus = 7;
|
||||
optional uint32 slotId = 8;
|
||||
}
|
||||
|
||||
message eventPayload {
|
||||
required uint32 ts = 1;
|
||||
required string deviceId = 2;
|
||||
required eventType_e eventType = 3;
|
||||
required string sessionId = 4;
|
||||
optional eventData_s eventData = 5;
|
||||
}
|
||||
|
||||
message rpcData_s {
|
||||
optional string sessionId = 1;
|
||||
repeated uint32 slotsData = 2;
|
||||
}
|
||||
|
||||
message slotControl_s {
|
||||
required uint32 slotId = 1;
|
||||
required uint32 state = 2;
|
||||
}
|
||||
|
||||
message getJobStatusByJobId_s{
|
||||
required string jobId = 1;
|
||||
}
|
||||
|
||||
message rpcRequest {
|
||||
required uint32 ts = 1;
|
||||
required string jobId = 2;
|
||||
required jobType_e jobType = 3;
|
||||
optional rpcData_s rpcData = 4;
|
||||
optional slotControl_s slotInfo = 5;
|
||||
optional swapDenyReason_e swapDeny = 8;
|
||||
optional getJobStatusByJobId_s getJobStatusByJobId = 9;
|
||||
optional languageType_e languageType = 10;
|
||||
}
|
||||
|
||||
message jobStatusByJobIdResponse_s{
|
||||
required string jobId = 1;
|
||||
required jobStatus_e jobStatus = 2;
|
||||
required jobResult_e jobResult = 3;
|
||||
}
|
||||
|
||||
message rpcResponse {
|
||||
required uint32 ts = 1;
|
||||
required string deviceId = 2;
|
||||
required string jobId = 3;
|
||||
required jobStatus_e jobStatus = 4;
|
||||
required jobResult_e jobResult = 5;
|
||||
optional jobStatusByJobIdResponse_s jobStatusByJobIdResponse = 6;
|
||||
}
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
|
||||
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class eventType_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
EVENT_SWAP_START: _ClassVar[eventType_e]
|
||||
EVENT_BATTERY_ENTRY: _ClassVar[eventType_e]
|
||||
EVENT_BATTERY_EXIT: _ClassVar[eventType_e]
|
||||
EVENT_ACTIVITY_FAILED: _ClassVar[eventType_e]
|
||||
EVENT_SWAP_ABORTED: _ClassVar[eventType_e]
|
||||
EVENT_BATFAULT_ALARM: _ClassVar[eventType_e]
|
||||
EVENT_SLOT_LOCK_ENEGAGED: _ClassVar[eventType_e]
|
||||
EVENT_SWAP_ENDED: _ClassVar[eventType_e]
|
||||
EVENT_CHGFAULT_ALARM: _ClassVar[eventType_e]
|
||||
EVENT_NFC_SCAN: _ClassVar[eventType_e]
|
||||
EVENT_SLOT_LOCK_DISENEGAGED: _ClassVar[eventType_e]
|
||||
EVENT_REVERSE_SWAP: _ClassVar[eventType_e]
|
||||
|
||||
class jobType_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
JOBTYPE_NONE: _ClassVar[jobType_e]
|
||||
JOBTYPE_GET_STATUS_OF_A_JOB: _ClassVar[jobType_e]
|
||||
JOBTYPE_SWAP_START: _ClassVar[jobType_e]
|
||||
JOBTYPE_CHARGER_ENABLE_DISABLE: _ClassVar[jobType_e]
|
||||
JOBTYPE_GATE_OPEN_CLOSE: _ClassVar[jobType_e]
|
||||
JOBTYPE_TRANSACTION_ABORT: _ClassVar[jobType_e]
|
||||
JOBTYPE_REBOOT: _ClassVar[jobType_e]
|
||||
JOBTYPE_SWAP_DENY: _ClassVar[jobType_e]
|
||||
JOBTYPE_LANGUAGE_UPDATE: _ClassVar[jobType_e]
|
||||
|
||||
class jobResult_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
JOB_RESULT_UNKNOWN: _ClassVar[jobResult_e]
|
||||
JOB_RESULT_SUCCESS: _ClassVar[jobResult_e]
|
||||
JOB_RESULT_REJECTED: _ClassVar[jobResult_e]
|
||||
JOB_RESULT_TIMEOUT: _ClassVar[jobResult_e]
|
||||
|
||||
class jobStatus_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
JOB_STATUS_IDLE: _ClassVar[jobStatus_e]
|
||||
JOB_STATUS_PENDING: _ClassVar[jobStatus_e]
|
||||
JOB_STATUS_EXECUTING: _ClassVar[jobStatus_e]
|
||||
JOB_STATUS_EXECUTED: _ClassVar[jobStatus_e]
|
||||
|
||||
class swapAbortReason_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
ABORT_UNKNOWN: _ClassVar[swapAbortReason_e]
|
||||
ABORT_BAT_EXIT_TIMEOUT: _ClassVar[swapAbortReason_e]
|
||||
ABORT_BAT_ENTRY_TIMEOUT: _ClassVar[swapAbortReason_e]
|
||||
ABORT_DOOR_CLOSE_TIMEOUT: _ClassVar[swapAbortReason_e]
|
||||
ABORT_DOOR_OPEN_TIMEOUT: _ClassVar[swapAbortReason_e]
|
||||
ABORT_INVALID_PARAM: _ClassVar[swapAbortReason_e]
|
||||
ABORT_REMOTE_REQUESTED: _ClassVar[swapAbortReason_e]
|
||||
ABORT_INVALID_BATTERY: _ClassVar[swapAbortReason_e]
|
||||
|
||||
class swapDenyReason_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
SWAP_DENY_INSUFFICIENT_BAL: _ClassVar[swapDenyReason_e]
|
||||
SWAP_DENY_INVALID_NFC: _ClassVar[swapDenyReason_e]
|
||||
SWAP_DENY_BATTERY_UNAVAILABLE: _ClassVar[swapDenyReason_e]
|
||||
|
||||
class languageType_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
LANGUAGE_TYPE_ENGLISH: _ClassVar[languageType_e]
|
||||
LANGUAGE_TYPE_HINDI: _ClassVar[languageType_e]
|
||||
LANGUAGE_TYPE_KANNADA: _ClassVar[languageType_e]
|
||||
LANGUAGE_TYPE_TELUGU: _ClassVar[languageType_e]
|
||||
EVENT_SWAP_START: eventType_e
|
||||
EVENT_BATTERY_ENTRY: eventType_e
|
||||
EVENT_BATTERY_EXIT: eventType_e
|
||||
EVENT_ACTIVITY_FAILED: eventType_e
|
||||
EVENT_SWAP_ABORTED: eventType_e
|
||||
EVENT_BATFAULT_ALARM: eventType_e
|
||||
EVENT_SLOT_LOCK_ENEGAGED: eventType_e
|
||||
EVENT_SWAP_ENDED: eventType_e
|
||||
EVENT_CHGFAULT_ALARM: eventType_e
|
||||
EVENT_NFC_SCAN: eventType_e
|
||||
EVENT_SLOT_LOCK_DISENEGAGED: eventType_e
|
||||
EVENT_REVERSE_SWAP: eventType_e
|
||||
JOBTYPE_NONE: jobType_e
|
||||
JOBTYPE_GET_STATUS_OF_A_JOB: jobType_e
|
||||
JOBTYPE_SWAP_START: jobType_e
|
||||
JOBTYPE_CHARGER_ENABLE_DISABLE: jobType_e
|
||||
JOBTYPE_GATE_OPEN_CLOSE: jobType_e
|
||||
JOBTYPE_TRANSACTION_ABORT: jobType_e
|
||||
JOBTYPE_REBOOT: jobType_e
|
||||
JOBTYPE_SWAP_DENY: jobType_e
|
||||
JOBTYPE_LANGUAGE_UPDATE: jobType_e
|
||||
JOB_RESULT_UNKNOWN: jobResult_e
|
||||
JOB_RESULT_SUCCESS: jobResult_e
|
||||
JOB_RESULT_REJECTED: jobResult_e
|
||||
JOB_RESULT_TIMEOUT: jobResult_e
|
||||
JOB_STATUS_IDLE: jobStatus_e
|
||||
JOB_STATUS_PENDING: jobStatus_e
|
||||
JOB_STATUS_EXECUTING: jobStatus_e
|
||||
JOB_STATUS_EXECUTED: jobStatus_e
|
||||
ABORT_UNKNOWN: swapAbortReason_e
|
||||
ABORT_BAT_EXIT_TIMEOUT: swapAbortReason_e
|
||||
ABORT_BAT_ENTRY_TIMEOUT: swapAbortReason_e
|
||||
ABORT_DOOR_CLOSE_TIMEOUT: swapAbortReason_e
|
||||
ABORT_DOOR_OPEN_TIMEOUT: swapAbortReason_e
|
||||
ABORT_INVALID_PARAM: swapAbortReason_e
|
||||
ABORT_REMOTE_REQUESTED: swapAbortReason_e
|
||||
ABORT_INVALID_BATTERY: swapAbortReason_e
|
||||
SWAP_DENY_INSUFFICIENT_BAL: swapDenyReason_e
|
||||
SWAP_DENY_INVALID_NFC: swapDenyReason_e
|
||||
SWAP_DENY_BATTERY_UNAVAILABLE: swapDenyReason_e
|
||||
LANGUAGE_TYPE_ENGLISH: languageType_e
|
||||
LANGUAGE_TYPE_HINDI: languageType_e
|
||||
LANGUAGE_TYPE_KANNADA: languageType_e
|
||||
LANGUAGE_TYPE_TELUGU: languageType_e
|
||||
|
||||
class slotLevelPayload(_message.Message):
|
||||
__slots__ = ("batteryPresent", "chargerPresent", "doorLockStatus", "doorStatus", "voltage", "current", "batteryFaultCode", "chargerFaultCode", "batteryMaxTemp", "chargerMaxTemp", "batteryIdentification", "batteryMode", "chargerMode", "slotTemperature", "gasSensor", "soc", "ts")
|
||||
BATTERYPRESENT_FIELD_NUMBER: _ClassVar[int]
|
||||
CHARGERPRESENT_FIELD_NUMBER: _ClassVar[int]
|
||||
DOORLOCKSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
DOORSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
VOLTAGE_FIELD_NUMBER: _ClassVar[int]
|
||||
CURRENT_FIELD_NUMBER: _ClassVar[int]
|
||||
BATTERYFAULTCODE_FIELD_NUMBER: _ClassVar[int]
|
||||
CHARGERFAULTCODE_FIELD_NUMBER: _ClassVar[int]
|
||||
BATTERYMAXTEMP_FIELD_NUMBER: _ClassVar[int]
|
||||
CHARGERMAXTEMP_FIELD_NUMBER: _ClassVar[int]
|
||||
BATTERYIDENTIFICATION_FIELD_NUMBER: _ClassVar[int]
|
||||
BATTERYMODE_FIELD_NUMBER: _ClassVar[int]
|
||||
CHARGERMODE_FIELD_NUMBER: _ClassVar[int]
|
||||
SLOTTEMPERATURE_FIELD_NUMBER: _ClassVar[int]
|
||||
GASSENSOR_FIELD_NUMBER: _ClassVar[int]
|
||||
SOC_FIELD_NUMBER: _ClassVar[int]
|
||||
TS_FIELD_NUMBER: _ClassVar[int]
|
||||
batteryPresent: int
|
||||
chargerPresent: int
|
||||
doorLockStatus: int
|
||||
doorStatus: int
|
||||
voltage: int
|
||||
current: int
|
||||
batteryFaultCode: int
|
||||
chargerFaultCode: int
|
||||
batteryMaxTemp: int
|
||||
chargerMaxTemp: int
|
||||
batteryIdentification: str
|
||||
batteryMode: int
|
||||
chargerMode: int
|
||||
slotTemperature: int
|
||||
gasSensor: int
|
||||
soc: int
|
||||
ts: int
|
||||
def __init__(self, batteryPresent: _Optional[int] = ..., chargerPresent: _Optional[int] = ..., doorLockStatus: _Optional[int] = ..., doorStatus: _Optional[int] = ..., voltage: _Optional[int] = ..., current: _Optional[int] = ..., batteryFaultCode: _Optional[int] = ..., chargerFaultCode: _Optional[int] = ..., batteryMaxTemp: _Optional[int] = ..., chargerMaxTemp: _Optional[int] = ..., batteryIdentification: _Optional[str] = ..., batteryMode: _Optional[int] = ..., chargerMode: _Optional[int] = ..., slotTemperature: _Optional[int] = ..., gasSensor: _Optional[int] = ..., soc: _Optional[int] = ..., ts: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class mainPayload(_message.Message):
|
||||
__slots__ = ("ts", "deviceId", "sessionId", "slotLevelPayload", "backupSupplyStatus", "switchStatus", "stationStatus", "stationDiagnosticCode", "coordinates")
|
||||
TS_FIELD_NUMBER: _ClassVar[int]
|
||||
DEVICEID_FIELD_NUMBER: _ClassVar[int]
|
||||
SESSIONID_FIELD_NUMBER: _ClassVar[int]
|
||||
SLOTLEVELPAYLOAD_FIELD_NUMBER: _ClassVar[int]
|
||||
BACKUPSUPPLYSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
SWITCHSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
STATIONSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
STATIONDIAGNOSTICCODE_FIELD_NUMBER: _ClassVar[int]
|
||||
COORDINATES_FIELD_NUMBER: _ClassVar[int]
|
||||
ts: int
|
||||
deviceId: str
|
||||
sessionId: str
|
||||
slotLevelPayload: _containers.RepeatedCompositeFieldContainer[slotLevelPayload]
|
||||
backupSupplyStatus: int
|
||||
switchStatus: _containers.RepeatedScalarFieldContainer[int]
|
||||
stationStatus: int
|
||||
stationDiagnosticCode: int
|
||||
coordinates: _containers.RepeatedScalarFieldContainer[float]
|
||||
def __init__(self, ts: _Optional[int] = ..., deviceId: _Optional[str] = ..., sessionId: _Optional[str] = ..., slotLevelPayload: _Optional[_Iterable[_Union[slotLevelPayload, _Mapping]]] = ..., backupSupplyStatus: _Optional[int] = ..., switchStatus: _Optional[_Iterable[int]] = ..., stationStatus: _Optional[int] = ..., stationDiagnosticCode: _Optional[int] = ..., coordinates: _Optional[_Iterable[float]] = ...) -> None: ...
|
||||
|
||||
class nfcPayload_s(_message.Message):
|
||||
__slots__ = ("manufacturingData", "customData")
|
||||
MANUFACTURINGDATA_FIELD_NUMBER: _ClassVar[int]
|
||||
CUSTOMDATA_FIELD_NUMBER: _ClassVar[int]
|
||||
manufacturingData: str
|
||||
customData: str
|
||||
def __init__(self, manufacturingData: _Optional[str] = ..., customData: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class eventData_s(_message.Message):
|
||||
__slots__ = ("nfcData", "batteryIdentification", "activityFailureReason", "swapAbortReason", "swapTime", "faultCode", "doorStatus", "slotId")
|
||||
NFCDATA_FIELD_NUMBER: _ClassVar[int]
|
||||
BATTERYIDENTIFICATION_FIELD_NUMBER: _ClassVar[int]
|
||||
ACTIVITYFAILUREREASON_FIELD_NUMBER: _ClassVar[int]
|
||||
SWAPABORTREASON_FIELD_NUMBER: _ClassVar[int]
|
||||
SWAPTIME_FIELD_NUMBER: _ClassVar[int]
|
||||
FAULTCODE_FIELD_NUMBER: _ClassVar[int]
|
||||
DOORSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
SLOTID_FIELD_NUMBER: _ClassVar[int]
|
||||
nfcData: nfcPayload_s
|
||||
batteryIdentification: str
|
||||
activityFailureReason: int
|
||||
swapAbortReason: swapAbortReason_e
|
||||
swapTime: int
|
||||
faultCode: int
|
||||
doorStatus: int
|
||||
slotId: int
|
||||
def __init__(self, nfcData: _Optional[_Union[nfcPayload_s, _Mapping]] = ..., batteryIdentification: _Optional[str] = ..., activityFailureReason: _Optional[int] = ..., swapAbortReason: _Optional[_Union[swapAbortReason_e, str]] = ..., swapTime: _Optional[int] = ..., faultCode: _Optional[int] = ..., doorStatus: _Optional[int] = ..., slotId: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class eventPayload(_message.Message):
|
||||
__slots__ = ("ts", "deviceId", "eventType", "sessionId", "eventData")
|
||||
TS_FIELD_NUMBER: _ClassVar[int]
|
||||
DEVICEID_FIELD_NUMBER: _ClassVar[int]
|
||||
EVENTTYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
SESSIONID_FIELD_NUMBER: _ClassVar[int]
|
||||
EVENTDATA_FIELD_NUMBER: _ClassVar[int]
|
||||
ts: int
|
||||
deviceId: str
|
||||
eventType: eventType_e
|
||||
sessionId: str
|
||||
eventData: eventData_s
|
||||
def __init__(self, ts: _Optional[int] = ..., deviceId: _Optional[str] = ..., eventType: _Optional[_Union[eventType_e, str]] = ..., sessionId: _Optional[str] = ..., eventData: _Optional[_Union[eventData_s, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class rpcData_s(_message.Message):
|
||||
__slots__ = ("sessionId", "slotsData")
|
||||
SESSIONID_FIELD_NUMBER: _ClassVar[int]
|
||||
SLOTSDATA_FIELD_NUMBER: _ClassVar[int]
|
||||
sessionId: str
|
||||
slotsData: _containers.RepeatedScalarFieldContainer[int]
|
||||
def __init__(self, sessionId: _Optional[str] = ..., slotsData: _Optional[_Iterable[int]] = ...) -> None: ...
|
||||
|
||||
class slotControl_s(_message.Message):
|
||||
__slots__ = ("slotId", "state")
|
||||
SLOTID_FIELD_NUMBER: _ClassVar[int]
|
||||
STATE_FIELD_NUMBER: _ClassVar[int]
|
||||
slotId: int
|
||||
state: int
|
||||
def __init__(self, slotId: _Optional[int] = ..., state: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class getJobStatusByJobId_s(_message.Message):
|
||||
__slots__ = ("jobId",)
|
||||
JOBID_FIELD_NUMBER: _ClassVar[int]
|
||||
jobId: str
|
||||
def __init__(self, jobId: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class rpcRequest(_message.Message):
|
||||
__slots__ = ("ts", "jobId", "jobType", "rpcData", "slotInfo", "swapDeny", "getJobStatusByJobId", "languageType")
|
||||
TS_FIELD_NUMBER: _ClassVar[int]
|
||||
JOBID_FIELD_NUMBER: _ClassVar[int]
|
||||
JOBTYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
RPCDATA_FIELD_NUMBER: _ClassVar[int]
|
||||
SLOTINFO_FIELD_NUMBER: _ClassVar[int]
|
||||
SWAPDENY_FIELD_NUMBER: _ClassVar[int]
|
||||
GETJOBSTATUSBYJOBID_FIELD_NUMBER: _ClassVar[int]
|
||||
LANGUAGETYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
ts: int
|
||||
jobId: str
|
||||
jobType: jobType_e
|
||||
rpcData: rpcData_s
|
||||
slotInfo: slotControl_s
|
||||
swapDeny: swapDenyReason_e
|
||||
getJobStatusByJobId: getJobStatusByJobId_s
|
||||
languageType: languageType_e
|
||||
def __init__(self, ts: _Optional[int] = ..., jobId: _Optional[str] = ..., jobType: _Optional[_Union[jobType_e, str]] = ..., rpcData: _Optional[_Union[rpcData_s, _Mapping]] = ..., slotInfo: _Optional[_Union[slotControl_s, _Mapping]] = ..., swapDeny: _Optional[_Union[swapDenyReason_e, str]] = ..., getJobStatusByJobId: _Optional[_Union[getJobStatusByJobId_s, _Mapping]] = ..., languageType: _Optional[_Union[languageType_e, str]] = ...) -> None: ...
|
||||
|
||||
class jobStatusByJobIdResponse_s(_message.Message):
|
||||
__slots__ = ("jobId", "jobStatus", "jobResult")
|
||||
JOBID_FIELD_NUMBER: _ClassVar[int]
|
||||
JOBSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
JOBRESULT_FIELD_NUMBER: _ClassVar[int]
|
||||
jobId: str
|
||||
jobStatus: jobStatus_e
|
||||
jobResult: jobResult_e
|
||||
def __init__(self, jobId: _Optional[str] = ..., jobStatus: _Optional[_Union[jobStatus_e, str]] = ..., jobResult: _Optional[_Union[jobResult_e, str]] = ...) -> None: ...
|
||||
|
||||
class rpcResponse(_message.Message):
|
||||
__slots__ = ("ts", "deviceId", "jobId", "jobStatus", "jobResult", "jobStatusByJobIdResponse")
|
||||
TS_FIELD_NUMBER: _ClassVar[int]
|
||||
DEVICEID_FIELD_NUMBER: _ClassVar[int]
|
||||
JOBID_FIELD_NUMBER: _ClassVar[int]
|
||||
JOBSTATUS_FIELD_NUMBER: _ClassVar[int]
|
||||
JOBRESULT_FIELD_NUMBER: _ClassVar[int]
|
||||
JOBSTATUSBYJOBIDRESPONSE_FIELD_NUMBER: _ClassVar[int]
|
||||
ts: int
|
||||
deviceId: str
|
||||
jobId: str
|
||||
jobStatus: jobStatus_e
|
||||
jobResult: jobResult_e
|
||||
jobStatusByJobIdResponse: jobStatusByJobIdResponse_s
|
||||
def __init__(self, ts: _Optional[int] = ..., deviceId: _Optional[str] = ..., jobId: _Optional[str] = ..., jobStatus: _Optional[_Union[jobStatus_e, str]] = ..., jobResult: _Optional[_Union[jobResult_e, str]] = ..., jobStatusByJobIdResponse: _Optional[_Union[jobStatusByJobIdResponse_s, _Mapping]] = ...) -> None: ...
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
# --- Dynamic Theme Stylesheets ---
|
||||
|
||||
def get_light_theme_styles(scale=1.0):
|
||||
|
||||
log_font_size = max(10, int(11 * scale))
|
||||
button_font_size = max(7, int(10 * scale))
|
||||
|
||||
return f"""
|
||||
QMainWindow, QWidget {{
|
||||
background-color: #f0f0f0;
|
||||
color: #000;
|
||||
}}
|
||||
#LogPanel {{
|
||||
font-family: "Courier New", Consolas, monospace;
|
||||
font-size: {log_font_size}pt;
|
||||
background-color: #212121;
|
||||
color: #eceff1;
|
||||
border: 1px solid #455a64;
|
||||
}}
|
||||
QGroupBox {{
|
||||
font-family: Arial;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: {int(8 * scale)}px;
|
||||
margin-top: {int(6 * scale)}px;
|
||||
}}
|
||||
QGroupBox::title {{
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top center;
|
||||
padding: 0 {int(10 * scale)}px;
|
||||
color: #000;
|
||||
}}
|
||||
QTabWidget::pane {{ border-top: 2px solid #c8c8c8; }}
|
||||
QTabBar::tab {{
|
||||
background: #e1e1e1; border: 1px solid #c8c8c8;
|
||||
padding: {int(6 * scale)}px {int(15 * scale)}px;
|
||||
border-top-left-radius: {int(4 * scale)}px; border-top-right-radius: {int(4 * scale)}px;
|
||||
}}
|
||||
QTabBar::tab:selected {{ background: #f0f0f0; border-bottom-color: #f0f0f0; }}
|
||||
QFormLayout::label {{ color: #000; padding-top: {int(3 * scale)}px; }}
|
||||
QLineEdit, QPlainTextEdit, QComboBox {{
|
||||
background-color: #fff; border: 1px solid #c8c8c8;
|
||||
border-radius: {int(4 * scale)}px; padding: {int(4 * scale)}px;
|
||||
font-size: {max(7, int(9 * scale))}pt;
|
||||
}}
|
||||
QLineEdit:read-only {{ background-color: #e9e9e9; }}
|
||||
QPushButton {{
|
||||
background-color: #e1e1e1; border: 1px solid #c8c8c8;
|
||||
padding: {int(5 * scale)}px {int(10 * scale)}px;
|
||||
border-radius: {int(4 * scale)}px;
|
||||
}}
|
||||
QPushButton:hover {{ background-color: #dcdcdc; }}
|
||||
QPushButton:pressed {{ background-color: #c8c8c8; }}
|
||||
#RefreshButton, #ResetButton {{
|
||||
padding: {int(6 * scale)}px {int(16 * scale)}px;
|
||||
font-size: {button_font_size * 1.3}pt;
|
||||
font-weight: bold;
|
||||
border-radius: {int(4*scale)}px;
|
||||
}}
|
||||
#RefreshButton {{
|
||||
background-color: #2e7d32; /* A slightly darker green */
|
||||
}}
|
||||
#ResetButton {{
|
||||
background-color: #c62828; /* A slightly darker red */
|
||||
}}
|
||||
#ChamberOpenDoorButton, #ChamberChgOnButton, #ChamberChgOffButton {{
|
||||
padding: {int(8 * scale)}px;
|
||||
font-size: {button_font_size}pt;
|
||||
font-weight: bold;
|
||||
border-radius: {int(4*scale)}px;
|
||||
}}
|
||||
|
||||
#ChamberOpenDoorButton {{ background-color: #E1E1E1; }}
|
||||
#ChamberChgOnButton {{ background-color: #E1E1E1; }}
|
||||
#ChamberChgOffButton {{ background-color: #E1E1E1; }}
|
||||
|
||||
#ChamberOpenDoorButton:hover {{ background-color: #3498DB; }}
|
||||
#ChamberChgOnButton:hover {{ background-color: #229954; }}
|
||||
#ChamberChgOffButton:hover {{ background-color: #c0392b; }}
|
||||
|
||||
QPushButton:disabled {{ background-color: #d3d3d3; color: #a0a0a0; }}
|
||||
|
||||
#ConnectButton, #DisconnectButton {{
|
||||
padding: {int(6 * scale)}px {int(16 * scale)}px;
|
||||
font-size: {button_font_size}pt;
|
||||
font-weight: bold;
|
||||
border-radius: {int(4 * scale)}px;
|
||||
color: white;
|
||||
}}
|
||||
|
||||
#ConnectButton {{ background-color: #27ae60; }} /* Green */
|
||||
#DisconnectButton {{ background-color: #c0392b; }} /* Red */
|
||||
|
||||
#ConnectButton:hover {{ background-color: #52be80; }}
|
||||
#DisconnectButton:hover {{ background-color: #cd6155; }}
|
||||
|
||||
#ConnectButton:pressed {{ background-color: #52be80; }}
|
||||
#DisconnectButton:pressed {{ background-color: #cd6155; }}
|
||||
|
||||
#ConnectButton:disabled, #DisconnectButton:disabled {{
|
||||
background-color: #546e7a;
|
||||
color: #90a4ae;
|
||||
}}
|
||||
|
||||
#RefreshButton, #StartSwapButton {{ background-color: #27ae60; color: white; border: none; }}
|
||||
#RefreshButton:hover, #StartSwapButton:hover {{ background-color: #229954; }}
|
||||
#ResetButton, #AbortSwapButton {{ background-color: #c0392b; color: white; border: none; }}
|
||||
#ResetButton:hover, #AbortSwapButton:hover {{ background-color: #c0392b; }}
|
||||
#SendAudioButton {{ background-color: #3498db; color: white; border: none; font-size: {max(10, int(14 * scale))}px; }}
|
||||
#SendAudioButton:hover {{ background-color: #2980b9; }}
|
||||
QLabel[status="present"] {{ background-color: #2ecc71; color: white; border-radius: {int(4*scale)}px; padding: {int(3*scale)}px; }}
|
||||
QLabel[status="absent"] {{ background-color: #e74c3c; color: white; border-radius: {int(4*scale)}px; padding: {int(3*scale)}px; }}
|
||||
QLabel[alarm="active"] {{ background-color: #e74c3c; color: white; font-weight: bold; border-radius: {int(4*scale)}px; padding: {int(2*scale)}px; }}
|
||||
QLabel[alarm="inactive"] {{ background-color: transparent; color: black; }}
|
||||
QGroupBox#ChamberWidget {{ border: 2px solid #3498db; }}
|
||||
"""
|
||||
|
||||
def get_dark_theme_styles(scale=1.0):
|
||||
|
||||
log_font_size = max(10, int(11 * scale))
|
||||
button_font_size = max(7, int(10 * scale))
|
||||
|
||||
return f"""
|
||||
QMainWindow, QWidget {{ background-color: #2b2b2b; color: #f0f0f0; }}
|
||||
#LogPanel {{
|
||||
font-family: "Courier New", Consolas, monospace;
|
||||
font-size: {log_font_size}pt;
|
||||
background-color: #212121;
|
||||
color: #eceff1;
|
||||
border: 1px solid #455a64;
|
||||
}}
|
||||
QGroupBox {{
|
||||
font-family: Arial; border: 1px solid #4a4a4a;
|
||||
border-radius: {int(8 * scale)}px; margin-top: {int(6 * scale)}px;
|
||||
}}
|
||||
QGroupBox::title {{ subcontrol-origin: margin; subcontrol-position: top center; padding: 0 {int(10 * scale)}px; color: #f0f0f0; }}
|
||||
QTabWidget::pane {{ border-top: 2px solid #4a4a4a; }}
|
||||
QTabBar::tab {{
|
||||
background: #3c3c3c; border: 1px solid #4a4a4a; color: #f0f0f0;
|
||||
padding: {int(6 * scale)}px {int(15 * scale)}px;
|
||||
border-top-left-radius: {int(4 * scale)}px; border-top-right-radius: {int(4 * scale)}px;
|
||||
}}
|
||||
QTabBar::tab:selected {{ background: #2b2b2b; border-bottom-color: #2b2b2b; }}
|
||||
QFormLayout::label {{ color: #f0f0f0; padding-top: {int(3 * scale)}px; }}
|
||||
QLineEdit, QPlainTextEdit, QComboBox {{
|
||||
background-color: #3c3c3c; border: 1px solid #4a4a4a;
|
||||
border-radius: {int(4 * scale)}px; padding: {int(4 * scale)}px; color: #f0f0f0;
|
||||
font-size: {max(7, int(9 * scale))}pt;
|
||||
}}
|
||||
QLineEdit:read-only {{ background-color: #333333; }}
|
||||
QPushButton {{
|
||||
background-color: #555555; border: 1px solid #4a4a4a;
|
||||
padding: {int(5 * scale)}px {int(10 * scale)}px;
|
||||
border-radius: {int(4 * scale)}px; color: #f0f0f0;
|
||||
}}
|
||||
QPushButton:hover {{ background-color: #6a6a6a; }}
|
||||
QPushButton:pressed {{ background-color: #4a4a4a; }}
|
||||
QPushButton:disabled {{ background-color: #404040; color: #888888; }}
|
||||
#RefreshButton, #ResetButton {{
|
||||
padding: {int(6 * scale)}px {int(16 * scale)}px;
|
||||
font-size: {button_font_size * 1.3}pt;
|
||||
font-weight: bold;
|
||||
border-radius: {int(4*scale)}px;
|
||||
}}
|
||||
#RefreshButton {{
|
||||
background-color: #2e7d32; /* A slightly darker green */
|
||||
}}
|
||||
#ResetButton {{
|
||||
background-color: #c62828; /* A slightly darker red */
|
||||
}}
|
||||
#ChamberOpenDoorButton, #ChamberChgOnButton, #ChamberChgOffButton {{
|
||||
padding: {int(8 * scale)}px;
|
||||
font-size: {button_font_size}pt;
|
||||
font-weight: bold;
|
||||
border-radius: {int(4*scale)}px;
|
||||
}}
|
||||
|
||||
#ChamberOpenDoorButton {{ background-color: #3C3C3C; }}
|
||||
#ChamberChgOnButton {{ background-color: #3C3C3C; }}
|
||||
#ChamberChgOffButton {{ background-color: #3C3C3C; }}
|
||||
|
||||
#ChamberOpenDoorButton:hover {{ background-color: #607d8b; }}
|
||||
#ChamberChgOnButton:hover {{ background-color: #52be80; }}
|
||||
#ChamberChgOffButton:hover {{ background-color: #cd6155; }}
|
||||
|
||||
#ConnectButton, #DisconnectButton {{
|
||||
padding: {int(6 * scale)}px {int(16 * scale)}px;
|
||||
font-size: {button_font_size}pt;
|
||||
font-weight: bold;
|
||||
border-radius: {int(4*scale)}px;
|
||||
color: white;
|
||||
}}
|
||||
|
||||
#ConnectButton {{ background-color: #27ae60; }} /* Green */
|
||||
#DisconnectButton {{ background-color: #c0392b; }} /* Red */
|
||||
|
||||
#ConnectButton:hover {{ background-color: #52be80; }}
|
||||
#DisconnectButton:hover {{ background-color: #cd6155; }}
|
||||
|
||||
#ConnectButton:pressed {{ background-color: #52be80; }}
|
||||
#DisconnectButton:pressed {{ background-color: #cd6155; }}
|
||||
|
||||
#ConnectButton:disabled, #DisconnectButton:disabled {{
|
||||
background-color: #546e7a;
|
||||
color: #90a4ae;
|
||||
}}
|
||||
|
||||
#RefreshButton, #StartSwapButton {{ background-color: #27ae60; color: white; border: none; }}
|
||||
#RefreshButton:hover, #StartSwapButton:hover {{ background-color: #52be80; }}
|
||||
#ResetButton, #AbortSwapButton {{ background-color: #c0392b; color: white; border: none; }}
|
||||
#ResetButton:hover, #AbortSwapButton:hover {{ background-color: #cd6155; }}
|
||||
#SendAudioButton {{ background-color: #3498db; color: white; border: none; font-size: {max(10, int(14 * scale))}px; }}
|
||||
#SendAudioButton:hover {{ background-color: #5dade2; }}
|
||||
QLabel[status="present"] {{ background-color: #2ecc71; color: white; border-radius: {int(4*scale)}px; padding: {int(3*scale)}px; }}
|
||||
QLabel[status="absent"] {{ background-color: #e74c3c; color: white; border-radius: {int(4*scale)}px; padding: {int(3*scale)}px; }}
|
||||
QLabel[alarm="active"] {{ background-color: #e74c3c; color: white; font-weight: bold; border-radius: {int(4*scale)}px; padding: {int(2*scale)}px; }}
|
||||
QLabel[alarm="inactive"] {{ background-color: transparent; color: #f0f0f0; }}
|
||||
QGroupBox#ChamberWidget {{ border: 2px solid #3498db; }}
|
||||
"""
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
# --- REPLACE the entire content of ui/widgets.py with this ---
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QGroupBox, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFrame, QPushButton, QFormLayout
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal # <-- IMPORT pyqtSignal
|
||||
from PyQt6.QtGui import QFont
|
||||
|
||||
class ChamberWidget(QGroupBox):
|
||||
# --- ADD SIGNALS HERE ---
|
||||
open_door_requested = pyqtSignal()
|
||||
chg_on_requested = pyqtSignal()
|
||||
chg_off_requested = pyqtSignal()
|
||||
|
||||
def __init__(self, title="CHAMBER - X", scale=1.0):
|
||||
super().__init__(title)
|
||||
self.setObjectName("ChamberWidget")
|
||||
self.setFont(QFont("Arial", max(8, int(9 * scale)), QFont.Weight.Bold))
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setSpacing(max(2, int(4 * scale)))
|
||||
|
||||
# This section creates the 'id_field' that was missing
|
||||
id_layout = QHBoxLayout()
|
||||
id_layout.addWidget(QLabel("BAT ID: "))
|
||||
self.id_field = self._create_data_field(scale) # This line ensures self.id_field exists
|
||||
id_layout.addWidget(self.id_field)
|
||||
main_layout.addLayout(id_layout)
|
||||
|
||||
columns_layout = QHBoxLayout()
|
||||
battery_form_layout = QFormLayout()
|
||||
battery_form_layout.setVerticalSpacing(max(2, int(4 * scale)))
|
||||
self.battery_status_label = self._create_status_label("ABSENT", scale)
|
||||
self.battery_status_label.setProperty("status", "absent")
|
||||
self.soc_field = self._create_data_field(scale)
|
||||
self.voltage_field = self._create_data_field(scale)
|
||||
self.temp_field = self._create_data_field(scale)
|
||||
self.battery_fault_field = self._create_data_field(scale)
|
||||
|
||||
battery_form_layout.addRow("Status:", self.battery_status_label)
|
||||
battery_form_layout.addRow("SOC:", self.soc_field)
|
||||
battery_form_layout.addRow("Voltage:", self.voltage_field)
|
||||
battery_form_layout.addRow("Temp:", self.temp_field)
|
||||
battery_form_layout.addRow("Fault:", self.battery_fault_field)
|
||||
|
||||
separator_line = QFrame()
|
||||
separator_line.setFrameShape(QFrame.Shape.VLine)
|
||||
separator_line.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
|
||||
charger_form_layout = QFormLayout()
|
||||
charger_form_layout.setVerticalSpacing(max(2, int(4 * scale)))
|
||||
self.charger_status_label = self._create_status_label("OFF", scale)
|
||||
self.charger_status_label.setProperty("status", "absent")
|
||||
self.slot_temp_field = self._create_data_field(scale)
|
||||
self.chg_temp_field = self._create_data_field(scale)
|
||||
self.door_status_field = self._create_data_field(scale)
|
||||
self.charger_fault_field = self._create_data_field(scale)
|
||||
|
||||
charger_form_layout.addRow("Chg Status:", self.charger_status_label)
|
||||
charger_form_layout.addRow("Chg Temp:", self.chg_temp_field)
|
||||
charger_form_layout.addRow("Slot Temp:", self.slot_temp_field)
|
||||
charger_form_layout.addRow("Door Status:", self.door_status_field)
|
||||
charger_form_layout.addRow("Fault:", self.charger_fault_field)
|
||||
|
||||
columns_layout.addLayout(battery_form_layout)
|
||||
columns_layout.addWidget(separator_line)
|
||||
columns_layout.addLayout(charger_form_layout)
|
||||
main_layout.addLayout(columns_layout)
|
||||
|
||||
main_layout.addStretch()
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.open_door_btn = QPushButton("OPEN DOOR")
|
||||
self.chg_on_btn = QPushButton("CHG ON")
|
||||
self.chg_off_btn = QPushButton("CHG OFF")
|
||||
|
||||
self.open_door_btn.setObjectName("ChamberOpenDoorButton")
|
||||
self.chg_on_btn.setObjectName("ChamberChgOnButton")
|
||||
self.chg_off_btn.setObjectName("ChamberChgOffButton")
|
||||
|
||||
self.open_door_btn.clicked.connect(self.open_door_requested.emit)
|
||||
self.chg_on_btn.clicked.connect(self.chg_on_requested.emit)
|
||||
self.chg_off_btn.clicked.connect(self.chg_off_requested.emit)
|
||||
|
||||
button_layout.addWidget(self.open_door_btn)
|
||||
button_layout.addWidget(self.chg_on_btn)
|
||||
button_layout.addWidget(self.chg_off_btn)
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
# ... (the rest of the class is unchanged) ...
|
||||
def _create_status_label(self, text, scale):
|
||||
label = QLabel(text)
|
||||
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
label.setFont(QFont("Arial", max(7, int(8 * scale)), QFont.Weight.Bold))
|
||||
return label
|
||||
|
||||
def _create_data_field(self, scale):
|
||||
field = QLineEdit("N/A")
|
||||
field.setReadOnly(True)
|
||||
field.setFont(QFont("Arial", max(7, int(8 * scale))))
|
||||
return field
|
||||
|
||||
def update_data(self, data):
|
||||
if data.get("batteryPresent") == 1:
|
||||
self.battery_status_label.setText("PRESENT")
|
||||
self.battery_status_label.setProperty("status", "present")
|
||||
else:
|
||||
self.battery_status_label.setText("ABSENT")
|
||||
self.battery_status_label.setProperty("status", "absent")
|
||||
|
||||
if data.get("chargerPresent") == 1:
|
||||
self.charger_status_label.setText("CHARGER ON")
|
||||
self.charger_status_label.setProperty("status", "present")
|
||||
else:
|
||||
self.charger_status_label.setText("CHARGER OFF")
|
||||
self.charger_status_label.setProperty("status", "absent")
|
||||
|
||||
for widget in [self.battery_status_label, self.charger_status_label]:
|
||||
widget.style().unpolish(widget)
|
||||
widget.style().polish(widget)
|
||||
|
||||
self.id_field.setText(data.get("batteryIdentification", "N/A"))
|
||||
self.soc_field.setText(f'{data.get("soc", 0)}%')
|
||||
self.voltage_field.setText(f'{data.get("voltage", 0) / 1000.0:.2f} V')
|
||||
self.temp_field.setText(f'{data.get("batteryMaxTemp", 0) / 10.0:.1f} °C')
|
||||
self.battery_fault_field.setText(str(data.get("batteryFaultCode", 0)))
|
||||
self.slot_temp_field.setText(f'{data.get("slotTemperature", 0) / 10.0:.1f} °C')
|
||||
self.chg_temp_field.setText(f'{data.get("chargerTemp", 0) / 10.0:.1f} °C')
|
||||
self.charger_fault_field.setText(str(data.get("chargerFaultCode", 0)))
|
||||
door_status = "CLOSED" if data.get("doorStatus") == 1 else "OPEN"
|
||||
self.door_status_field.setText(door_status)
|
||||
|
||||
def reset_to_default(self):
|
||||
"""Resets all fields in this chamber widget to their default state."""
|
||||
self.battery_status_label.setText("ABSENT")
|
||||
self.battery_status_label.setProperty("status", "absent")
|
||||
self.charger_status_label.setText("CHARGER OFF")
|
||||
self.charger_status_label.setProperty("status", "absent")
|
||||
|
||||
# Re-apply the stylesheet for the status labels
|
||||
for widget in [self.battery_status_label, self.charger_status_label]:
|
||||
widget.style().unpolish(widget)
|
||||
widget.style().polish(widget)
|
||||
|
||||
self.id_field.setText("N/A")
|
||||
self.soc_field.setText("N/A")
|
||||
self.voltage_field.setText("N/A")
|
||||
self.temp_field.setText("N/A")
|
||||
self.battery_fault_field.setText("N/A")
|
||||
self.slot_temp_field.setText("N/A")
|
||||
self.chg_temp_field.setText("N/A")
|
||||
self.door_status_field.setText("N/A")
|
||||
self.charger_fault_field.setText("N/A")
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
def resource_path(relative_path):
|
||||
""" Get absolute path to resource, works for dev and for PyInstaller """
|
||||
try:
|
||||
# PyInstaller creates a temp folder and stores path in _MEIPASS
|
||||
base_path = sys._MEIPASS
|
||||
except Exception:
|
||||
# If not running as a bundled exe, use the normal script path
|
||||
base_path = os.path.abspath(".")
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||