diff --git a/backend/api/device.py b/backend/api/device.py index 4b840231..d949dbbc 100644 --- a/backend/api/device.py +++ b/backend/api/device.py @@ -312,6 +312,7 @@ def patch(self, device_id : str): device = device_mgr.get_device(device_id) schema = DeviceSchema() device.update_spec(schema.load(request.json)['specification']) + device.compute_output_power() from submodule.rs_project import RsProjectManager RsProjectManager.get_instance().set_modified(True) return schema.dump(device), 200 diff --git a/src/GlobalStateProvider.js b/src/GlobalStateProvider.js index 6746dc57..71634f95 100644 --- a/src/GlobalStateProvider.js +++ b/src/GlobalStateProvider.js @@ -38,6 +38,16 @@ export function GlobalStateProvider({ children, fetch }) { // TODO temp fix for const [bcpuNames, setBcpuNames] = useState([]); const [connectivityNames, setConnectivityNames] = useState([]); const [dmaNames, setDmaNames] = useState([]); + const [thermalData, setThermalData] = useState({ + ambientTypical: 25, + ambientWorseCase: 50, + thetaJa: 10, + }); + const [powerData, setPowerData] = useState({ + powerBudget: 1.0, + fpgaScaling: 25, + pcScaling: 25, + }); let peripheralsMessages = {}; @@ -128,6 +138,25 @@ export function GlobalStateProvider({ children, fetch }) { // TODO temp fix for updatePeripherals(device, item.href, item.type); }); }); + + server.GET(server.deviceInfo(device), (result) => { + if (result && result.specification) { + const { specification } = result; + setThermalData({ + ambientTypical: specification.thermal?.ambient?.typical || 25, + ambientWorseCase: specification.thermal?.ambient?.worsecase || 50, + thetaJa: specification.thermal?.theta_ja || 10, + }); + setPowerData({ + powerBudget: specification.power?.budget || 1.0, + fpgaScaling: + (specification.power?.typical_dynamic_scaling?.fpga_complex || 0) * 100, + pcScaling: + (specification.power?.typical_dynamic_scaling?.processing_complex || 0) + * 100, + }); + } + }); } else { setClockingState([]); setFleState([]); @@ -136,9 +165,49 @@ export function GlobalStateProvider({ children, fetch }) { // TODO temp fix for setIoState([]); setSocState({}); setPeripherals([]); + setThermalData({ + ambientTypical: 25, + ambientWorseCase: 50, + thetaJa: 10, + }); + setPowerData({ + powerBudget: 1.0, + fpgaScaling: 25, + pcScaling: 25, + }); } } + function updateThermalAndPowerData(device, newThermalData, newPowerData) { + const updatedData = { + specification: { + thermal: { + ambient: { + typical: newThermalData.ambientTypical, + worsecase: newThermalData.ambientWorseCase, + }, + theta_ja: newThermalData.thetaJa, + }, + power: { + budget: newPowerData.powerBudget, + typical_dynamic_scaling: { + fpga_complex: newPowerData.fpgaScaling / 100, + processing_complex: newPowerData.pcScaling / 100, + }, + }, + }, + }; + + server.PATCH(server.deviceInfo(device), updatedData, (response) => { + if (response.ok) { + setThermalData(newThermalData); + setPowerData(newPowerData); + } else { + console.error('Error updating thermal and power data:', response.statusText); + } + }); + } + function GetOptions(id) { const found = attributes.find((elem) => id === elem.id); return (found === undefined) ? [] : found.options; @@ -150,6 +219,7 @@ export function GlobalStateProvider({ children, fetch }) { // TODO temp fix for const values = useMemo(() => ({ updateGlobalState, + updateThermalAndPowerData, clockingState, fleState, bramState, @@ -163,8 +233,10 @@ export function GlobalStateProvider({ children, fetch }) { // TODO temp fix for connectivityNames, dmaNames, fetchAttributes, + thermalData, + powerData, // eslint-disable-next-line react-hooks/exhaustive-deps - }), [bramState, clockingState, dspState, fleState, ioState, socState]); + }), [bramState, clockingState, dspState, fleState, ioState, socState, thermalData, powerData]); return ( diff --git a/src/components/Tables/PowerSummaryTable.js b/src/components/Tables/PowerSummaryTable.js index a25d9cbb..304de319 100644 --- a/src/components/Tables/PowerSummaryTable.js +++ b/src/components/Tables/PowerSummaryTable.js @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { IoMdCloseCircleOutline } from 'react-icons/io'; import { Tooltip } from 'antd'; import { PowerCell } from './TableCells'; import { fixed, color } from '../../utils/common'; import { State } from '../ComponentsLib'; - +import { GET, deviceInfo } from '../../utils/serverAPI'; import '../style/PowerSummaryTable.css'; function PowerSummaryTableToolTip({ title, statusColor }) { @@ -17,91 +17,308 @@ function PowerSummaryTableToolTip({ title, statusColor }) { ); } + function PowerSummaryTable({ - title, data, total, percent, + title, data = [], total = 0, percent = 0, deviceId = 'MPW1', #TODO }) { - function getErrors(messages) { - if (messages === undefined) return []; - const errors = messages.filter((item) => item.filter((inner) => inner.type === 'error').length > 0); - return errors; - } - function getWarning(messages) { - if (messages === undefined) return []; - const warnings = messages.filter((item) => item.filter((inner) => inner.type === 'warn').length > 0); - return warnings; - } - function buildMessage(messages) { - return messages.reduce((sum, item, currentIndex) => { - item.forEach((i, index) => sum.push( - // eslint-disable-next-line react/no-array-index-key - - {i.text} -
-
, - )); - return sum; - }, []); - } - function message(messages) { - const errors = getErrors(messages); - if (errors.length > 0) { - return buildMessage(errors); + const [thermalData, setThermalData] = useState({ + ambientTypical: 25, + ambientWorseCase: 50, + thetaJa: 10, + }); + + const [powerData, setPowerData] = useState({ + powerBudget: 1.0, + fpgaScaling: 25, + pcScaling: 25, + }); + + const ambientTypicalRef = useRef(null); + const ambientWorseCaseRef = useRef(null); + const thetaJaRef = useRef(null); + const powerBudgetRef = useRef(null); + const fpgaScalingRef = useRef(null); + const pcScalingRef = useRef(null); + + useEffect(() => { + const fetchData = async () => { + try { + // Update the API call to match the correct endpoint + GET(deviceInfo(deviceId), (result) => { + if (result && result.specification) { + const { specification } = result; + + // Process thermal data + setThermalData({ + ambientTypical: specification.thermal?.ambient?.typical || 25, + ambientWorseCase: specification.thermal?.ambient?.worsecase || 50, + thetaJa: specification.thermal?.theta_ja || 10, + }); + + // Process power data + setPowerData({ + powerBudget: specification.power?.budget || 1.0, + fpgaScaling: (specification.power?.typical_dynamic_scaling?.fpga_complex || 0) * 100, + pcScaling: + (specification.power?.typical_dynamic_scaling?.processing_complex || 0) * 100, + }); + } + }); + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + fetchData(); // Trigger the fetch function + }, [deviceId]); // Re-run effect when deviceId changes + + const updateBackend = async (deviceIdParam, thermalDataParam, powerDataParam) => { + const updatedData = { + specification: { + thermal: { + ambient: { + typical: thermalDataParam.ambientTypical || 0, + worsecase: thermalDataParam.ambientWorseCase || 0, // Matches schema + }, + theta_ja: thermalDataParam.thetaJa || 0, + }, + power: { + budget: powerDataParam.powerBudget || 0, + typical_dynamic_scaling: { + fpga_complex: powerDataParam.fpgaScaling || 0, + processing_complex: powerDataParam.pcScaling || 0, + }, + }, + }, + }; + + console.log('PATCH Payload:', JSON.stringify(updatedData, null, 2)); + + try { + const response = await fetch(`http://127.0.0.1:5000/devices/${deviceIdParam}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updatedData), + }); + + if (!response.ok) { + throw new Error(`PATCH request failed: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + console.log('PATCH Response:', result); + } catch (error) { + console.error('Error during PATCH request:', error); } - const warnings = getWarning(messages); - if (warnings.length > 0) { - return buildMessage(warnings); + }; + + const handleFieldUpdate = (field, value) => { + const updatedThermalData = { ...thermalData }; + const updatedPowerData = { ...powerData }; + + if (field in thermalData) { + updatedThermalData[field] = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value); + } else { + updatedPowerData[field] = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value); } - return ''; - } - - function isError(messages) { return getErrors(messages).length > 0; } - function isWarning(messages) { return getWarning(messages).length > 0; } - function statusColor(messages) { - return color(isError(messages), isWarning(messages)); - } + + setThermalData(updatedThermalData); + setPowerData(updatedPowerData); + + updateBackend(deviceId, updatedThermalData, updatedPowerData); + }; + + const enforceNumericInput = (e) => { + const { value } = e.target; + const valid = /^-?\d*\.?\d*%?$/.test(value); + if (!valid) { + e.target.value = value.slice(0, -1); + } + }; + + const handleKeyDown = (e, nextFieldRef) => { + if (e.key === 'Enter' && nextFieldRef && nextFieldRef.current) { + nextFieldRef.current.focus(); + } + }; + + const getErrors = (messages) => messages?.filter((item) => item.some((inner) => inner.type === 'error')) || []; + const getWarnings = (messages) => messages?.filter((item) => item.some((inner) => inner.type === 'warn')) || []; + + const buildMessage = (messages) => messages.reduce((acc, item) => { // Removed `currentIndex` + item.forEach((i) => acc.push( + + {' '} + {/* Replace with the unique property */} + {i.text} +
+
, + )); + return acc; + }, []); + + const message = (messages) => { + const errors = getErrors(messages); + return errors.length > 0 ? buildMessage(errors) : buildMessage(getWarnings(messages)); + }; + + const statusColor = (messages) => color( + getErrors(messages).length > 0, + getWarnings(messages).length > 0, + ); + return (
-
{title}
+ {title === 'FPGA Complex and Core Power' && ( +
+
Thermal Specification
+ + + + + + + + + + + + + + + + + +
+ TypicalWorse-Case
Ambient + handleFieldUpdate('ambientTypical', e.target.value)} + onInput={enforceNumericInput} + ref={ambientTypicalRef} + onKeyDown={(e) => handleKeyDown(e, ambientWorseCaseRef)} + /> + {' '} + °C + + handleFieldUpdate('ambientWorseCase', e.target.value)} + onInput={enforceNumericInput} + ref={ambientWorseCaseRef} + onKeyDown={(e) => handleKeyDown(e, thetaJaRef)} + /> + {' '} + °C +
+ ΘJA: + handleFieldUpdate('thetaJa', e.target.value)} + onInput={enforceNumericInput} + ref={thetaJaRef} + onKeyDown={(e) => handleKeyDown(e, powerBudgetRef)} + /> + {' '} + °C/W +
+ +
Power Specification
+ + + + + + + + + + + + +
Power Budget + handleFieldUpdate('powerBudget', e.target.value)} + onInput={enforceNumericInput} + ref={powerBudgetRef} + onKeyDown={(e) => handleKeyDown(e, fpgaScalingRef)} + /> + {' '} + W +
Typical Dynamic Scaling % + FPGA: + handleFieldUpdate('fpgaScaling', e.target.value)} + onInput={enforceNumericInput} + ref={fpgaScalingRef} + onKeyDown={(e) => handleKeyDown(e, pcScalingRef)} + /> + {' '} + % + + PC: + handleFieldUpdate('pcScaling', e.target.value)} + onInput={enforceNumericInput} + ref={pcScalingRef} + /> + {' '} + % +
+
+ )} + +
{title || 'FPGA Complex and Core Power'}
- { - data.map((item, index) => ( - // eslint-disable-next-line react/no-array-index-key - - - - - - - - )) - } + {data.map((item) => ( + + {' '} + {/* Use a unique property like `id` */} + + + + + + + ))}
{item.text} - {`${fixed(item.percent, 0)} %`} - - { - (isError(item.messages) || isWarning(item.messages)) && ( - - ) - } -
+ + {item.text || 'N/A'} + {`${fixed(item.percent || 0, 0)} %`} + + {(getErrors(item.messages).length > 0 + || getWarnings(item.messages).length > 0) && ( + + )} +
+
- - + +
Total - {` ${fixed(total)} W`} + {` ${fixed(total || 0)} W`}
@@ -110,11 +327,21 @@ function PowerSummaryTable({ PowerSummaryTable.propTypes = { title: PropTypes.string.isRequired, - data: PropTypes.oneOfType([ - PropTypes.array, - ]).isRequired, + data: PropTypes.arrayOf( + PropTypes.shape({ + text: PropTypes.string, // Example property + power: PropTypes.number, // Example property + messages: PropTypes.arrayOf( + PropTypes.shape({ + type: PropTypes.string.isRequired, + content: PropTypes.string, + }), + ), // Nested array of objects + }), + ).isRequired, total: PropTypes.number.isRequired, percent: PropTypes.number.isRequired, + deviceId: PropTypes.string.isRequired, }; export default PowerSummaryTable; diff --git a/src/components/style/PowerSummaryTable.css b/src/components/style/PowerSummaryTable.css index c98c0c5e..88a4881c 100644 --- a/src/components/style/PowerSummaryTable.css +++ b/src/components/style/PowerSummaryTable.css @@ -1,36 +1,180 @@ +/* Main container for the entire table layout */ .pst-container { - padding: 8px; + padding: 4px; display: flex; flex-direction: column; - row-gap: 5px; + row-gap: 2px; height: 100%; + width: 100%; + box-sizing: border-box; +} + +/* Styling for the Thermal and Power Specification section */ +.thermal-power-specification { + padding: 4px; + background-color: #f0f7f5; + border: 1px solid #d1d1d1; + border-radius: 6px; + margin-bottom: 4px; + display: flex; + flex-direction: column; + row-gap: 4px; + width: 100%; + margin-left: auto; + margin-right: auto; + box-sizing: border-box; +} + +/* Header styling for the Thermal and Power Specification */ +.spec-header { + font-weight: bold; + font-size: 11px; + text-align: center; + color: #333; + background-color: #e1e1e1; + padding: 4px; + border: 1px solid #c0c0c0; + border-radius: 4px; +} + +/* Table styling for the specification sections */ +.spec-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 4px; +} + +.spec-table th, .spec-table td { + padding: 3px; + text-align: center; + border: 1px solid #d1d1d1; + font-size: 10px; +} + +/* Styling for headers in the Typical and Worst-Case columns */ +.spec-table .typical-header, .spec-table .worst-header { + font-weight: bold; + background-color: #f0f0f0; + border-bottom: 1px solid #c0c0c0; +} + +/* Special handling for ΘJA row that spans across two columns */ +.spec-table .theta-row td { + text-align: center; + font-weight: bold; + color: #444; + padding: 3px; + border-right: 1px solid #d1d1d1; + border-left: 1px solid #d1d1d1; +} + +/* Input field styling */ +.spec-table td input { + width: 45px; + padding: 2px; + font-size: 10px; + text-align: center; +} + +/* Bottom section for Power Specification */ +.power-spec-section { + padding: 4px; + background-color: #f0f7f5; + border: 1px solid #d1d1d1; + border-radius: 6px; + margin-bottom: 4px; + width: 100%; + margin-left: auto; + margin-right: auto; +} + +/* Table for Power Specification */ +.power-spec-table { + width: 100%; + border-collapse: collapse; +} + +.power-spec-table th, .power-spec-table td { + padding: 3px; + text-align: center; + border: 1px solid #d1d1d1; + font-size: 10px; +} + +/* Styling for power budget and dynamic scaling */ +.power-spec-table .scaling-cell { + font-weight: bold; + color: #444; } +/* Input fields in Power Specification */ +.power-spec-table td input { + width: 45px; + padding: 2px; + text-align: center; + font-size: 10px; +} + +/* Header styling for both Thermal and Power sections */ +.bold-text-title { + font-weight: bold; + font-size: 11px; + color: #333; + text-align: left; +} + +/* FPGA Complex and Core Power table */ .pst-table { width: 100%; border-collapse: collapse; + box-sizing: border-box; + height: auto; /* Changed height to auto to fit content */ + display: table; + table-layout: fixed; /* Ensure equal column widths */ + margin-bottom: 0; /* Removed extra margin for symmetrical alignment */ } .pst-table td { - padding-top: 0.5em; - padding-bottom: 0.5em; + padding: 0.20em 0.99em; /* Adjusted padding for consistent spacing */ + border-bottom: 1px solid #e1e1e1; + text-align: left; /* Align content to the left */ + font-size: 10px; + width: 100%; } -.pst-table td { +/* Processing Complex (SOC) Power table */ +.pst-table-soc { + width: 100%; + border-collapse: collapse; + box-sizing: border-box; + height: auto; /* Changed height to auto to fit content */ + display: table; + table-layout: fixed; + margin-bottom: 0; /* Ensures symmetrical gap with the FPGA table */ +} + +.pst-table-soc td { + padding: 0.80em 0.99em; /* Same padding adjustments for consistency */ border-bottom: 1px solid #e1e1e1; + text-align: left; + font-size: 20px; + width: 100%; } -.pst-table tbody>tr:last-child>td { +.pst-table tbody>tr:last-child>td, +.pst-table-soc tbody>tr:last-child>td { border-bottom: 0; } +/* Bottom section */ .pst-bottom { display: flex; flex-direction: row; + justify-content: space-between; border-top: 1px solid #e1e1e1; - margin-top: 10px; - padding-top: 3px; - padding-bottom: 3px; + margin-top: 0; + padding-top: 2px; + padding-bottom: 2px; align-items: flex-end; } @@ -40,23 +184,40 @@ .pst-bottom-progress { text-align: left; + width: 50%; } .pst-bottom-total { text-align: right; - width: 100%; + width: 50%; color: #7c7c7c; - font-size: 17px; + font-size: 10px; } +/* Styling for dot icons */ .dot-td { text-align: center; } .dot { - width: 10px; - height: 10px; + width: 8px; + height: 8px; border-radius: 50%; display: inline-block; vertical-align: middle; } + +/* Remove unnecessary bottom space */ +.pst-container, +.pst-table, +.pst-table-soc { + margin-bottom: 0; + height: 100%; +} + +/* Ensure table fills the entire container */ +.pst-table, .pst-table-soc { + display: flex; + flex-direction: column; + justify-content: space-between; +} diff --git a/src/tests/GlobalStateProvider.test.js b/src/tests/GlobalStateProvider.test.js index 8a4f5f43..be412656 100644 --- a/src/tests/GlobalStateProvider.test.js +++ b/src/tests/GlobalStateProvider.test.js @@ -6,6 +6,7 @@ import * as server from '../utils/serverAPI'; jest.mock('../utils/serverAPI', () => ({ GET: jest.fn(), + PATCH: jest.fn(), api: { fetch: jest.fn((elem, device) => `/mock-api/${elem}/${device}`), }, @@ -19,17 +20,25 @@ jest.mock('../utils/serverAPI', () => ({ }, peripheralPath: jest.fn((device, path) => `/mock-api/peripheral/${device}/${path}`), attributes: jest.fn(() => '/mock-api/attributes'), + deviceInfo: jest.fn((device) => `/mock-api/device-info/${device}`), })); const TestComponent = () => { - const { updateGlobalState, clockingState, peripherals, acpuNames, fetchAttributes, GetOptions } = useGlobalState(); + const { + updateGlobalState, + clockingState, + peripherals, + acpuNames, + fetchAttributes, + GetOptions, + } = useGlobalState(); const options = GetOptions('mock-attribute'); return (
{clockingState.join(', ')}
{peripherals.length}
-
{acpuNames.map(acpu => acpu.text).join(', ')}
+
{acpuNames.map((acpu) => acpu.text).join(', ')}
{options.join(', ')}
@@ -60,6 +69,8 @@ describe('GlobalStateProvider', () => { callback([{ href: '/mock-peripheral', type: 'DMA' }]); } else if (url.includes('mock-peripheral')) { callback({ consumption: { messages: 'DMA Message' }, targets: 8 }); + } else if (url.includes('device-info')) { + callback({ specification: { thermal: {}, power: {} } }); } });