update page

This commit is contained in:
2026-02-01 16:38:08 +01:00
parent 1f00856167
commit 39655c2913
11 changed files with 5192 additions and 992 deletions

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,7 @@ import ALMTypePage from "./pages/ALMTypePage";
import TraceabilityMatrixPage from "./pages/TraceabilityMatrixPage"; import TraceabilityMatrixPage from "./pages/TraceabilityMatrixPage";
import ESPIDFHelperPage from "./pages/ESPIDFHelperPage"; import ESPIDFHelperPage from "./pages/ESPIDFHelperPage";
import WorkPackageGraphPage from "./pages/WorkPackageGraphPage"; import WorkPackageGraphPage from "./pages/WorkPackageGraphPage";
import SelectedSensorsPage from "./pages/SelectedSensorsPage";
import LoginPage from "./pages/LoginPage"; import LoginPage from "./pages/LoginPage";
import NotFound from "./pages/NotFound"; import NotFound from "./pages/NotFound";
@@ -74,6 +75,14 @@ const App = () => (
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/sensors"
element={
<ProtectedRoute>
<SelectedSensorsPage />
</ProtectedRoute>
}
/>
<Route <Route
path="/alm/:type" path="/alm/:type"
element={ element={

View File

@@ -12,19 +12,15 @@ import {
CheckCircle, CheckCircle,
XCircle, XCircle,
AlertCircle, AlertCircle,
Cloud,
Server, Server,
RefreshCw, RefreshCw,
Loader2 Loader2
} from 'lucide-react'; } from 'lucide-react';
import { WorkPackage } from '@/types/traceability'; import { WorkPackage } from '@/types/traceability';
import { parseCSVContent, ParseResult } from '@/lib/csvParser'; import { parseCSVContent, ParseResult } from '@/lib/csvParser';
import {
fetchFromOpenProject, const STORAGE_KEY = 'traceability_data';
fetchFromBackendProxy, const SERVER_URL_KEY = 'traceability_server_url';
OpenProjectConfig,
DEFAULT_CONFIG
} from '@/services/openProjectService';
interface DataUpdateDialogProps { interface DataUpdateDialogProps {
onDataLoaded: (workPackages: WorkPackage[]) => void; onDataLoaded: (workPackages: WorkPackage[]) => void;
@@ -40,11 +36,10 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
const [fileName, setFileName] = useState<string>(''); const [fileName, setFileName] = useState<string>('');
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
// Backend proxy config // Server endpoint config - persisted
const [proxyUrl, setProxyUrl] = useState(''); const [serverUrl, setServerUrl] = useState(() =>
localStorage.getItem(SERVER_URL_KEY) || '/api/traceability'
// Direct OpenProject config );
const [opConfig, setOpConfig] = useState<OpenProjectConfig>(DEFAULT_CONFIG);
// Drag and drop state // Drag and drop state
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@@ -53,6 +48,15 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
setLogs(prev => [...prev, log]); setLogs(prev => [...prev, log]);
}; };
const persistData = (workPackages: WorkPackage[]) => {
const data = {
lastUpdated: new Date().toISOString(),
workPackages
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
addLog(`💾 Data persisted to storage (${workPackages.length} items)`);
};
const handleFile = async (file: File) => { const handleFile = async (file: File) => {
setFileName(file.name); setFileName(file.name);
setIsLoading(true); setIsLoading(true);
@@ -60,10 +64,13 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
setErrors([]); setErrors([]);
try { try {
addLog(`📂 Reading file: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`);
const text = await file.text(); const text = await file.text();
addLog(`📝 File loaded, ${text.length} characters`);
const result = parseCSVContent(text); const result = parseCSVContent(text);
setParseResult(result); setParseResult(result);
setLogs(result.logs); setLogs(prev => [...prev, ...result.logs]);
setErrors(result.errors); setErrors(result.errors);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -84,66 +91,87 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
if (file) handleFile(file); if (file) handleFile(file);
}; };
const handleFetchFromProxy = async () => { const handleFetchFromServer = async () => {
if (!proxyUrl) { if (!serverUrl) {
setErrors(['Please enter a backend proxy URL']); setErrors(['Please enter a server endpoint URL']);
return; return;
} }
// Save URL for next time
localStorage.setItem(SERVER_URL_KEY, serverUrl);
setIsLoading(true); setIsLoading(true);
setLogs([]); setLogs([]);
setErrors([]); setErrors([]);
setParseResult(null); setParseResult(null);
try { try {
const result = await fetchFromBackendProxy(proxyUrl, addLog); addLog(`🔍 Connecting to server: ${serverUrl}`);
setLogs(result.logs);
setErrors(result.errors);
if (result.success) { const response = await fetch(serverUrl, {
const typeCounts = result.workPackages.reduce((acc, wp) => { method: 'GET',
headers: {
'Accept': 'application/json, text/csv'
}
});
if (!response.ok) {
throw new Error(`Server Error: ${response.status} ${response.statusText}`);
}
const contentType = response.headers.get('content-type') || '';
addLog(`📄 Response content-type: ${contentType}`);
if (contentType.includes('text/csv')) {
addLog(`📋 Parsing CSV response...`);
const csvText = await response.text();
const result = parseCSVContent(csvText);
setLogs(prev => [...prev, ...result.logs]);
setErrors(result.errors);
setParseResult(result);
} else if (contentType.includes('application/json')) {
addLog(`📋 Parsing JSON response...`);
const data = await response.json();
let workPackages: WorkPackage[];
if (Array.isArray(data)) {
workPackages = data;
} else if (data.workPackages) {
workPackages = data.workPackages;
} else {
throw new Error('Invalid JSON format: expected array or {workPackages: [...]}');
}
addLog(`✅ Received ${workPackages.length} work packages`);
const typeCounts = workPackages.reduce((acc, wp) => {
acc[wp.type] = (acc[wp.type] || 0) + 1; acc[wp.type] = (acc[wp.type] || 0) + 1;
return acc; return acc;
}, {} as Record<string, number>); }, {} as Record<string, number>);
setParseResult({ setParseResult({
success: true, success: true,
workPackages: result.workPackages, workPackages,
logs: result.logs, logs: [],
errors: result.errors, errors: [],
typeCounts typeCounts
}); });
} else {
// Try to parse as CSV anyway
addLog(`⚠️ Unknown content-type, attempting CSV parse...`);
const text = await response.text();
const result = parseCSVContent(text);
setLogs(prev => [...prev, ...result.logs]);
setErrors(result.errors);
setParseResult(result);
} }
} finally {
setIsLoading(false);
}
};
const handleFetchFromOpenProject = async () => { } catch (error) {
setIsLoading(true); const errorMsg = error instanceof Error ? error.message : 'Unknown error';
setLogs([]); setErrors([errorMsg]);
setErrors([]); addLog(`❌ Error: ${errorMsg}`);
setParseResult(null);
try {
const result = await fetchFromOpenProject(opConfig, undefined, addLog);
setLogs(result.logs);
setErrors(result.errors);
if (result.success) {
const typeCounts = result.workPackages.reduce((acc, wp) => {
acc[wp.type] = (acc[wp.type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
setParseResult({
success: true,
workPackages: result.workPackages,
logs: result.logs,
errors: result.errors,
typeCounts
});
}
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -151,6 +179,8 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
const handleApply = () => { const handleApply = () => {
if (parseResult?.success && parseResult.workPackages.length > 0) { if (parseResult?.success && parseResult.workPackages.length > 0) {
// Persist to localStorage for other users/sessions
persistData(parseResult.workPackages);
onDataLoaded(parseResult.workPackages); onDataLoaded(parseResult.workPackages);
onClose?.(); onClose?.();
} }
@@ -174,23 +204,19 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
Update Traceability Data Update Traceability Data
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Choose how to update your data from OpenProject Upload a CSV file or fetch from your server
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="upload" className="flex items-center gap-2"> <TabsTrigger value="upload" className="flex items-center gap-2">
<Upload className="h-4 w-4" /> <Upload className="h-4 w-4" />
Upload CSV Upload CSV
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="proxy" className="flex items-center gap-2"> <TabsTrigger value="server" className="flex items-center gap-2">
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
Backend API Server Fetch
</TabsTrigger>
<TabsTrigger value="direct" className="flex items-center gap-2">
<Cloud className="h-4 w-4" />
Direct Fetch
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -222,103 +248,46 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
</p> </p>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Run <code className="bg-muted px-1 rounded">python get_traceability.py</code> locally to generate the CSV Run <code className="bg-muted px-1 rounded">python get_traceability.py</code> to generate the CSV file
</p> </p>
</TabsContent> </TabsContent>
{/* Tab 2: Backend Proxy */} {/* Tab 2: Server Fetch */}
<TabsContent value="proxy" className="space-y-4"> <TabsContent value="server" className="space-y-4">
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<Label htmlFor="proxyUrl">Backend API Endpoint</Label> <Label htmlFor="serverUrl">Server Endpoint</Label>
<Input <Input
id="proxyUrl" id="serverUrl"
placeholder="https://your-server.com/api/traceability" placeholder="/api/traceability or https://your-server.com/api/data"
value={proxyUrl} value={serverUrl}
onChange={(e) => setProxyUrl(e.target.value)} onChange={(e) => setServerUrl(e.target.value)}
/> />
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
Your backend should return JSON or CSV with work package data Endpoint that runs your Python script and returns CSV or JSON
</p> </p>
</div> </div>
<Button <Button
onClick={handleFetchFromProxy} onClick={handleFetchFromServer}
disabled={isLoading || !proxyUrl} disabled={isLoading || !serverUrl}
className="w-full" className="w-full"
> >
{isLoading ? ( {isLoading ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Fetching...</> <><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Fetching...</>
) : ( ) : (
<><Server className="h-4 w-4 mr-2" /> Fetch from Backend</> <><Server className="h-4 w-4 mr-2" /> Fetch from Server</>
)} )}
</Button> </Button>
</div> </div>
<div className="bg-muted/50 rounded-lg p-3 text-xs space-y-2"> <div className="bg-muted/50 rounded-lg p-3 text-xs space-y-2">
<p className="font-medium">Backend Setup Guide:</p> <p className="font-medium">Server Setup:</p>
<ol className="list-decimal ml-4 space-y-1 text-muted-foreground"> <ol className="list-decimal ml-4 space-y-1 text-muted-foreground">
<li>Deploy the Python script on your server</li> <li>Create an endpoint that runs <code>get_traceability.py</code></li>
<li>Create an API endpoint that runs the script</li> <li>Return the CSV file or JSON with work packages</li>
<li>Return JSON or serve the generated CSV</li> <li>Example: <code>GET /api/traceability</code> returns CSV</li>
</ol> </ol>
</div> </div>
</TabsContent> </TabsContent>
{/* Tab 3: Direct OpenProject Fetch */}
<TabsContent value="direct" className="space-y-4">
<div className="space-y-3">
<div>
<Label htmlFor="baseUrl">OpenProject URL</Label>
<Input
id="baseUrl"
placeholder="https://openproject.example.com"
value={opConfig.baseUrl}
onChange={(e) => setOpConfig(prev => ({ ...prev, baseUrl: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="projectId">Project Identifier</Label>
<Input
id="projectId"
placeholder="project-id"
value={opConfig.projectIdentifier}
onChange={(e) => setOpConfig(prev => ({ ...prev, projectIdentifier: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="apiKey">API Key</Label>
<Input
id="apiKey"
type="password"
placeholder="Your OpenProject API Key"
value={opConfig.apiKey}
onChange={(e) => setOpConfig(prev => ({ ...prev, apiKey: e.target.value }))}
/>
</div>
<Button
onClick={handleFetchFromOpenProject}
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Fetching...</>
) : (
<><Cloud className="h-4 w-4 mr-2" /> Fetch from OpenProject</>
)}
</Button>
</div>
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-3 text-xs">
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-amber-600 mt-0.5" />
<div>
<p className="font-medium text-amber-700">CORS Notice</p>
<p className="text-amber-600 mt-1">
Direct browser fetch may fail due to CORS. Configure your OpenProject server to allow
cross-origin requests, or use the Backend API option.
</p>
</div>
</div>
</div>
</TabsContent>
</Tabs> </Tabs>
{/* Results Section */} {/* Results Section */}
@@ -387,7 +356,7 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
{parseResult?.success && ( {parseResult?.success && (
<Button onClick={handleApply}> <Button onClick={handleApply}>
<CheckCircle className="h-4 w-4 mr-2" /> <CheckCircle className="h-4 w-4 mr-2" />
Apply Data Apply & Save
</Button> </Button>
)} )}
<Button variant="outline" onClick={handleReset}> <Button variant="outline" onClick={handleReset}>

View File

@@ -18,6 +18,7 @@ import {
ChevronRight, ChevronRight,
Cpu, Cpu,
Share2, Share2,
Thermometer,
} from "lucide-react"; } from "lucide-react";
import { NavLink } from "@/components/NavLink"; import { NavLink } from "@/components/NavLink";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
@@ -40,6 +41,7 @@ const mainItems = [
{ title: "Work Package Graph", url: "/graph", icon: Share2 }, { title: "Work Package Graph", url: "/graph", icon: Share2 },
{ title: "Documentation", url: "/documentation", icon: BookOpen }, { title: "Documentation", url: "/documentation", icon: BookOpen },
{ title: "Gap Analysis", url: "/analysis", icon: Search }, { title: "Gap Analysis", url: "/analysis", icon: Search },
{ title: "Selected Sensors", url: "/sensors", icon: Thermometer },
{ title: "ESP-IDF Helper", url: "/esp-idf", icon: Cpu }, { title: "ESP-IDF Helper", url: "/esp-idf", icon: Cpu },
]; ];

View File

@@ -3,8 +3,9 @@ import { DocumentFile } from '@/types/documentation';
const STORAGE_KEY = 'documentation_files'; const STORAGE_KEY = 'documentation_files';
// Default documentation structure // Default documentation structure - updated with all feature groups
const defaultDocuments: DocumentFile[] = [ const defaultDocuments: DocumentFile[] = [
// System Overview
{ {
id: 'about-asf', id: 'about-asf',
title: 'About ASF (Agricultural Sensor Framework)', title: 'About ASF (Agricultural Sensor Framework)',
@@ -17,16 +18,26 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'system-assumptions', id: 'system-assumptions',
title: 'System Assumptions & Limitations', title: 'System Assumptions & Limitations',
description: 'Design constraints and environmental assumptions.', description: 'Design constraints, environmental assumptions, and system limitations.',
category: 'System Overview', category: 'System Overview',
content: '# System Assumptions\n\nUpload the System_Assumptions_Limitations.md file to see full documentation.', content: '# System Assumptions\n\nUpload the System_Assumptions_Limitations.md file to see full documentation.',
fileName: 'System_Assumptions_Limitations.md', fileName: 'System_Assumptions_Limitations.md',
lastUpdated: new Date().toISOString() lastUpdated: new Date().toISOString()
}, },
{
id: 'cross-feature-constraints',
title: 'Cross-Feature Constraints',
description: 'Mandatory architectural rules applying across multiple features.',
category: 'System Overview',
content: '# Cross-Feature Constraints\n\nUpload the Cross-Feature_Constraints.md file to see full documentation.',
fileName: 'Cross-Feature_Constraints.md',
lastUpdated: new Date().toISOString()
},
// Feature Groups
{ {
id: 'fg-daq', id: 'fg-daq',
title: 'FG-DAQ: Data Acquisition', title: 'FG-DAQ: Data Acquisition',
description: 'Sensor reading, sampling, and data collection features.', description: 'Multi-sensor data acquisition, high-frequency sampling, and timestamped data generation.',
category: 'Feature Groups', category: 'Feature Groups',
content: '# Data Acquisition Features\n\nUpload the DAQ_Sensor_Data_Acquisition_Features.md file to see full documentation.', content: '# Data Acquisition Features\n\nUpload the DAQ_Sensor_Data_Acquisition_Features.md file to see full documentation.',
fileName: 'DAQ_Sensor_Data_Acquisition_Features.md', fileName: 'DAQ_Sensor_Data_Acquisition_Features.md',
@@ -35,7 +46,7 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'fg-dqc', id: 'fg-dqc',
title: 'FG-DQC: Quality & Calibration', title: 'FG-DQC: Quality & Calibration',
description: 'Data validation, calibration management, and quality assurance.', description: 'Sensor identification, slot compatibility, fault detection, and calibration management.',
category: 'Feature Groups', category: 'Feature Groups',
content: '# Quality & Calibration Features\n\nUpload the DQC_Data_Quality_Calibration_Features.md file to see full documentation.', content: '# Quality & Calibration Features\n\nUpload the DQC_Data_Quality_Calibration_Features.md file to see full documentation.',
fileName: 'DQC_Data_Quality_Calibration_Features.md', fileName: 'DQC_Data_Quality_Calibration_Features.md',
@@ -44,7 +55,7 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'fg-com', id: 'fg-com',
title: 'FG-COM: Communication', title: 'FG-COM: Communication',
description: 'Network connectivity and data transmission features.', description: 'MQTT over TLS 1.2, ESP-NOW peer communication, CBOR payloads, and LoRa fallback.',
category: 'Feature Groups', category: 'Feature Groups',
content: '# Communication Features\n\nUpload the COM_Communication_Features.md file to see full documentation.', content: '# Communication Features\n\nUpload the COM_Communication_Features.md file to see full documentation.',
fileName: 'COM_Communication_Features.md', fileName: 'COM_Communication_Features.md',
@@ -53,7 +64,7 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'fg-diag', id: 'fg-diag',
title: 'FG-DIAG: Diagnostics', title: 'FG-DIAG: Diagnostics',
description: 'System health monitoring and fault detection.', description: 'Fault detection, classification, diagnostic code registry, and health monitoring.',
category: 'Feature Groups', category: 'Feature Groups',
content: '# Diagnostics Features\n\nUpload the DIAG_Diagnostics_Health_Monitoring_Features.md file to see full documentation.', content: '# Diagnostics Features\n\nUpload the DIAG_Diagnostics_Health_Monitoring_Features.md file to see full documentation.',
fileName: 'DIAG_Diagnostics_Health_Monitoring_Features.md', fileName: 'DIAG_Diagnostics_Health_Monitoring_Features.md',
@@ -62,7 +73,7 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'fg-data', id: 'fg-data',
title: 'FG-DATA: Persistence', title: 'FG-DATA: Persistence',
description: 'Data storage and retention management.', description: 'Data Pool (DP) component, SD card storage, NVS management, and data retention.',
category: 'Feature Groups', category: 'Feature Groups',
content: '# Data Persistence Features\n\nUpload the DATA_Persistence_Data_Management_Features.md file to see full documentation.', content: '# Data Persistence Features\n\nUpload the DATA_Persistence_Data_Management_Features.md file to see full documentation.',
fileName: 'DATA_Persistence_Data_Management_Features.md', fileName: 'DATA_Persistence_Data_Management_Features.md',
@@ -71,7 +82,7 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'fg-ota', id: 'fg-ota',
title: 'FG-OTA: Over-The-Air Updates', title: 'FG-OTA: Over-The-Air Updates',
description: 'Firmware and configuration update mechanisms.', description: 'A/B partitioning, SHA-256 verification, rollback mechanism, and update negotiation.',
category: 'Feature Groups', category: 'Feature Groups',
content: '# OTA Update Features\n\nUpload the OTA_Firmware_Update_OTA_Features.md file to see full documentation.', content: '# OTA Update Features\n\nUpload the OTA_Firmware_Update_OTA_Features.md file to see full documentation.',
fileName: 'OTA_Firmware_Update_OTA_Features.md', fileName: 'OTA_Firmware_Update_OTA_Features.md',
@@ -79,8 +90,8 @@ const defaultDocuments: DocumentFile[] = [
}, },
{ {
id: 'fg-sec', id: 'fg-sec',
title: 'FG-SEC: Security', title: 'FG-SEC: Security & Safety',
description: 'Authentication, encryption, and access control.', description: 'Secure Boot V2, flash encryption AES-256, mTLS with X.509, and anti-rollback.',
category: 'Feature Groups', category: 'Feature Groups',
content: '# Security Features\n\nUpload the SEC_Security_Safety_Features.md file to see full documentation.', content: '# Security Features\n\nUpload the SEC_Security_Safety_Features.md file to see full documentation.',
fileName: 'SEC_Security_Safety_Features.md', fileName: 'SEC_Security_Safety_Features.md',
@@ -89,21 +100,50 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'fg-sys', id: 'fg-sys',
title: 'FG-SYS: System Management', title: 'FG-SYS: System Management',
description: 'State machine, teardown, and local HMI.', description: 'FSM with 11 states, controlled teardown, OLED HMI, and debug session support.',
category: 'Feature Groups', category: 'Feature Groups',
content: '# System Management Features\n\nUpload the SYS_System_Management_Features.md file to see full documentation.', content: '# System Management Features\n\nUpload the SYS_System_Management_Features.md file to see full documentation.',
fileName: 'SYS_System_Management_Features.md', fileName: 'SYS_System_Management_Features.md',
lastUpdated: new Date().toISOString() lastUpdated: new Date().toISOString()
}, },
{
id: 'fg-hw',
title: 'FG-HW: Hardware Abstraction',
description: 'Sensor Abstraction Layer (SAL), interface abstraction, and storage abstraction.',
category: 'Feature Groups',
content: '# Hardware Abstraction Features\n\nUpload the HW_Hardware_Abstraction_Features.md file to see full documentation.',
fileName: 'HW_Hardware_Abstraction_Features.md',
lastUpdated: new Date().toISOString()
},
{
id: 'fg-pwr',
title: 'FG-PWR: Power & Fault Handling',
description: 'Brownout detection, power-loss protection, supercapacitor support, and recovery.',
category: 'Feature Groups',
content: '# Power & Fault Handling Features\n\nUpload the PWR_Power_Fault_Handling_Features.md file to see full documentation.',
fileName: 'PWR_Power_Fault_Handling_Features.md',
lastUpdated: new Date().toISOString()
},
// State Machine & Failure Handling
{ {
id: 'state-machine', id: 'state-machine',
title: 'System State Machine Specification', title: 'System State Machine Specification',
description: 'Formal definition of all operational states and transitions.', description: 'Complete FSM with 11 states: INIT, RUNNING, WARNING, FAULT, TEARDOWN, OTA_PREP, etc.',
category: 'State Machine', category: 'State Machine',
content: '# State Machine Specification\n\nUpload the System_State_Machine_Specification.md file to see full documentation.', content: '# State Machine Specification\n\nUpload the System_State_Machine_Specification.md file to see full documentation.',
fileName: 'System_State_Machine_Specification.md', fileName: 'System_State_Machine_Specification.md',
lastUpdated: new Date().toISOString() lastUpdated: new Date().toISOString()
}, },
{
id: 'failure-handling',
title: 'Failure Handling Model',
description: 'Fault taxonomy, severity levels, escalation rules, and recovery behaviors.',
category: 'State Machine',
content: '# Failure Handling Model\n\nUpload the Failure_Handling_Model.md file to see full documentation.',
fileName: 'Failure_Handling_Model.md',
lastUpdated: new Date().toISOString()
},
// Requirements
{ {
id: 'srs', id: 'srs',
title: 'Software Requirements Specification (SRS)', title: 'Software Requirements Specification (SRS)',
@@ -113,6 +153,16 @@ const defaultDocuments: DocumentFile[] = [
fileName: 'SRS.md', fileName: 'SRS.md',
lastUpdated: new Date().toISOString() lastUpdated: new Date().toISOString()
}, },
{
id: 'system-review',
title: 'System Review Checklist',
description: 'Pre-implementation verification checklist for requirements and architecture.',
category: 'Requirements',
content: '# System Review Checklist\n\nUpload the System_Review_Checklist.md file to see full documentation.',
fileName: 'System_Review_Checklist.md',
lastUpdated: new Date().toISOString()
},
// Traceability & Verification
{ {
id: 'traceability', id: 'traceability',
title: 'Annex A: Traceability Matrix', title: 'Annex A: Traceability Matrix',
@@ -125,16 +175,17 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'vv-matrix', id: 'vv-matrix',
title: 'V&V Matrix', title: 'V&V Matrix',
description: 'Verification and Validation method assignments.', description: 'Verification methods (UT, IT, HIL, REV, ANAL) and acceptance criteria.',
category: 'Traceability & Verification', category: 'Traceability & Verification',
content: '# V&V Matrix\n\nUpload the VV_Matrix.md file to see full documentation.', content: '# V&V Matrix\n\nUpload the VV_Matrix.md file to see full documentation.',
fileName: 'VV_Matrix.md', fileName: 'VV_Matrix.md',
lastUpdated: new Date().toISOString() lastUpdated: new Date().toISOString()
}, },
// Interfaces & Budgets
{ {
id: 'interfaces', id: 'interfaces',
title: 'Annex B: External Interfaces', title: 'Annex B: External Interfaces',
description: 'Hardware and software interface specifications.', description: 'Main Hub communication, sensor interfaces, storage, and HMI specifications.',
category: 'Interfaces & Budgets', category: 'Interfaces & Budgets',
content: '# External Interfaces\n\nUpload the Annex_B_Interfaces.md file to see full documentation.', content: '# External Interfaces\n\nUpload the Annex_B_Interfaces.md file to see full documentation.',
fileName: 'Annex_B_Interfaces.md', fileName: 'Annex_B_Interfaces.md',
@@ -143,7 +194,7 @@ const defaultDocuments: DocumentFile[] = [
{ {
id: 'budgets', id: 'budgets',
title: 'Annex C: Resource Budgets', title: 'Annex C: Resource Budgets',
description: 'RAM, Flash, CPU, and timing allocations.', description: 'RAM ≤225KB, Flash ≤5.05MB, CPU ≤42%, timing budgets, and partition layout.',
category: 'Interfaces & Budgets', category: 'Interfaces & Budgets',
content: '# Resource Budgets\n\nUpload the Annex_C_Budgets.md file to see full documentation.', content: '# Resource Budgets\n\nUpload the Annex_C_Budgets.md file to see full documentation.',
fileName: 'Annex_C_Budgets.md', fileName: 'Annex_C_Budgets.md',

View File

@@ -1,6 +1,8 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { WorkPackage, TraceabilityData, WorkPackageType } from '@/types/traceability'; import { WorkPackage, TraceabilityData, WorkPackageType } from '@/types/traceability';
const STORAGE_KEY = 'traceability_data';
export function useTraceabilityData() { export function useTraceabilityData() {
const [data, setData] = useState<TraceabilityData | null>(null); const [data, setData] = useState<TraceabilityData | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -16,11 +18,6 @@ export function useTraceabilityData() {
const cleanText = csvText.replace(/^\uFEFF/, ''); const cleanText = csvText.replace(/^\uFEFF/, '');
const workPackages: WorkPackage[] = []; const workPackages: WorkPackage[] = [];
// CSV format: ID,Type,Status,Title,Description,Parent_ID,Relations
// Description can be multi-line and contain HTML
let pos = 0;
const lines = cleanText.split('\n'); const lines = cleanText.split('\n');
logs.push(`[CSV Parser] Total lines: ${lines.length}`); logs.push(`[CSV Parser] Total lines: ${lines.length}`);
@@ -31,7 +28,6 @@ export function useTraceabilityData() {
let currentRow: string[] = []; let currentRow: string[] = [];
let inQuotedField = false; let inQuotedField = false;
let currentField = ''; let currentField = '';
let lineNum = 1;
// Process character by character for proper CSV parsing // Process character by character for proper CSV parsing
const content = lines.slice(1).join('\n'); const content = lines.slice(1).join('\n');
@@ -43,11 +39,9 @@ export function useTraceabilityData() {
if (inQuotedField) { if (inQuotedField) {
if (char === '"') { if (char === '"') {
if (nextChar === '"') { if (nextChar === '"') {
// Escaped quote
currentField += '"'; currentField += '"';
i++; i++;
} else { } else {
// End of quoted field
inQuotedField = false; inQuotedField = false;
} }
} else { } else {
@@ -55,7 +49,6 @@ export function useTraceabilityData() {
} }
} else { } else {
if (char === '"' && currentField === '') { if (char === '"' && currentField === '') {
// Start of quoted field
inQuotedField = true; inQuotedField = true;
} else if (char === ',') { } else if (char === ',') {
currentRow.push(currentField); currentRow.push(currentField);
@@ -64,11 +57,9 @@ export function useTraceabilityData() {
currentRow.push(currentField); currentRow.push(currentField);
currentField = ''; currentField = '';
// Process the completed row if it has enough fields and starts with an ID
if (currentRow.length >= 4 && /^\d+$/.test(currentRow[0]?.trim())) { if (currentRow.length >= 4 && /^\d+$/.test(currentRow[0]?.trim())) {
const [id, type, status, title, description = '', parentId = '', relations = ''] = currentRow; const [id, type, status, title, description = '', parentId = '', relations = ''] = currentRow;
const wpType = type?.toLowerCase().replace(/\s+/g, ' ').trim() as WorkPackageType;
const wpType = type?.toLowerCase().replace(/\s+/g, '') as WorkPackageType;
workPackages.push({ workPackages.push({
id: parseInt(id, 10), id: parseInt(id, 10),
@@ -86,7 +77,6 @@ export function useTraceabilityData() {
} }
currentRow = []; currentRow = [];
lineNum++;
} else { } else {
currentField += char; currentField += char;
} }
@@ -98,7 +88,7 @@ export function useTraceabilityData() {
currentRow.push(currentField); currentRow.push(currentField);
if (currentRow.length >= 4 && /^\d+$/.test(currentRow[0]?.trim())) { if (currentRow.length >= 4 && /^\d+$/.test(currentRow[0]?.trim())) {
const [id, type, status, title, description = '', parentId = '', relations = ''] = currentRow; const [id, type, status, title, description = '', parentId = '', relations = ''] = currentRow;
const wpType = type?.toLowerCase().replace(/\s+/g, '') as WorkPackageType; const wpType = type?.toLowerCase().replace(/\s+/g, ' ').trim() as WorkPackageType;
workPackages.push({ workPackages.push({
id: parseInt(id, 10), id: parseInt(id, 10),
@@ -124,6 +114,7 @@ export function useTraceabilityData() {
return { workPackages, logs }; return { workPackages, logs };
}, []); }, []);
// Try to load from localStorage first, then fall back to CSV file
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -131,7 +122,31 @@ export function useTraceabilityData() {
newLogs.push(`[Loader] Starting data load at ${new Date().toLocaleTimeString()}`); newLogs.push(`[Loader] Starting data load at ${new Date().toLocaleTimeString()}`);
try { try {
// Add cache-busting to force fresh data // Check localStorage first for persisted data
const storedData = localStorage.getItem(STORAGE_KEY);
if (storedData) {
try {
const parsed = JSON.parse(storedData);
if (parsed.workPackages && parsed.workPackages.length > 0) {
newLogs.push(`[Loader] Found persisted data: ${parsed.workPackages.length} work packages`);
newLogs.push(`[Loader] Last updated: ${parsed.lastUpdated}`);
const lastUpdatedDate = new Date(parsed.lastUpdated);
setData({
lastUpdated: lastUpdatedDate,
workPackages: parsed.workPackages
});
setLastUpdated(lastUpdatedDate);
setParseLog(newLogs);
setLoading(false);
return;
}
} catch (e) {
newLogs.push(`[Loader] Could not parse stored data, loading from CSV`);
}
}
// Fall back to CSV file
const cacheBuster = `?t=${Date.now()}`; const cacheBuster = `?t=${Date.now()}`;
newLogs.push(`[Loader] Fetching /data/traceability_export.csv${cacheBuster}`); newLogs.push(`[Loader] Fetching /data/traceability_export.csv${cacheBuster}`);
@@ -145,18 +160,71 @@ export function useTraceabilityData() {
const { workPackages, logs } = parseCSV(csvText); const { workPackages, logs } = parseCSV(csvText);
// Combine loader logs with parser logs
const allLogs = [...newLogs, ...logs]; const allLogs = [...newLogs, ...logs];
allLogs.push(`[Loader] Data load complete at ${new Date().toLocaleTimeString()}`); allLogs.push(`[Loader] Data load complete at ${new Date().toLocaleTimeString()}`);
setParseLog(allLogs); setParseLog(allLogs);
const now = new Date(); const now = new Date();
setData({ const dataObj = {
lastUpdated: now, lastUpdated: now,
workPackages workPackages
}); };
setData(dataObj);
setLastUpdated(now); setLastUpdated(now);
// Persist to localStorage for next load
localStorage.setItem(STORAGE_KEY, JSON.stringify({
lastUpdated: now.toISOString(),
workPackages
}));
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
newLogs.push(`[Loader] ERROR: ${errorMsg}`);
setParseLog(newLogs);
setError(errorMsg);
} finally {
setLoading(false);
}
}, [parseCSV]);
// Force reload from CSV file (ignoring localStorage)
const reloadFromCSV = useCallback(async () => {
setLoading(true);
setError(null);
const newLogs: string[] = [];
newLogs.push(`[Loader] Force reloading from CSV file...`);
try {
const cacheBuster = `?t=${Date.now()}`;
newLogs.push(`[Loader] Fetching /data/traceability_export.csv${cacheBuster}`);
const response = await fetch(`/data/traceability_export.csv${cacheBuster}`);
if (!response.ok) {
throw new Error(`Failed to load: ${response.status} ${response.statusText}`);
}
const csvText = await response.text();
newLogs.push(`[Loader] Received ${csvText.length} bytes`);
const { workPackages, logs } = parseCSV(csvText);
const allLogs = [...newLogs, ...logs];
allLogs.push(`[Loader] CSV reload complete`);
setParseLog(allLogs);
const now = new Date();
setData({ lastUpdated: now, workPackages });
setLastUpdated(now);
// Update localStorage with new data
localStorage.setItem(STORAGE_KEY, JSON.stringify({
lastUpdated: now.toISOString(),
workPackages
}));
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Unknown error'; const errorMsg = err instanceof Error ? err.message : 'Unknown error';
newLogs.push(`[Loader] ERROR: ${errorMsg}`); newLogs.push(`[Loader] ERROR: ${errorMsg}`);
@@ -171,6 +239,20 @@ export function useTraceabilityData() {
loadData(); loadData();
}, [loadData]); }, [loadData]);
// Update data and persist
const updateData = useCallback((newData: TraceabilityData) => {
setData(newData);
setLastUpdated(newData.lastUpdated);
// Persist to localStorage
localStorage.setItem(STORAGE_KEY, JSON.stringify({
lastUpdated: newData.lastUpdated.toISOString(),
workPackages: newData.workPackages
}));
setParseLog(prev => [...prev, `[Update] Data updated with ${newData.workPackages.length} work packages`]);
}, []);
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [loadData]); }, [loadData]);
@@ -195,9 +277,10 @@ export function useTraceabilityData() {
error, error,
lastUpdated, lastUpdated,
refresh, refresh,
reloadFromCSV,
groupedByType, groupedByType,
typeCounts, typeCounts,
parseLog, parseLog,
setData // Expose setData for manual data updates setData: updateData
}; };
} }

View File

@@ -0,0 +1,323 @@
/**
* Traceability utilities for building flexible cross-type relationships
*/
import { WorkPackage, ParsedRelation, WorkPackageType } from '@/types/traceability';
export interface TraceabilityNode {
workPackage: WorkPackage;
linkedItems: LinkedItem[];
}
export interface LinkedItem {
workPackage: WorkPackage;
relationType: string;
direction: 'incoming' | 'outgoing';
}
export interface TraceabilityGraph {
nodes: Map<number, TraceabilityNode>;
edges: TraceabilityEdge[];
}
export interface TraceabilityEdge {
sourceId: number;
targetId: number;
relationType: string;
}
// Type display configuration
export const TYPE_CONFIG: Record<string, { color: string; bgColor: string; borderColor: string; icon: string }> = {
'feature': { color: 'text-purple-600', bgColor: 'bg-purple-500/10', borderColor: 'border-purple-500/20', icon: 'Target' },
'sw_feature': { color: 'text-purple-500', bgColor: 'bg-purple-500/10', borderColor: 'border-purple-500/20', icon: 'Target' },
'epic': { color: 'text-indigo-600', bgColor: 'bg-indigo-500/10', borderColor: 'border-indigo-500/20', icon: 'Layers' },
'user story': { color: 'text-violet-600', bgColor: 'bg-violet-500/10', borderColor: 'border-violet-500/20', icon: 'User' },
'requirements': { color: 'text-blue-600', bgColor: 'bg-blue-500/10', borderColor: 'border-blue-500/20', icon: 'CheckSquare' },
'swreq': { color: 'text-green-600', bgColor: 'bg-green-500/10', borderColor: 'border-green-500/20', icon: 'FileText' },
'software test case': { color: 'text-amber-600', bgColor: 'bg-amber-500/10', borderColor: 'border-amber-500/20', icon: 'TestTube' },
'system test': { color: 'text-orange-600', bgColor: 'bg-orange-500/10', borderColor: 'border-orange-500/20', icon: 'TestTube' },
'task': { color: 'text-gray-600', bgColor: 'bg-gray-500/10', borderColor: 'border-gray-500/20', icon: 'CheckCircle' },
'bug': { color: 'text-red-600', bgColor: 'bg-red-500/10', borderColor: 'border-red-500/20', icon: 'Bug' },
'component': { color: 'text-cyan-600', bgColor: 'bg-cyan-500/10', borderColor: 'border-cyan-500/20', icon: 'Box' },
'interface': { color: 'text-teal-600', bgColor: 'bg-teal-500/10', borderColor: 'border-teal-500/20', icon: 'Link' },
'risk': { color: 'text-rose-600', bgColor: 'bg-rose-500/10', borderColor: 'border-rose-500/20', icon: 'AlertTriangle' },
'milestone': { color: 'text-yellow-600', bgColor: 'bg-yellow-500/10', borderColor: 'border-yellow-500/20', icon: 'Flag' },
'phase': { color: 'text-slate-600', bgColor: 'bg-slate-500/10', borderColor: 'border-slate-500/20', icon: 'Calendar' },
'summary task': { color: 'text-stone-600', bgColor: 'bg-stone-500/10', borderColor: 'border-stone-500/20', icon: 'List' },
};
export function getTypeConfig(type: string) {
return TYPE_CONFIG[type.toLowerCase()] || {
color: 'text-gray-600',
bgColor: 'bg-gray-500/10',
borderColor: 'border-gray-500/20',
icon: 'File'
};
}
export function parseRelations(relationsStr: string): ParsedRelation[] {
if (!relationsStr) return [];
const relations: ParsedRelation[] = [];
const matches = relationsStr.matchAll(/(\w+)\(#(\d+)\)/g);
for (const match of matches) {
relations.push({
type: match[1],
targetId: parseInt(match[2], 10),
});
}
return relations;
}
/**
* Build a complete traceability graph from work packages
* This creates bidirectional relationships regardless of type
*/
export function buildTraceabilityGraph(workPackages: WorkPackage[]): TraceabilityGraph {
const nodes = new Map<number, TraceabilityNode>();
const edges: TraceabilityEdge[] = [];
const wpById = new Map(workPackages.map(wp => [wp.id, wp]));
// Initialize all nodes
workPackages.forEach(wp => {
nodes.set(wp.id, {
workPackage: wp,
linkedItems: []
});
});
// Build edges from relations and parent links
workPackages.forEach(wp => {
const node = nodes.get(wp.id)!;
// Parse explicit relations
const relations = parseRelations(wp.relations);
relations.forEach(rel => {
const targetWp = wpById.get(rel.targetId);
if (targetWp) {
// Add outgoing link
node.linkedItems.push({
workPackage: targetWp,
relationType: rel.type,
direction: 'outgoing'
});
// Add edge
edges.push({
sourceId: wp.id,
targetId: rel.targetId,
relationType: rel.type
});
// Add incoming link to target
const targetNode = nodes.get(rel.targetId);
if (targetNode) {
targetNode.linkedItems.push({
workPackage: wp,
relationType: rel.type,
direction: 'incoming'
});
}
}
});
// Handle parent-child relationship
if (wp.parentId) {
const parentId = parseInt(wp.parentId, 10);
const parentWp = wpById.get(parentId);
if (parentWp) {
node.linkedItems.push({
workPackage: parentWp,
relationType: 'parent',
direction: 'outgoing'
});
edges.push({
sourceId: wp.id,
targetId: parentId,
relationType: 'parent'
});
const parentNode = nodes.get(parentId);
if (parentNode) {
parentNode.linkedItems.push({
workPackage: wp,
relationType: 'child',
direction: 'incoming'
});
}
}
}
});
return { nodes, edges };
}
/**
* Get all items linked to a specific work package (any type)
*/
export function getLinkedItems(
wpId: number,
graph: TraceabilityGraph,
filterTypes?: WorkPackageType[]
): LinkedItem[] {
const node = graph.nodes.get(wpId);
if (!node) return [];
let items = node.linkedItems;
if (filterTypes && filterTypes.length > 0) {
items = items.filter(item => filterTypes.includes(item.workPackage.type));
}
return items;
}
/**
* Find all work packages that link to a given work package
*/
export function findIncomingLinks(
wpId: number,
graph: TraceabilityGraph
): LinkedItem[] {
const node = graph.nodes.get(wpId);
if (!node) return [];
return node.linkedItems.filter(item => item.direction === 'incoming');
}
/**
* Find all work packages that a given work package links to
*/
export function findOutgoingLinks(
wpId: number,
graph: TraceabilityGraph
): LinkedItem[] {
const node = graph.nodes.get(wpId);
if (!node) return [];
return node.linkedItems.filter(item => item.direction === 'outgoing');
}
/**
* Build hierarchical chains starting from a given type
* This is flexible - can start from any type and trace to any related types
*/
export interface FlexibleChain {
root: WorkPackage;
children: FlexibleChainNode[];
totalDescendants: number;
}
export interface FlexibleChainNode {
workPackage: WorkPackage;
relationType: string;
children: FlexibleChainNode[];
}
export function buildChainsFromType(
workPackages: WorkPackage[],
rootType: WorkPackageType,
maxDepth: number = 4
): FlexibleChain[] {
const graph = buildTraceabilityGraph(workPackages);
const roots = workPackages.filter(wp => wp.type === rootType);
const visited = new Set<number>();
function buildChildNodes(wpId: number, depth: number): FlexibleChainNode[] {
if (depth >= maxDepth) return [];
const node = graph.nodes.get(wpId);
if (!node) return [];
const children: FlexibleChainNode[] = [];
// Get all linked items (both directions)
node.linkedItems.forEach(linked => {
if (!visited.has(linked.workPackage.id)) {
visited.add(linked.workPackage.id);
children.push({
workPackage: linked.workPackage,
relationType: linked.relationType,
children: buildChildNodes(linked.workPackage.id, depth + 1)
});
}
});
return children;
}
return roots.map(root => {
visited.clear();
visited.add(root.id);
const children = buildChildNodes(root.id, 0);
function countDescendants(nodes: FlexibleChainNode[]): number {
return nodes.reduce((sum, n) => sum + 1 + countDescendants(n.children), 0);
}
return {
root,
children,
totalDescendants: countDescendants(children)
};
});
}
/**
* Calculate coverage statistics between any two types
*/
export interface CoverageStats {
sourceType: string;
targetType: string;
totalSource: number;
linkedSource: number;
coverage: number;
}
export function calculateCoverage(
workPackages: WorkPackage[],
sourceType: WorkPackageType,
targetType: WorkPackageType
): CoverageStats {
const graph = buildTraceabilityGraph(workPackages);
const sourceItems = workPackages.filter(wp => wp.type === sourceType);
let linkedCount = 0;
sourceItems.forEach(source => {
const node = graph.nodes.get(source.id);
if (node) {
const hasLink = node.linkedItems.some(
item => item.workPackage.type === targetType
);
if (hasLink) linkedCount++;
}
});
return {
sourceType,
targetType,
totalSource: sourceItems.length,
linkedSource: linkedCount,
coverage: sourceItems.length > 0 ? Math.round((linkedCount / sourceItems.length) * 100) : 0
};
}
/**
* Get relationship matrix between all types
*/
export function getRelationshipMatrix(workPackages: WorkPackage[]): Map<string, Map<string, number>> {
const matrix = new Map<string, Map<string, number>>();
const graph = buildTraceabilityGraph(workPackages);
graph.edges.forEach(edge => {
const sourceWp = workPackages.find(wp => wp.id === edge.sourceId);
const targetWp = workPackages.find(wp => wp.id === edge.targetId);
if (sourceWp && targetWp) {
if (!matrix.has(sourceWp.type)) {
matrix.set(sourceWp.type, new Map());
}
const typeMap = matrix.get(sourceWp.type)!;
typeMap.set(targetWp.type, (typeMap.get(targetWp.type) || 0) + 1);
}
});
return matrix;
}

View File

@@ -30,361 +30,323 @@ const budgetAnalysis = {
cpu: { allocated: 42, budget: "80% peak", status: "ok" } cpu: { allocated: 42, budget: "80% peak", status: "ok" }
}; };
// Gap Analysis Data // Gap Analysis Data - Updated based on latest feature engineering specifications
const gapAnalysis = [ const gapAnalysis = [
{ {
category: "Communication (FG-COM)", category: "Communication (FG-COM)",
severity: "critical", severity: "medium",
gaps: [ gaps: [
{ {
id: "GAP-COM-001", id: "GAP-COM-001",
title: "WiFi Protocol Details Missing", title: "LoRa Module Specifications Missing",
description: "Documents mention 'WiFi / Zigbee / LoRa' but lack specific protocol selection and rationale.", description: "LoRa fallback is marked as 'optional external module' but no module selection criteria provided.",
questions: [ questions: [
"Which WiFi standard specifically? (802.11b/g/n all supported by ESP32-S3)", "Which LoRa module model is recommended? (SX1276, SX1262, etc.)",
"What is the target data rate requirement?", "What frequency band? (868MHz EU, 915MHz US)",
"Is 2.4GHz band acceptable (5GHz not supported)?", "SPI or UART interface preference?"
"What is the expected range/coverage?"
], ],
recommendation: "Define primary protocol as WiFi 802.11n for best throughput. Add Zigbee/LoRa as optional fallback.", recommendation: "Define approved LoRa module (recommend SX1262) with frequency band configuration.",
esp32Impact: "ESP32-S3 supports 802.11 b/g/n at 2.4GHz up to 150 Mbps. No 5GHz support." esp32Impact: "ESP32-S3 has native SPI support for LoRa modules. No hardware limitation."
}, },
{ {
id: "GAP-COM-002", id: "GAP-COM-002",
title: "TLS/DTLS Version Unspecified", title: "ESP-NOW Encryption Details",
description: "Security requires TLS/DTLS but version and cipher suites are not defined.", description: "ESP-NOW peer communication mentioned but encryption method not fully specified.",
questions: [ questions: [
"TLS 1.2 or TLS 1.3?", "Use ESP-NOW built-in encryption or application-layer?",
"Which cipher suites are acceptable?", "Key exchange mechanism for peer discovery?",
"Certificate management approach?", "Maximum peer count for mesh scenarios?"
"Key rotation policy?"
], ],
recommendation: "Use TLS 1.2 with AES-128-GCM for ESP32-S3 hardware acceleration compatibility.", recommendation: "Use ESP-NOW CCMP encryption with pre-shared LMK (Local Master Key).",
esp32Impact: "ESP32-S3 has hardware crypto accelerators for AES-128/256, SHA, RSA." esp32Impact: "ESP-NOW supports up to 20 encrypted peers. Hardware crypto accelerated."
}, },
{ {
id: "GAP-COM-003", id: "GAP-COM-003",
title: "Peer-to-Peer Protocol Undefined", title: "CBOR Schema Version Management",
description: "F-COM-03 mentions peer Sensor Hub communication but no protocol specified.", description: "CBOR versioned payloads mentioned but schema versioning strategy not defined.",
questions: [ questions: [
"What transport for peer-to-peer? (WiFi Direct, BLE, ESP-NOW?)", "How are schema versions negotiated?",
"What is the maximum peer count?", "Backward compatibility requirements?",
"Discovery mechanism?" "Schema registry location?"
], ],
recommendation: "Consider ESP-NOW for low-latency peer communication (250 byte payload, no WiFi connection needed).", recommendation: "Include schema version in CBOR header. Maintain N-1 backward compatibility.",
esp32Impact: "ESP-NOW supported natively. BLE Mesh also available." esp32Impact: "No hardware impact. Software versioning only."
},
{
id: "GAP-COM-004",
title: "Heartbeat/Keep-Alive Details Missing",
description: "30-second timeout mentioned but no heartbeat interval or mechanism defined.",
questions: [
"What is the heartbeat interval?",
"Payload content of heartbeat?",
"Exponential backoff parameters for reconnection?"
],
recommendation: "Define 10-second heartbeat interval with 3 missed = disconnected.",
esp32Impact: "No specific hardware limitation."
} }
] ]
}, },
{ {
category: "Data Acquisition (FG-DAQ)", category: "Data Acquisition (FG-DAQ)",
severity: "high", severity: "low",
gaps: [ gaps: [
{ {
id: "GAP-DAQ-001", id: "GAP-DAQ-001",
title: "Specific Sensor Models Not Defined", title: "Sensor Warm-Up State Transitions",
description: "Sensor types listed (Temp, Humidity, CO2, etc.) but no specific models or part numbers.", description: "Sensor states defined (INIT, WARMUP, STABLE, etc.) but warm-up duration per sensor type not specified.",
questions: [ questions: [
"What are the specific sensor models/part numbers?", "What are warm-up times for SHT4xI, BME680, ENS160, SEN6x?",
"What are the I2C addresses for each sensor type?", "Should WARMUP readings be discarded or flagged?",
"Are there alternative/fallback sensor models?" "Power-on sequencing between sensors?"
], ],
recommendation: "Create a Sensor Catalog with approved models, I2C addresses, and driver requirements.", recommendation: "Define per-sensor warm-up: SHT4xI (1ms), BME680 (3s gas), ENS160 (3min initial), SEN6x (30s).",
esp32Impact: "ESP32-S3 has 2x I2C buses - ensure no address conflicts if using multiple sensors." esp32Impact: "Software timing only. Consider FreeRTOS delayed start for sensors."
}, },
{ {
id: "GAP-DAQ-002", id: "GAP-DAQ-002",
title: "ADC Channel Allocation Undefined", title: "Multi-Sensor Fusion Strategy",
description: "Analog sensors mentioned but no ADC channel assignment or calibration approach.", description: "Multiple sensors measure overlapping parameters (T/RH from SHT4xI, BME680, SEN6x). Fusion not specified.",
questions: [ questions: [
"How many analog sensors are expected?", "Which sensor is primary for temperature/humidity?",
"Which ADC channels will be used?", "Cross-validation threshold for sensor disagreement?",
"What reference voltage?", "Fallback priority order?"
"ADC calibration approach?"
], ],
recommendation: "Define ADC channel map. Use eFuse-stored calibration for ESP32-S3 ADC linearity correction.", recommendation: "Primary: SHT4xI for T/RH (highest accuracy). Cross-validate against BME680/SEN6x.",
esp32Impact: "ESP32-S3 has 20 ADC channels, 12-bit resolution. Some GPIOs are ADC1 (usable with WiFi), some ADC2 (conflicts with WiFi)." esp32Impact: "CPU budget allows sensor fusion algorithms."
},
{
id: "GAP-DAQ-003",
title: "Sensor Warm-Up Times Not Specified",
description: "Many gas sensors (CO2, NH3) require warm-up periods not addressed.",
questions: [
"What are warm-up time requirements per sensor type?",
"How should readings during warm-up be handled?",
"Power-on sequencing requirements?"
],
recommendation: "Add sensor warm-up state handling. Typical: CO2 sensors 30-60 seconds, MOS sensors 24-48 hours for first use.",
esp32Impact: "No specific hardware impact, software timing only."
},
{
id: "GAP-DAQ-004",
title: "Filtering Algorithm Not Specified",
description: "High-frequency sampling mentions filtering but algorithm type not defined.",
questions: [
"Moving average, median filter, Kalman, or other?",
"Filter window size configurable?",
"Outlier rejection criteria?"
],
recommendation: "Recommend median filter (robust to outliers) with configurable window size in Machine Constants.",
esp32Impact: "CPU budget allows for moderate filtering complexity."
} }
] ]
}, },
{ {
category: "Security (FG-SEC)", category: "Security (FG-SEC)",
severity: "critical", severity: "medium",
gaps: [ gaps: [
{ {
id: "GAP-SEC-001", id: "GAP-SEC-001",
title: "Key Management Lifecycle Undefined", title: "Certificate Provisioning Workflow",
description: "Secure boot and encryption mentioned but key provisioning and rotation not specified.", description: "mTLS with X.509 specified but certificate provisioning during manufacturing not detailed.",
questions: [ questions: [
"How are keys provisioned during manufacturing?", "Factory provisioning via UART/JTAG?",
"Key rotation policy for TLS certificates?", "Certificate signing authority (CA) chain?",
"Key storage location (eFuse vs NVS encrypted)?", "Device CSR generation or pre-signed?"
"Revocation mechanism?"
], ],
recommendation: "Define key provisioning during manufacturing, use eFuse for secure boot keys, NVS encrypted partition for runtime keys.", recommendation: "Factory provision root CA + device certificate via secure JTAG. Use ESP-IDF secure provisioning.",
esp32Impact: "ESP32-S3 has eFuse for permanent key storage and hardware secure boot support." esp32Impact: "ESP32-S3 supports secure JTAG provisioning. eFuse protection available."
}, },
{ {
id: "GAP-SEC-002", id: "GAP-SEC-002",
title: "Authentication Method Not Specified", title: "eFuse Burn Sequence Documentation",
description: "Authentication required but mechanism not defined (certificates, PSK, tokens?).", description: "eFuse for secure boot and anti-rollback mentioned but burn sequence/order not documented.",
questions: [ questions: [
"Mutual TLS with certificates?", "eFuse programming order for production?",
"Pre-shared keys?", "Testing strategy before permanent eFuse burn?",
"Device identity provisioning?" "Recovery if eFuse burn fails mid-sequence?"
], ],
recommendation: "Use X.509 certificates with device-unique identity for mutual TLS.", recommendation: "Document eFuse burn sequence: 1) Flash encryption key, 2) Secure boot key, 3) JTAG disable.",
esp32Impact: "ESP32-S3 supports X.509, may need external crypto chip for many certificates." esp32Impact: "eFuse operations are irreversible. Production checklist critical."
}, },
{ {
id: "GAP-SEC-003", id: "GAP-SEC-003",
title: "Anti-Rollback Policy Vague", title: "Debug Port Security in Production",
description: "Rollback prevention mentioned as 'optional future' but critical for security.", description: "Debug session support mentioned but production debug port security not fully addressed.",
questions: [ questions: [
"Is anti-rollback mandatory?", "JTAG disabled in production builds?",
"What version tracking mechanism?", "Secure debug authentication method?",
"Emergency recovery procedure?" "Debug log filtering for sensitive data?"
], ],
recommendation: "Implement anti-rollback using ESP32-S3 eFuse version counter.", recommendation: "Disable JTAG via eFuse in production. Use authenticated debug over UART with session tokens.",
esp32Impact: "ESP32-S3 supports secure version tracking in eFuse (limited burn count)." esp32Impact: "ESP32-S3 supports eFuse-based JTAG disable and secure debug."
} }
] ]
}, },
{ {
category: "OTA Updates (FG-OTA)", category: "OTA Updates (FG-OTA)",
severity: "high", severity: "low",
gaps: [ gaps: [
{ {
id: "GAP-OTA-001", id: "GAP-OTA-001",
title: "Partition Scheme Not Defined", title: "Factory Partition Use Case",
description: "Dual-partition OTA implied but specific partition layout not documented.", description: "Factory partition marked as 'optional' in partition table. Recovery scenario unclear.",
questions: [ questions: [
"What is the partition table layout?", "When is factory partition used for recovery?",
"Factory partition included?", "How to trigger factory reset from corrupted OTA?",
"Partition sizes for app0/app1?", "Factory partition size requirements?"
"NVS partition size?"
], ],
recommendation: "Define: factory (4MB) + ota_0 (4MB) or app0 + app1 if 8MB flash. Reserve 64KB for NVS.", recommendation: "Include minimal factory partition (~1MB) for emergency recovery via GPIO boot mode.",
esp32Impact: "8MB flash allows dual 3.5MB app partitions with ample NVS/data space." esp32Impact: "8MB flash can accommodate factory + 2x OTA partitions."
}, },
{ {
id: "GAP-OTA-002", id: "GAP-OTA-002",
title: "OTA Chunk Size Not Specified", title: "OTA Progress Reporting Granularity",
description: "Firmware transferred 'in chunks' but chunk size and protocol not defined.", description: "OTA status reporting mentioned but progress indication granularity not specified.",
questions: [ questions: [
"What chunk size? (4KB, 16KB, other?)", "Progress update frequency (per chunk, percentage)?",
"Acknowledgment per chunk or batch?", "OLED display update during OTA?",
"Resume capability after interruption?" "MQTT progress topic structure?"
], ],
recommendation: "Use 4KB chunks with per-chunk CRC and resume from last successful chunk.", recommendation: "Report progress every 5% or 30 seconds, whichever comes first. Update OLED display.",
esp32Impact: "4KB aligns with flash page size for efficient writes." esp32Impact: "No hardware limitation. Balance progress updates vs. flash write performance."
},
{
id: "GAP-OTA-003",
title: "Rollback Trigger Conditions Undefined",
description: "Rollback mentioned but trigger conditions not specified.",
questions: [
"What triggers automatic rollback?",
"Timeout for marking update successful?",
"Health check criteria?"
],
recommendation: "Implement watchdog-triggered rollback if app doesn't confirm within 60 seconds of boot.",
esp32Impact: "ESP32-S3 has app rollback support in bootloader."
} }
] ]
}, },
{ {
category: "Persistence (FG-DATA)", category: "Persistence (FG-DATA)",
severity: "high", severity: "medium",
gaps: [ gaps: [
{ {
id: "GAP-DATA-001", id: "GAP-DATA-001",
title: "SD Card File System Not Specified", title: "SD Card Failure Detection Criteria",
description: "SD card storage mentioned but filesystem type not defined.", description: "SD_DEGRADED state defined but specific failure detection criteria not specified.",
questions: [ questions: [
"FAT32, exFAT, or custom filesystem?", "CRC error threshold for degraded state?",
"Maximum file size?", "Write timeout threshold?",
"Directory structure?", "Automatic SD card remount attempts?"
"Wear leveling approach?"
], ],
recommendation: "Use FAT32 for compatibility. Implement circular buffer with fixed-size files for wear distribution.", recommendation: "Degraded after 3 consecutive CRC errors or 5s write timeout. Max 3 remount attempts.",
esp32Impact: "ESP-IDF supports FAT32 on SD. SPI mode vs SDMMC mode?" esp32Impact: "Use ESP-IDF SDMMC error callbacks for detection."
}, },
{ {
id: "GAP-DATA-002", id: "GAP-DATA-002",
title: "Data Retention Policy Incomplete", title: "Data Pool (DP) Maximum Size",
description: "Configurable retention mentioned but no default values or limits.", description: "DP component for runtime/persistent data mentioned but maximum size not defined.",
questions: [ questions: [
"Default retention period?", "Maximum RAM allocation for DP?",
"Maximum storage allocation per data type?", "Number of data entries?",
"Overwrite priority (FIFO, priority-based)?" "Priority eviction policy?"
], ],
recommendation: "Default 7-day retention, FIFO overwrite, 80% SD capacity for sensor data.", recommendation: "DP max 32KB RAM. LRU eviction for non-critical data. Critical data always retained.",
esp32Impact: "No specific hardware constraint." esp32Impact: "225KB RAM budget allows 32KB for DP while maintaining headroom."
}, },
{ {
id: "GAP-DATA-003", id: "GAP-DATA-003",
title: "NVS Partition Size and Usage Undefined", title: "NVS Namespace Organization",
description: "NVS mentioned for machine constants but sizing not specified.", description: "NVS for machine constants but namespace organization not documented.",
questions: [ questions: [
"Expected size of machine constants?", "Namespace per feature group or global?",
"How many NVS entries expected?", "Key naming convention?",
"NVS encryption enabled?" "NVS corruption recovery?"
], ],
recommendation: "Allocate 64KB NVS minimum. Enable NVS encryption for sensitive data.", recommendation: "Namespace per feature group (nvs_daq, nvs_com, etc.). Use NVS backup partition.",
esp32Impact: "ESP32-S3 supports NVS encryption with flash encryption enabled." esp32Impact: "ESP32-S3 NVS supports multiple namespaces. Enable NVS encryption."
} }
] ]
}, },
{ {
category: "Diagnostics (FG-DIAG)", category: "Diagnostics (FG-DIAG)",
severity: "medium", severity: "low",
gaps: [ gaps: [
{ {
id: "GAP-DIAG-001", id: "GAP-DIAG-001",
title: "Diagnostic Code Registry Not Provided", title: "Diagnostic Export Format",
description: "Structured diagnostic codes required but no code table provided.", description: "Diagnostic persistence defined but export format for remote retrieval not specified.",
questions: [ questions: [
"What is the diagnostic code format?", "Binary or text-based export?",
"What are all defined diagnostic codes?", "Compression for large diagnostic logs?",
"Code ranges per subsystem?" "Filtering options for remote query?"
], ],
recommendation: "Create diagnostic code registry: 0x1xxx=DAQ, 0x2xxx=COM, 0x3xxx=SEC, etc.", recommendation: "CBOR binary format with optional gzip compression. Filter by severity/timestamp.",
esp32Impact: "No specific hardware constraint." esp32Impact: "CBOR encoding consistent with COM payload format."
}, },
{ {
id: "GAP-DIAG-002", id: "GAP-DIAG-002",
title: "Log Storage Capacity Undefined", title: "Coredump Partition Size Validation",
description: "Persistent diagnostics mentioned but storage limits not specified.", description: "64KB coredump partition specified but validation against actual coredump size needed.",
questions: [ questions: [
"Maximum log entries?", "Typical coredump size with current stack sizes?",
"Log rotation policy?", "Compression enabled for coredumps?",
"Crash dump storage?" "Multiple coredump retention?"
], ],
recommendation: "1000 diagnostic entries max, FIFO rotation, separate crash dump partition.", recommendation: "Enable coredump compression. 64KB sufficient for single compressed coredump.",
esp32Impact: "Consider coredump partition in flash (~64KB)." esp32Impact: "ESP-IDF coredump with compression typically 20-40KB."
} }
] ]
}, },
{ {
category: "System Management (FG-SYS)", category: "System Management (FG-SYS)",
severity: "medium", severity: "low",
gaps: [ gaps: [
{ {
id: "GAP-SYS-001", id: "GAP-SYS-001",
title: "Watchdog Timer Configuration Not Specified", title: "OLED Menu Navigation Timeout",
description: "System reliability requires WDT but parameters not defined.", description: "OLED menu system defined but navigation timeout/return to default screen not specified.",
questions: [ questions: [
"Task WDT timeout value?", "Menu inactivity timeout?",
"Which tasks are monitored?", "Default screen after timeout?",
"Interrupt WDT configuration?" "Screen saver/burn-in prevention?"
], ],
recommendation: "10-second task WDT, monitor all FreeRTOS tasks, enable interrupt WDT.", recommendation: "30-second menu timeout, return to status screen. Implement pixel shifting for OLED.",
esp32Impact: "ESP32-S3 has multiple WDT: Task WDT, Interrupt WDT, RTC WDT." esp32Impact: "Software implementation. Consider I2C bus sharing with sensors."
}, },
{ {
id: "GAP-SYS-002", id: "GAP-SYS-002",
title: "Power Management Strategy Undefined", title: "Button Debounce Configuration",
description: "Continuous AC power assumed but brownout/UPS handling not addressed.", description: "Physical buttons mentioned but debounce parameters not specified.",
questions: [ questions: [
"Brownout detection threshold?", "Debounce time (ms)?",
"UPS/battery backup expected?", "Long-press duration for special functions?",
"Graceful shutdown on power loss?" "Multi-button combinations?"
], ],
recommendation: "Enable brownout detector at 3.0V, implement emergency data flush on brownout interrupt.", recommendation: "50ms debounce, 2s long-press for reset/service mode entry.",
esp32Impact: "ESP32-S3 has programmable brownout detector." esp32Impact: "Use ESP-IDF GPIO ISR with FreeRTOS debounce task."
},
{
id: "GAP-SYS-003",
title: "Time Synchronization Details Missing",
description: "Synchronized time mentioned but sync mechanism not specified.",
questions: [
"NTP, GPS, or Main Hub time sync?",
"Sync interval?",
"Drift tolerance?",
"RTC backup?"
],
recommendation: "Sync from Main Hub on connect, resync every 1 hour, ±1 second tolerance.",
esp32Impact: "ESP32-S3 has RTC. External RTC battery may be needed for power loss."
} }
] ]
}, },
{ {
category: "Hardware Interface (HW)", category: "Hardware Abstraction (FG-HW)",
severity: "high", severity: "medium",
gaps: [ gaps: [
{ {
id: "GAP-HW-001", id: "GAP-HW-001",
title: "GPIO Pin Assignment Not Documented", title: "I2C Bus Sharing Strategy",
description: "Multiple interfaces but no pin mapping provided.", description: "Multiple I2C devices defined but bus arbitration/sharing strategy not documented.",
questions: [ questions: [
"Complete GPIO pin assignment table?", "Single I2C bus or multiple buses?",
"Reserved/strapping pins identified?", "I2C mutex for concurrent access?",
"I2C pull-up resistor values?" "Clock stretching handling?"
], ],
recommendation: "Create comprehensive GPIO map. Avoid strapping pins (GPIO0, GPIO45, GPIO46).", recommendation: "Use I2C_NUM_0 for sensors, I2C_NUM_1 for OLED. Mutex per bus for thread safety.",
esp32Impact: "ESP32-S3 has 45 GPIOs but some have restrictions (strapping, flash, PSRAM)." esp32Impact: "ESP32-S3 has 2x I2C controllers. Separate buses recommended for isolation."
}, },
{ {
id: "GAP-HW-002", id: "GAP-HW-002",
title: "OLED Display Specifications Missing", title: "SEN6x 5V Supply Management",
description: "OLED via I2C mentioned but no specifications.", description: "SEN6x requires 5V but ESP32-S3 is 3.3V. Level shifting/power supply not addressed.",
questions: [ questions: [
"Display resolution?", "5V supply source (USB VBUS, regulator)?",
"Controller chip (SSD1306, SH1106)?", "Level shifter for I2C lines?",
"I2C address?", "Power sequencing with 3.3V devices?"
"Refresh rate requirements?"
], ],
recommendation: "Recommend SSD1306 128x64 OLED at 0x3C, widely supported.", recommendation: "Use VBUS (5V) for SEN6x. SEN6x I2C is 3.3V compatible (open-drain).",
esp32Impact: "I2C bus speed consideration - 400kHz recommended for smooth updates." esp32Impact: "No level shifter needed for I2C. Ensure stable 5V supply."
}, },
{ {
id: "GAP-HW-003", id: "GAP-HW-003",
title: "SD Card Interface Mode Unspecified", title: "ENS160 1.8V Core Supply",
description: "SD card mentioned but interface mode not defined.", description: "ENS160 requires 1.8V core voltage. Supply configuration not specified.",
questions: [ questions: [
"SPI mode or SDMMC (1-bit/4-bit)?", "External LDO or module-integrated?",
"Maximum SD card size supported?", "LDO enable sequencing?",
"Speed class requirements?" "Current capability for ENS160?"
], ],
recommendation: "Use SDMMC 4-bit mode for best performance if GPIOs available, else SPI.", recommendation: "Use ENS160 module with integrated 1.8V LDO. Verify module specifications.",
esp32Impact: "ESP32-S3 supports both. SDMMC uses dedicated pins, SPI is more flexible." esp32Impact: "ESP32-S3 does not provide 1.8V output. External LDO or module required."
}
]
},
{
category: "Power & Fault Handling (FG-PWR)",
severity: "low",
gaps: [
{
id: "GAP-PWR-001",
title: "Supercapacitor Sizing Validation",
description: "0.5-1.0F supercapacitor recommended but sizing calculation not provided.",
questions: [
"Total current draw during emergency flush?",
"Minimum voltage for NVS write completion?",
"Supercapacitor ESR requirements?"
],
recommendation: "1F supercapacitor, ESR < 100mΩ. Provides ~1.5s at 200mA draw from 3.3V to 2.8V.",
esp32Impact: "Brownout threshold 3.0V. Allow margin for NVS write completion."
},
{
id: "GAP-PWR-002",
title: "RTC Battery Selection Criteria",
description: "External RTC battery marked as optional. Selection criteria not defined.",
questions: [
"Required RTC accuracy during power loss?",
"Battery lifetime expectations?",
"Temperature range for battery?"
],
recommendation: "CR2032 (220mAh, 3V) for typical applications. DS3231 RTC for ±2ppm accuracy.",
esp32Impact: "ESP32-S3 internal RTC drifts without power. External RTC recommended for >1min outages."
} }
] ]
}, },
@@ -394,27 +356,27 @@ const gapAnalysis = [
gaps: [ gaps: [
{ {
id: "GAP-DQC-001", id: "GAP-DQC-001",
title: "Calibration Data Format Undefined", title: "Calibration Coefficient Format",
description: "Calibration management mentioned but data structure not specified.", description: "Calibration management defined but coefficient storage format not specified.",
questions: [ questions: [
"What calibration parameters per sensor type?", "Polynomial order for calibration curves?",
"Polynomial coefficients, offset/gain, or lookup tables?", "Fixed-point or floating-point coefficients?",
"Calibration data versioning?" "Per-unit or batch calibration?"
], ],
recommendation: "Define per-sensor calibration structure with version, timestamp, and polynomial coefficients.", recommendation: "3rd order polynomial, 32-bit float coefficients. Per-unit calibration stored in NVS.",
esp32Impact: "No specific hardware constraint." esp32Impact: "ESP32-S3 FPU supports efficient float operations."
}, },
{ {
id: "GAP-DQC-002", id: "GAP-DQC-002",
title: "Sensor Validity Thresholds Not Defined", title: "Sensor Auto-Detection Mechanism",
description: "Out-of-range detection mentioned but thresholds not specified.", description: "Sensor identification mentioned but auto-detection protocol not defined.",
questions: [ questions: [
"Valid range per sensor type?", "I2C address scanning at boot?",
"Rate-of-change limits?", "WHO_AM_I register verification?",
"Stuck sensor detection criteria?" "Fallback if sensor not detected?"
], ],
recommendation: "Define per-sensor-type: min/max physical range, max rate of change, stuck duration.", recommendation: "I2C scan + WHO_AM_I verification. Log missing sensors, continue with available.",
esp32Impact: "No specific hardware constraint." esp32Impact: "I2C scan takes ~50ms per address. Optimize scan range."
} }
] ]
} }

View File

@@ -49,7 +49,7 @@ const typeColors: Record<string, string> = {
}; };
export default function Dashboard() { export default function Dashboard() {
const { data, loading, lastUpdated, refresh, typeCounts, groupedByType, parseLog, setData } = const { data, loading, lastUpdated, refresh, reloadFromCSV, typeCounts, groupedByType, parseLog, setData } =
useTraceabilityData(); useTraceabilityData();
const [showDebug, setShowDebug] = useState(false); const [showDebug, setShowDebug] = useState(false);
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);
@@ -156,7 +156,7 @@ export default function Dashboard() {
<RefreshCw className="h-4 w-4 mr-2" /> <RefreshCw className="h-4 w-4 mr-2" />
Update Data Update Data
</Button> </Button>
<Button variant="outline" onClick={refresh} disabled={loading}> <Button variant="outline" onClick={reloadFromCSV} disabled={loading}>
Reload from CSV Reload from CSV
</Button> </Button>
</div> </div>

View File

@@ -0,0 +1,659 @@
import { useState } from "react";
import { AppLayout } from "@/components/layout/AppLayout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import {
Cpu,
Thermometer,
Droplets,
Wind,
Gauge,
ExternalLink,
FileText,
Zap,
Cable,
Code,
Info,
CheckCircle,
AlertTriangle
} from "lucide-react";
import { cn } from "@/lib/utils";
// Sensor data with datasheet links
const sensors = {
bme680: {
id: 'bme680',
name: 'BME680',
manufacturer: 'Bosch Sensortec',
subtitle: 'Environmental 4-in-1 Sensor',
description: 'Low-power MEMS sensor measuring gas, pressure, temperature, and humidity. Critical for Indoor Air Quality (IAQ) monitoring.',
datasheetUrl: 'https://eu.mouser.com/datasheet/3/1278/1/A2F32BC94C3A80DFBB84AF6E5127DF06330B8846DE5C551EF47B9C5F80540CFA.pdf',
color: 'blue',
icon: Wind,
specs: {
'Supply Voltage': '1.71V - 3.6V',
'Interface': 'I2C / SPI',
'I2C Address': '0x76 (SDO=GND) / 0x77 (SDO=VDD)',
'Temp Range': '-40 to 85 °C',
'Pressure Range': '300 to 1100 hPa',
'Humidity Range': '0 to 100 %RH',
'Gas Sensing': 'VOCs (0-500 IAQ index)',
'Temp Accuracy': '±0.5 °C',
'Humidity Accuracy': '±3 %RH',
'Pressure Accuracy': '±0.6 hPa'
},
power: {
sleep: 0.00015,
idle: 0.15,
active: 12,
unit: 'mA'
},
wiring: [
{ pin: 'VDD', connect: '3.3V', note: 'Main supply' },
{ pin: 'GND', connect: 'GND', note: '' },
{ pin: 'SDA', connect: 'GPIO21', note: 'I2C Data' },
{ pin: 'SCL', connect: 'GPIO22', note: 'I2C Clock' },
{ pin: 'SDO', connect: 'GND or VDD', note: 'Address select' },
{ pin: 'CS', connect: 'VDD', note: 'I2C mode select' }
],
notes: [
'Requires Bosch BSEC2 library for IAQ scores',
'Native read provides raw gas resistance (Ohms)',
'Gas sensor requires 24-48h burn-in for accuracy',
'Keep away from direct heat sources'
]
},
ens160: {
id: 'ens160',
name: 'ENS160',
manufacturer: 'ScioSense',
subtitle: 'Digital Metal-Oxide Multi-Gas Sensor',
description: 'TrueVOC™ technology with on-chip algorithms for eCO2 and TVOC output. Features independent hotplates for selective gas detection.',
datasheetUrl: 'https://eu.mouser.com/datasheet/3/1500/1/518e5e0e1bfa8fdf441c96f0a3d093ad.pdf',
color: 'green',
icon: Wind,
specs: {
'Supply VDD': '1.71 - 1.98V',
'Supply VDDIO': '1.71 - 3.6V',
'Interface': 'I2C / SPI',
'I2C Address': '0x52 (Alt) / 0x53 (Default)',
'Outputs': 'TVOC (ppb), eCO2 (ppm), AQI-UBA',
'Warm-up Time': '3 min initial, 1 hr stabilization',
'TVOC Range': '0 - 65,000 ppb',
'eCO2 Range': '400 - 65,000 ppm'
},
power: {
sleep: 0.1,
idle: 0.9,
active: 29,
unit: 'mA'
},
wiring: [
{ pin: 'VDD', connect: '1.8V LDO', note: '1.8V core supply' },
{ pin: 'VDDIO', connect: '3.3V', note: 'I/O voltage' },
{ pin: 'GND', connect: 'GND', note: '' },
{ pin: 'SDA', connect: 'GPIO21', note: 'I2C Data' },
{ pin: 'SCL', connect: 'GPIO22', note: 'I2C Clock' },
{ pin: 'ADDR', connect: 'GND/VDD', note: 'Address select' }
],
notes: [
'Requires strict adherence to heater timings',
'Most modules include 1.8V LDO on-board',
'On-chip processing reduces ESP32 load',
'Independent hotplates for selective detection'
]
},
sen6x: {
id: 'sen6x',
name: 'SEN6x',
manufacturer: 'Sensirion',
subtitle: 'All-in-One Air Quality Module',
description: 'Comprehensive module integrating PM1.0, PM2.5, PM4.0, PM10, NOx, VOC, RH, and Temperature sensors. Simplifies hardware design.',
datasheetUrl: 'https://eu.mouser.com/datasheet/3/1278/1/Sensirion_Datasheet_SEN6x.pdf',
color: 'purple',
icon: Gauge,
specs: {
'Supply Voltage': '4.5V - 5.5V (5V typical)',
'Interface': 'I2C',
'I2C Address': '0x69 (fixed)',
'PM Accuracy': '±5 μg/m³ or ±5%',
'Particle Sizes': 'PM1.0, PM2.5, PM4.0, PM10',
'Additional Sensors': 'NOx, VOC, RH, Temperature',
'Lifetime': '> 10 years (24/7 operation)',
'Response Time': '< 10 seconds (PM)'
},
power: {
sleep: 2.6,
idle: 6.5,
active: 110,
unit: 'mA'
},
wiring: [
{ pin: 'PIN 1 (VDD)', connect: '5V (VBUS)', note: 'CRITICAL: 3.3V insufficient!' },
{ pin: 'PIN 2 (GND)', connect: 'GND', note: '' },
{ pin: 'PIN 3 (SDA)', connect: 'GPIO21', note: 'I2C Data (3.3V logic compatible)' },
{ pin: 'PIN 4 (SCL)', connect: 'GPIO22', note: 'I2C Clock' },
{ pin: 'PIN 5 (SEL)', connect: 'GND', note: 'I2C mode select' }
],
notes: [
'REQUIRES 5V supply - do not use 3.3V!',
'Logic levels are 3.3V compatible',
'Large data frames over I2C',
'Built-in fan for active sampling',
'Replaces multiple discrete sensors'
]
},
sht4xi: {
id: 'sht4xi',
name: 'SHT4xI',
manufacturer: 'Sensirion',
subtitle: 'Industrial Precision RH/T Sensor',
description: 'Ruggedized version designed for harsh environments. Features built-in heater to remove condensation and prevent drift in high humidity.',
datasheetUrl: 'https://eu.mouser.com/datasheet/3/1278/1/HT_DS_Datasheet_SHT4xI_Digital_1.pdf',
color: 'amber',
icon: Droplets,
specs: {
'Supply Voltage': '2.3V - 5.5V',
'Interface': 'I2C',
'I2C Address': '0x44 (fixed)',
'Temp Accuracy': '±0.1 °C',
'Humidity Accuracy': '±1.5 %RH',
'Temp Range': '-40 to 125 °C',
'Humidity Range': '0 to 100 %RH',
'Heater': 'Three levels (up to 200mW)'
},
power: {
sleep: 0.001,
idle: 0.02,
active: 0.35,
unit: 'mA'
},
wiring: [
{ pin: 'VDD', connect: '3.3V', note: 'Supply' },
{ pin: 'GND', connect: 'GND', note: '' },
{ pin: 'SDA', connect: 'GPIO21', note: 'I2C Data' },
{ pin: 'SCL', connect: 'GPIO22', note: 'I2C Clock' }
],
notes: [
'Simplest integration - standard I2C read',
'Heater commands block measurement during execution',
'Heater can draw up to 35mA at high setting',
'Industrial-grade: designed for harsh environments',
'Gold standard for T/RH reference measurement'
]
}
};
const colorMap: Record<string, { bg: string; border: string; text: string; light: string }> = {
blue: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', text: 'text-blue-600', light: 'bg-blue-50' },
green: { bg: 'bg-green-500/10', border: 'border-green-500/30', text: 'text-green-600', light: 'bg-green-50' },
purple: { bg: 'bg-purple-500/10', border: 'border-purple-500/30', text: 'text-purple-600', light: 'bg-purple-50' },
amber: { bg: 'bg-amber-500/10', border: 'border-amber-500/30', text: 'text-amber-600', light: 'bg-amber-50' }
};
// I2C Bus Map
const i2cBusMap = [
{ addr: '0x44', name: 'SHT4xI', color: 'amber' },
{ addr: '0x52', name: 'ENS160 (Alt)', color: 'green' },
{ addr: '0x53', name: 'ENS160 (Def)', color: 'green' },
{ addr: '0x69', name: 'SEN6x', color: 'purple' },
{ addr: '0x76', name: 'BME680 (SDO=GND)', color: 'blue' },
{ addr: '0x77', name: 'BME680 (SDO=VDD)', color: 'blue' }
];
export default function SelectedSensorsPage() {
const [selectedSensor, setSelectedSensor] = useState<string>('overview');
const [activeTab, setActiveTab] = useState<string>('specs');
const [powerModes, setPowerModes] = useState<Record<string, 'sleep' | 'idle' | 'active'>>({
bme680: 'idle',
ens160: 'idle',
sen6x: 'idle',
sht4xi: 'idle',
esp32: 'active'
});
const calculateTotalPower = () => {
let total = 0;
Object.entries(sensors).forEach(([key, sensor]) => {
const mode = powerModes[key] || 'idle';
total += sensor.power[mode];
});
// Add ESP32-S3 base power
total += powerModes.esp32 === 'active' ? 80 : 10;
return total;
};
const renderOverview = () => (
<div className="space-y-6">
{/* Header */}
<div className="pb-4 border-b">
<h2 className="text-2xl font-bold">System Integration Dashboard</h2>
<p className="text-muted-foreground mt-1">
ESP32-S3 environmental sensing array overview. Analyze power consumption and I2C topology before implementation.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Power Budget Calculator */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-amber-500" />
Power Consumption Simulator
<Badge variant="outline" className="ml-auto font-mono">3.3V Rail</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{Object.entries(sensors).map(([key, sensor]) => (
<div key={key} className="flex items-center justify-between p-2 hover:bg-muted/50 rounded">
<div className="flex items-center gap-3">
<span className="text-sm font-medium w-20">{sensor.name}</span>
<select
className="text-xs border rounded px-2 py-1"
value={powerModes[key]}
onChange={(e) => setPowerModes(prev => ({ ...prev, [key]: e.target.value as 'sleep' | 'idle' | 'active' }))}
>
<option value="sleep">Sleep</option>
<option value="idle">Idle</option>
<option value="active">Active</option>
</select>
</div>
<span className="font-mono text-sm">
{sensor.power[powerModes[key] || 'idle']} mA
</span>
</div>
))}
<div className="flex items-center justify-between p-2 hover:bg-muted/50 rounded">
<div className="flex items-center gap-3">
<span className="text-sm font-medium w-20">ESP32-S3</span>
<select
className="text-xs border rounded px-2 py-1"
value={powerModes.esp32}
onChange={(e) => setPowerModes(prev => ({ ...prev, esp32: e.target.value as 'idle' | 'active' }))}
>
<option value="idle">Idle (10mA)</option>
<option value="active">Active (80mA)</option>
</select>
</div>
<span className="font-mono text-sm">
{powerModes.esp32 === 'active' ? 80 : 10} mA
</span>
</div>
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Estimated Total</span>
<span className="text-2xl font-bold font-mono text-primary">
{calculateTotalPower().toFixed(2)} mA
</span>
</div>
</CardContent>
</Card>
{/* I2C Bus Map */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Cable className="h-5 w-5 text-emerald-500" />
I2C Bus Map
<Badge variant="outline" className="ml-auto font-mono">SDA/SCL</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="bg-muted/50 rounded-lg p-4 border min-h-[200px]">
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-2 font-mono">Master</p>
<div className="bg-foreground text-background p-2 rounded text-center font-mono text-sm mb-4">
ESP32-S3 (I2C Controller 0)
</div>
<div className="w-full h-px bg-border mb-4 relative">
<div className="absolute left-1/2 -top-1 w-2 h-2 bg-border rounded-full transform -translate-x-1/2" />
</div>
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-2 font-mono">Slaves</p>
<div className="flex flex-wrap gap-2">
{i2cBusMap.map((dev) => (
<div
key={dev.addr}
className={cn(
"flex-1 min-w-[90px] p-2 rounded text-center border",
colorMap[dev.color].bg,
colorMap[dev.color].border
)}
>
<span className={cn("block text-xs font-mono", colorMap[dev.color].text)}>
{dev.addr}
</span>
<span className="block text-sm font-medium">{dev.name}</span>
</div>
))}
</div>
</div>
<div className="mt-4 text-xs text-muted-foreground space-y-1">
<p> Disable internal pull-ups if external 2.2/4.7 resistors present.</p>
<p> SEN6x and SHT4x have fixed addresses. ENS160/BME680 are configurable.</p>
</div>
</CardContent>
</Card>
</div>
{/* Quick Sensor Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{Object.values(sensors).map((sensor) => {
const colors = colorMap[sensor.color];
const Icon = sensor.icon;
return (
<Card
key={sensor.id}
className={cn("cursor-pointer hover:shadow-md transition-shadow", colors.border)}
onClick={() => {
setSelectedSensor(sensor.id);
setActiveTab('specs');
}}
>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-lg">
<Icon className={cn("h-5 w-5", colors.text)} />
{sensor.name}
</CardTitle>
<CardDescription>{sensor.subtitle}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<Badge variant="outline" className={colors.text}>
{sensor.manufacturer}
</Badge>
<a
href={sensor.datasheetUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className={cn("flex items-center gap-1 text-xs hover:underline", colors.text)}
>
<FileText className="h-3 w-3" />
Datasheet
<ExternalLink className="h-3 w-3" />
</a>
</div>
</CardContent>
</Card>
);
})}
</div>
</div>
);
const renderSensorDetail = (sensorId: string) => {
const sensor = sensors[sensorId as keyof typeof sensors];
if (!sensor) return null;
const colors = colorMap[sensor.color];
const Icon = sensor.icon;
return (
<div className="space-y-6">
{/* Header */}
<div className="pb-4 border-b">
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3 mb-2">
<Button variant="ghost" size="sm" onClick={() => setSelectedSensor('overview')}>
Back
</Button>
<Badge className={cn(colors.bg, colors.text, colors.border)}>
{sensor.manufacturer}
</Badge>
</div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Icon className={cn("h-6 w-6", colors.text)} />
{sensor.name}: {sensor.subtitle}
</h2>
<p className="text-muted-foreground mt-1">{sensor.description}</p>
</div>
<a
href={sensor.datasheetUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90"
>
<FileText className="h-4 w-4" />
View Datasheet
<ExternalLink className="h-4 w-4" />
</a>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="specs" className="gap-2">
<Info className="h-4 w-4" />
Specifications
</TabsTrigger>
<TabsTrigger value="wiring" className="gap-2">
<Cable className="h-4 w-4" />
Wiring & Hardware
</TabsTrigger>
<TabsTrigger value="integration" className="gap-2">
<Code className="h-4 w-4" />
ESP-IDF Integration
</TabsTrigger>
</TabsList>
<TabsContent value="specs" className="mt-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Technical Specifications</CardTitle>
</CardHeader>
<CardContent>
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.entries(sensor.specs).map(([key, value]) => (
<div key={key} className="border-l-2 border-primary/30 pl-3">
<dt className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{key}</dt>
<dd className="mt-1 text-sm font-semibold">{value}</dd>
</div>
))}
</dl>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Power Consumption Profile</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-muted/50 rounded">
<span>Sleep Mode</span>
<span className="font-mono font-bold">{sensor.power.sleep} mA</span>
</div>
<div className="flex items-center justify-between p-3 bg-muted/50 rounded">
<span>Idle Mode</span>
<span className="font-mono font-bold">{sensor.power.idle} mA</span>
</div>
<div className="flex items-center justify-between p-3 bg-primary/10 rounded border border-primary/20">
<span>Active Mode</span>
<span className="font-mono font-bold text-primary">{sensor.power.active} mA</span>
</div>
</div>
<p className="text-xs text-muted-foreground mt-4 text-center">
Typical values at 3.3V / 25°C
</p>
</CardContent>
</Card>
</div>
<Card className="mt-6">
<CardHeader className={cn("rounded-t-lg", colors.light)}>
<CardTitle className={colors.text}>Important Notes</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<ul className="space-y-2">
{sensor.notes.map((note, idx) => (
<li key={idx} className="flex items-start gap-2">
{note.includes('CRITICAL') || note.includes('REQUIRES') ? (
<AlertTriangle className="h-4 w-4 text-amber-500 mt-0.5 shrink-0" />
) : (
<CheckCircle className="h-4 w-4 text-green-500 mt-0.5 shrink-0" />
)}
<span className="text-sm">{note}</span>
</li>
))}
</ul>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="wiring" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Pin Connections to ESP32-S3</CardTitle>
<CardDescription>Standard I2C wiring configuration</CardDescription>
</CardHeader>
<CardContent>
<div className="bg-muted/50 rounded-lg p-4 border">
<div className="font-mono text-sm space-y-2">
{sensor.wiring.map((wire, idx) => (
<div key={idx} className="flex items-center justify-between border-b border-border/50 py-2">
<span className="font-medium">{wire.pin}</span>
<span className={colors.text}> {wire.connect}</span>
{wire.note && (
<span className="text-xs text-muted-foreground">{wire.note}</span>
)}
</div>
))}
</div>
</div>
<div className="mt-6 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
<h4 className="font-semibold text-amber-800 dark:text-amber-200 mb-2">Pull-up Resistors</h4>
<ul className="text-sm text-amber-700 dark:text-amber-300 space-y-1">
<li> Use 4.7 pull-ups on SDA and SCL if not present on module</li>
<li> Disable ESP32 internal pull-ups when external resistors used</li>
<li> For 400kHz I2C, 2.2 pull-ups may be needed with long cables</li>
</ul>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="integration" className="mt-4">
<Card>
<CardHeader>
<CardTitle>ESP-IDF v5.4 Integration</CardTitle>
<CardDescription>I2C initialization and basic read pattern</CardDescription>
</CardHeader>
<CardContent>
<div className="bg-slate-950 text-slate-50 rounded-lg p-4 overflow-x-auto">
<pre className="text-sm font-mono">
{`// I2C Master Configuration for ${sensor.name}
#include "driver/i2c_master.h"
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_SDA GPIO_NUM_21
#define I2C_MASTER_SCL GPIO_NUM_22
#define I2C_MASTER_FREQ 400000 // 400kHz
#define ${sensor.name.toUpperCase()}_ADDR ${Object.keys(sensor.specs).includes('I2C Address') ? sensor.specs['I2C Address'].split(' ')[0] : '0x00'}
i2c_master_bus_handle_t bus_handle;
i2c_master_bus_config_t bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = I2C_MASTER_NUM,
.scl_io_num = I2C_MASTER_SCL,
.sda_io_num = I2C_MASTER_SDA,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = false,
};
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle));
// Add device
i2c_device_config_t dev_config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = ${sensor.name.toUpperCase()}_ADDR,
.scl_speed_hz = I2C_MASTER_FREQ,
};
i2c_master_dev_handle_t ${sensor.id}_handle;
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &dev_config, &${sensor.id}_handle));`}
</pre>
</div>
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
<h4 className="font-semibold text-blue-800 dark:text-blue-200 mb-2">Recommended Libraries</h4>
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1">
{sensor.id === 'bme680' && <li> <a href="https://github.com/boschsensortec/BSEC2-lib" target="_blank" rel="noopener noreferrer" className="underline">Bosch BSEC2 Library</a> for IAQ processing</li>}
{sensor.id === 'sen6x' && <li> <a href="https://github.com/Sensirion/embedded-sps" target="_blank" rel="noopener noreferrer" className="underline">Sensirion Embedded SPS</a> driver</li>}
{sensor.id === 'sht4xi' && <li> <a href="https://github.com/Sensirion/embedded-sht" target="_blank" rel="noopener noreferrer" className="underline">Sensirion Embedded SHT</a> driver</li>}
<li> ESP-IDF I2C Master API (v5.4)</li>
</ul>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
return (
<AppLayout>
<div className="space-y-6">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold flex items-center gap-3">
<Cpu className="h-8 w-8 text-primary" />
Selected Sensors
</h1>
<p className="text-muted-foreground mt-1">
ESP32-S3 Sensor Array Analysis & Integration Guide
</p>
</div>
<Badge variant="outline" className="font-mono">
ESP-IDF v5.4 | ESP32-S3
</Badge>
</div>
{/* Navigation */}
<div className="flex gap-2 flex-wrap">
<Button
variant={selectedSensor === 'overview' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedSensor('overview')}
>
Dashboard & Bus Map
</Button>
{Object.values(sensors).map((sensor) => {
const colors = colorMap[sensor.color];
return (
<Button
key={sensor.id}
variant={selectedSensor === sensor.id ? 'default' : 'outline'}
size="sm"
onClick={() => {
setSelectedSensor(sensor.id);
setActiveTab('specs');
}}
className={selectedSensor === sensor.id ? '' : colors.text}
>
{sensor.name}
</Button>
);
})}
</div>
<Separator />
{/* Content */}
<ScrollArea className="h-[calc(100vh-280px)]">
{selectedSensor === 'overview' ? renderOverview() : renderSensorDetail(selectedSensor)}
</ScrollArea>
</div>
</AppLayout>
);
}

File diff suppressed because it is too large Load Diff