feat: Add CPU status retrieval and display in dashboard

- Implemented `get_cpu_status` method in PLCClient to fetch current CPU state, cycle time, and additional CPU info.
- Created a new API endpoint `/api/plc/status` to serve CPU status data.
- Updated frontend to load and display CPU status in the Dashboard, including state and cycle time.
- Added error handling for CPU status retrieval in the frontend.
This commit is contained in:
Miguel 2025-08-28 13:31:11 +02:00
parent dba0ca2528
commit 4eed5d2687
6 changed files with 1236 additions and 1267 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1128,3 +1128,64 @@ class PLCClient:
self.logger.warning(f"Error getting batch reader stats: {e}")
return base_stats
def get_cpu_status(self) -> Dict[str, Any]:
"""Get current CPU status including state and cycle time"""
if not self.is_connected():
return {"connected": False, "error": "PLC not connected"}
try:
# Get CPU state (RUN, STOP, etc.)
cpu_state = self.plc.get_cpu_state()
# Get execution time (cycle time)
exec_time = self.plc.get_exec_time()
# Get additional CPU info
cpu_info = self.plc.get_cpu_info()
# Map CPU state codes to readable names
state_map = {0x08: "RUN", 0x04: "STOP", 0x00: "UNKNOWN"}
# Safely handle cpu_state formatting
try:
# Try to get a readable state name
if isinstance(cpu_state, (int, bytes)):
if isinstance(cpu_state, bytes) and len(cpu_state) > 0:
cpu_state = cpu_state[0]
state_name = state_map.get(cpu_state, f"STATE_{cpu_state}")
else:
state_name = f"STATE_{str(cpu_state)}"
except Exception as e:
state_name = "UNKNOWN"
if self.logger:
self.logger.warning(f"Error parsing CPU state {cpu_state}: {e}")
return {
"connected": True,
"state": state_name,
"state_code": cpu_state,
"cycle_time_ms": exec_time,
"cpu_info": {
"module_type_name": cpu_info.ModuleTypeName.decode(
"ascii", errors="ignore"
).strip(),
"serial_number": cpu_info.SerialNumber.decode(
"ascii", errors="ignore"
).strip(),
"as_name": cpu_info.ASName.decode("ascii", errors="ignore").strip(),
"module_name": cpu_info.ModuleName.decode(
"ascii", errors="ignore"
).strip(),
"copyright": cpu_info.Copyright.decode(
"ascii", errors="ignore"
).strip(),
},
"timestamp": time.time(),
}
except Exception as e:
error_msg = f"Error reading CPU status: {str(e)}"
if self.logger:
self.logger.error(error_msg)
return {"connected": True, "error": error_msg, "timestamp": time.time()}

View File

@ -701,12 +701,44 @@ function StatusBar({ status, isConnected, isLeader, connectionError }) {
const [plotJugglerFound, setPlotJugglerFound] = useState(false)
const [performanceData, setPerformanceData] = useState(null)
const [performanceLoading, setPerformanceLoading] = useState(false)
const [cpuStatus, setCpuStatus] = useState(null)
const toast = useToast()
const setLoading = (action, loading) => {
setActionLoading(prev => ({ ...prev, [action]: loading }))
}
// Load CPU status data
const loadCpuStatus = useCallback(async () => {
if (!plcConnected) {
setCpuStatus(null)
return
}
try {
const response = await api.getPlcStatus()
if (response.success && response.status) {
setCpuStatus(response.status)
}
} catch (error) {
// Silently fail - CPU status is optional
console.warn('Failed to load CPU status:', error)
setCpuStatus(null)
}
}, [plcConnected])
// Load CPU status when PLC is connected
useEffect(() => {
if (plcConnected && !connectionError) {
loadCpuStatus()
// Set up interval to refresh CPU status every 5 seconds
const interval = setInterval(loadCpuStatus, 5000)
return () => clearInterval(interval)
} else {
setCpuStatus(null)
}
}, [plcConnected, connectionError, loadCpuStatus])
// Load performance data
const loadPerformanceData = useCallback(async () => {
if (!plcConnected || !csvRecording) {
@ -925,6 +957,28 @@ function StatusBar({ status, isConnected, isLeader, connectionError }) {
</CardBody>
</Card>
{/* CPU Status Card - Shows when PLC is connected */}
{plcConnected && cpuStatus && (
<Card>
<CardBody>
<Stat>
<StatLabel>🖥 CPU Status</StatLabel>
<StatNumber fontSize="lg" color={cpuStatus.state === 'RUN' ? 'green.500' : 'orange.500'}>
{cpuStatus.state || 'UNKNOWN'}
</StatNumber>
{cpuStatus.cycle_time_ms !== undefined && (
<StatHelpText>
Cycle: {cpuStatus.cycle_time_ms}ms
{cpuStatus.cpu_info?.module_type_name && (
<><br/>📟 {cpuStatus.cpu_info.module_type_name}</>
)}
</StatHelpText>
)}
</Stat>
</CardBody>
</Card>
)}
<Card>
<CardBody>
<Stat>

View File

@ -363,4 +363,12 @@ export async function getHistoricalPerformance(windows = 6) {
return toJsonOrThrow(res)
}
// PLC Status API
export async function getPlcStatus() {
const res = await fetch(`${BASE_URL}/api/plc/status`, {
headers: { 'Accept': 'application/json' }
})
return toJsonOrThrow(res)
}

14
main.py
View File

@ -2902,6 +2902,20 @@ def disable_plc_reconnection():
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/api/plc/status")
def get_plc_status():
"""Get current PLC CPU status including state and cycle time"""
error_response = check_streamer_initialized()
if error_response:
return error_response
try:
status = streamer.plc_client.get_cpu_status()
return jsonify({"success": True, "status": status})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/api/events")
def get_events():
"""Get recent events from the application log"""

View File

@ -7,6 +7,6 @@
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-28T11:46:10.725708",
"last_update": "2025-08-28T13:30:03.511773",
"plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}