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 files
main
Kirubakaran 2025-08-21 00:26:09 +05:30
commit 412139f02d
23 changed files with 2809 additions and 0 deletions

27
.gitignore vendored Normal file
View File

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

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

0
core/__init__.py Normal file
View File

159
core/csv_logger.py Normal file
View File

@ -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}")

131
core/mqtt_client.py Normal file
View File

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

BIN
logo/black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
logo/v_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
logo/vec_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

24
logo/vec_logo_svg.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

BIN
logo/white.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

16
main.py Normal file
View File

@ -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
proto/__init__.py Normal file
View File

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

BIN
requirements.txt Normal file

Binary file not shown.

0
ui/__init__.py Normal file
View File

1521
ui/main_window.py Normal file

File diff suppressed because it is too large Load Diff

218
ui/styles.py Normal file
View File

@ -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; }}
"""

153
ui/widgets.py Normal file
View File

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

13
utils.py Normal file
View File

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