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

View File

@@ -12,19 +12,15 @@ import {
CheckCircle,
XCircle,
AlertCircle,
Cloud,
Server,
RefreshCw,
Loader2
} from 'lucide-react';
import { WorkPackage } from '@/types/traceability';
import { parseCSVContent, ParseResult } from '@/lib/csvParser';
import {
fetchFromOpenProject,
fetchFromBackendProxy,
OpenProjectConfig,
DEFAULT_CONFIG
} from '@/services/openProjectService';
const STORAGE_KEY = 'traceability_data';
const SERVER_URL_KEY = 'traceability_server_url';
interface DataUpdateDialogProps {
onDataLoaded: (workPackages: WorkPackage[]) => void;
@@ -40,11 +36,10 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
const [fileName, setFileName] = useState<string>('');
const fileInputRef = useRef<HTMLInputElement>(null);
// Backend proxy config
const [proxyUrl, setProxyUrl] = useState('');
// Direct OpenProject config
const [opConfig, setOpConfig] = useState<OpenProjectConfig>(DEFAULT_CONFIG);
// Server endpoint config - persisted
const [serverUrl, setServerUrl] = useState(() =>
localStorage.getItem(SERVER_URL_KEY) || '/api/traceability'
);
// Drag and drop state
const [isDragging, setIsDragging] = useState(false);
@@ -53,6 +48,15 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
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) => {
setFileName(file.name);
setIsLoading(true);
@@ -60,10 +64,13 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
setErrors([]);
try {
addLog(`📂 Reading file: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`);
const text = await file.text();
addLog(`📝 File loaded, ${text.length} characters`);
const result = parseCSVContent(text);
setParseResult(result);
setLogs(result.logs);
setLogs(prev => [...prev, ...result.logs]);
setErrors(result.errors);
} finally {
setIsLoading(false);
@@ -84,66 +91,87 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
if (file) handleFile(file);
};
const handleFetchFromProxy = async () => {
if (!proxyUrl) {
setErrors(['Please enter a backend proxy URL']);
const handleFetchFromServer = async () => {
if (!serverUrl) {
setErrors(['Please enter a server endpoint URL']);
return;
}
// Save URL for next time
localStorage.setItem(SERVER_URL_KEY, serverUrl);
setIsLoading(true);
setLogs([]);
setErrors([]);
setParseResult(null);
try {
const result = await fetchFromBackendProxy(proxyUrl, addLog);
setLogs(result.logs);
setErrors(result.errors);
addLog(`🔍 Connecting to server: ${serverUrl}`);
if (result.success) {
const typeCounts = result.workPackages.reduce((acc, wp) => {
const response = await fetch(serverUrl, {
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;
return acc;
}, {} as Record<string, number>);
setParseResult({
success: true,
workPackages: result.workPackages,
logs: result.logs,
errors: result.errors,
workPackages,
logs: [],
errors: [],
typeCounts
});
}
} finally {
setIsLoading(false);
}
};
const handleFetchFromOpenProject = async () => {
setIsLoading(true);
setLogs([]);
setErrors([]);
setParseResult(null);
try {
const result = await fetchFromOpenProject(opConfig, undefined, addLog);
setLogs(result.logs);
} 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);
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
});
setParseResult(result);
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
setErrors([errorMsg]);
addLog(`❌ Error: ${errorMsg}`);
} finally {
setIsLoading(false);
}
@@ -151,6 +179,8 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
const handleApply = () => {
if (parseResult?.success && parseResult.workPackages.length > 0) {
// Persist to localStorage for other users/sessions
persistData(parseResult.workPackages);
onDataLoaded(parseResult.workPackages);
onClose?.();
}
@@ -174,23 +204,19 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
Update Traceability Data
</CardTitle>
<CardDescription>
Choose how to update your data from OpenProject
Upload a CSV file or fetch from your server
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<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">
<Upload className="h-4 w-4" />
Upload CSV
</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" />
Backend API
</TabsTrigger>
<TabsTrigger value="direct" className="flex items-center gap-2">
<Cloud className="h-4 w-4" />
Direct Fetch
Server Fetch
</TabsTrigger>
</TabsList>
@@ -222,103 +248,46 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
</p>
</div>
<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>
</TabsContent>
{/* Tab 2: Backend Proxy */}
<TabsContent value="proxy" className="space-y-4">
{/* Tab 2: Server Fetch */}
<TabsContent value="server" className="space-y-4">
<div className="space-y-3">
<div>
<Label htmlFor="proxyUrl">Backend API Endpoint</Label>
<Label htmlFor="serverUrl">Server Endpoint</Label>
<Input
id="proxyUrl"
placeholder="https://your-server.com/api/traceability"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
id="serverUrl"
placeholder="/api/traceability or https://your-server.com/api/data"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
<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>
</div>
<Button
onClick={handleFetchFromProxy}
disabled={isLoading || !proxyUrl}
onClick={handleFetchFromServer}
disabled={isLoading || !serverUrl}
className="w-full"
>
{isLoading ? (
<><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>
</div>
<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">
<li>Deploy the Python script on your server</li>
<li>Create an API endpoint that runs the script</li>
<li>Return JSON or serve the generated CSV</li>
<li>Create an endpoint that runs <code>get_traceability.py</code></li>
<li>Return the CSV file or JSON with work packages</li>
<li>Example: <code>GET /api/traceability</code> returns CSV</li>
</ol>
</div>
</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>
{/* Results Section */}
@@ -387,7 +356,7 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
{parseResult?.success && (
<Button onClick={handleApply}>
<CheckCircle className="h-4 w-4 mr-2" />
Apply Data
Apply & Save
</Button>
)}
<Button variant="outline" onClick={handleReset}>

View File

@@ -18,6 +18,7 @@ import {
ChevronRight,
Cpu,
Share2,
Thermometer,
} from "lucide-react";
import { NavLink } from "@/components/NavLink";
import { useLocation } from "react-router-dom";
@@ -40,6 +41,7 @@ const mainItems = [
{ title: "Work Package Graph", url: "/graph", icon: Share2 },
{ title: "Documentation", url: "/documentation", icon: BookOpen },
{ title: "Gap Analysis", url: "/analysis", icon: Search },
{ title: "Selected Sensors", url: "/sensors", icon: Thermometer },
{ 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';
// Default documentation structure
// Default documentation structure - updated with all feature groups
const defaultDocuments: DocumentFile[] = [
// System Overview
{
id: 'about-asf',
title: 'About ASF (Agricultural Sensor Framework)',
@@ -17,16 +18,26 @@ const defaultDocuments: DocumentFile[] = [
{
id: 'system-assumptions',
title: 'System Assumptions & Limitations',
description: 'Design constraints and environmental assumptions.',
description: 'Design constraints, environmental assumptions, and system limitations.',
category: 'System Overview',
content: '# System Assumptions\n\nUpload the System_Assumptions_Limitations.md file to see full documentation.',
fileName: 'System_Assumptions_Limitations.md',
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',
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',
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',
@@ -35,7 +46,7 @@ const defaultDocuments: DocumentFile[] = [
{
id: 'fg-dqc',
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',
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',
@@ -44,7 +55,7 @@ const defaultDocuments: DocumentFile[] = [
{
id: 'fg-com',
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',
content: '# Communication Features\n\nUpload the COM_Communication_Features.md file to see full documentation.',
fileName: 'COM_Communication_Features.md',
@@ -53,7 +64,7 @@ const defaultDocuments: DocumentFile[] = [
{
id: 'fg-diag',
title: 'FG-DIAG: Diagnostics',
description: 'System health monitoring and fault detection.',
description: 'Fault detection, classification, diagnostic code registry, and health monitoring.',
category: 'Feature Groups',
content: '# Diagnostics Features\n\nUpload the DIAG_Diagnostics_Health_Monitoring_Features.md file to see full documentation.',
fileName: 'DIAG_Diagnostics_Health_Monitoring_Features.md',
@@ -62,7 +73,7 @@ const defaultDocuments: DocumentFile[] = [
{
id: 'fg-data',
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',
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',
@@ -71,7 +82,7 @@ const defaultDocuments: DocumentFile[] = [
{
id: 'fg-ota',
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',
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',
@@ -79,8 +90,8 @@ const defaultDocuments: DocumentFile[] = [
},
{
id: 'fg-sec',
title: 'FG-SEC: Security',
description: 'Authentication, encryption, and access control.',
title: 'FG-SEC: Security & Safety',
description: 'Secure Boot V2, flash encryption AES-256, mTLS with X.509, and anti-rollback.',
category: 'Feature Groups',
content: '# Security Features\n\nUpload the SEC_Security_Safety_Features.md file to see full documentation.',
fileName: 'SEC_Security_Safety_Features.md',
@@ -89,21 +100,50 @@ const defaultDocuments: DocumentFile[] = [
{
id: 'fg-sys',
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',
content: '# System Management Features\n\nUpload the SYS_System_Management_Features.md file to see full documentation.',
fileName: 'SYS_System_Management_Features.md',
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',
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',
content: '# State Machine Specification\n\nUpload the System_State_Machine_Specification.md file to see full documentation.',
fileName: 'System_State_Machine_Specification.md',
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',
title: 'Software Requirements Specification (SRS)',
@@ -113,6 +153,16 @@ const defaultDocuments: DocumentFile[] = [
fileName: 'SRS.md',
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',
title: 'Annex A: Traceability Matrix',
@@ -125,16 +175,17 @@ const defaultDocuments: DocumentFile[] = [
{
id: 'vv-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',
content: '# V&V Matrix\n\nUpload the VV_Matrix.md file to see full documentation.',
fileName: 'VV_Matrix.md',
lastUpdated: new Date().toISOString()
},
// Interfaces & Budgets
{
id: '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',
content: '# External Interfaces\n\nUpload the Annex_B_Interfaces.md file to see full documentation.',
fileName: 'Annex_B_Interfaces.md',
@@ -143,7 +194,7 @@ const defaultDocuments: DocumentFile[] = [
{
id: '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',
content: '# Resource Budgets\n\nUpload the Annex_C_Budgets.md file to see full documentation.',
fileName: 'Annex_C_Budgets.md',

View File

@@ -1,6 +1,8 @@
import { useState, useEffect, useCallback } from 'react';
import { WorkPackage, TraceabilityData, WorkPackageType } from '@/types/traceability';
const STORAGE_KEY = 'traceability_data';
export function useTraceabilityData() {
const [data, setData] = useState<TraceabilityData | null>(null);
const [loading, setLoading] = useState(true);
@@ -16,11 +18,6 @@ export function useTraceabilityData() {
const cleanText = csvText.replace(/^\uFEFF/, '');
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');
logs.push(`[CSV Parser] Total lines: ${lines.length}`);
@@ -31,7 +28,6 @@ export function useTraceabilityData() {
let currentRow: string[] = [];
let inQuotedField = false;
let currentField = '';
let lineNum = 1;
// Process character by character for proper CSV parsing
const content = lines.slice(1).join('\n');
@@ -43,11 +39,9 @@ export function useTraceabilityData() {
if (inQuotedField) {
if (char === '"') {
if (nextChar === '"') {
// Escaped quote
currentField += '"';
i++;
} else {
// End of quoted field
inQuotedField = false;
}
} else {
@@ -55,7 +49,6 @@ export function useTraceabilityData() {
}
} else {
if (char === '"' && currentField === '') {
// Start of quoted field
inQuotedField = true;
} else if (char === ',') {
currentRow.push(currentField);
@@ -64,11 +57,9 @@ export function useTraceabilityData() {
currentRow.push(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())) {
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({
id: parseInt(id, 10),
@@ -86,7 +77,6 @@ export function useTraceabilityData() {
}
currentRow = [];
lineNum++;
} else {
currentField += char;
}
@@ -98,7 +88,7 @@ export function useTraceabilityData() {
currentRow.push(currentField);
if (currentRow.length >= 4 && /^\d+$/.test(currentRow[0]?.trim())) {
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({
id: parseInt(id, 10),
@@ -124,6 +114,7 @@ export function useTraceabilityData() {
return { workPackages, logs };
}, []);
// Try to load from localStorage first, then fall back to CSV file
const loadData = useCallback(async () => {
setLoading(true);
setError(null);
@@ -131,7 +122,31 @@ export function useTraceabilityData() {
newLogs.push(`[Loader] Starting data load at ${new Date().toLocaleTimeString()}`);
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()}`;
newLogs.push(`[Loader] Fetching /data/traceability_export.csv${cacheBuster}`);
@@ -145,18 +160,71 @@ export function useTraceabilityData() {
const { workPackages, logs } = parseCSV(csvText);
// Combine loader logs with parser logs
const allLogs = [...newLogs, ...logs];
allLogs.push(`[Loader] Data load complete at ${new Date().toLocaleTimeString()}`);
setParseLog(allLogs);
const now = new Date();
setData({
const dataObj = {
lastUpdated: now,
workPackages
});
};
setData(dataObj);
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) {
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
newLogs.push(`[Loader] ERROR: ${errorMsg}`);
@@ -171,6 +239,20 @@ export function useTraceabilityData() {
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(() => {
loadData();
}, [loadData]);
@@ -195,9 +277,10 @@ export function useTraceabilityData() {
error,
lastUpdated,
refresh,
reloadFromCSV,
groupedByType,
typeCounts,
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" }
};
// Gap Analysis Data
// Gap Analysis Data - Updated based on latest feature engineering specifications
const gapAnalysis = [
{
category: "Communication (FG-COM)",
severity: "critical",
severity: "medium",
gaps: [
{
id: "GAP-COM-001",
title: "WiFi Protocol Details Missing",
description: "Documents mention 'WiFi / Zigbee / LoRa' but lack specific protocol selection and rationale.",
title: "LoRa Module Specifications Missing",
description: "LoRa fallback is marked as 'optional external module' but no module selection criteria provided.",
questions: [
"Which WiFi standard specifically? (802.11b/g/n all supported by ESP32-S3)",
"What is the target data rate requirement?",
"Is 2.4GHz band acceptable (5GHz not supported)?",
"What is the expected range/coverage?"
"Which LoRa module model is recommended? (SX1276, SX1262, etc.)",
"What frequency band? (868MHz EU, 915MHz US)",
"SPI or UART interface preference?"
],
recommendation: "Define primary protocol as WiFi 802.11n for best throughput. Add Zigbee/LoRa as optional fallback.",
esp32Impact: "ESP32-S3 supports 802.11 b/g/n at 2.4GHz up to 150 Mbps. No 5GHz support."
recommendation: "Define approved LoRa module (recommend SX1262) with frequency band configuration.",
esp32Impact: "ESP32-S3 has native SPI support for LoRa modules. No hardware limitation."
},
{
id: "GAP-COM-002",
title: "TLS/DTLS Version Unspecified",
description: "Security requires TLS/DTLS but version and cipher suites are not defined.",
title: "ESP-NOW Encryption Details",
description: "ESP-NOW peer communication mentioned but encryption method not fully specified.",
questions: [
"TLS 1.2 or TLS 1.3?",
"Which cipher suites are acceptable?",
"Certificate management approach?",
"Key rotation policy?"
"Use ESP-NOW built-in encryption or application-layer?",
"Key exchange mechanism for peer discovery?",
"Maximum peer count for mesh scenarios?"
],
recommendation: "Use TLS 1.2 with AES-128-GCM for ESP32-S3 hardware acceleration compatibility.",
esp32Impact: "ESP32-S3 has hardware crypto accelerators for AES-128/256, SHA, RSA."
recommendation: "Use ESP-NOW CCMP encryption with pre-shared LMK (Local Master Key).",
esp32Impact: "ESP-NOW supports up to 20 encrypted peers. Hardware crypto accelerated."
},
{
id: "GAP-COM-003",
title: "Peer-to-Peer Protocol Undefined",
description: "F-COM-03 mentions peer Sensor Hub communication but no protocol specified.",
title: "CBOR Schema Version Management",
description: "CBOR versioned payloads mentioned but schema versioning strategy not defined.",
questions: [
"What transport for peer-to-peer? (WiFi Direct, BLE, ESP-NOW?)",
"What is the maximum peer count?",
"Discovery mechanism?"
"How are schema versions negotiated?",
"Backward compatibility requirements?",
"Schema registry location?"
],
recommendation: "Consider ESP-NOW for low-latency peer communication (250 byte payload, no WiFi connection needed).",
esp32Impact: "ESP-NOW supported natively. BLE Mesh also available."
},
{
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."
recommendation: "Include schema version in CBOR header. Maintain N-1 backward compatibility.",
esp32Impact: "No hardware impact. Software versioning only."
}
]
},
{
category: "Data Acquisition (FG-DAQ)",
severity: "high",
severity: "low",
gaps: [
{
id: "GAP-DAQ-001",
title: "Specific Sensor Models Not Defined",
description: "Sensor types listed (Temp, Humidity, CO2, etc.) but no specific models or part numbers.",
title: "Sensor Warm-Up State Transitions",
description: "Sensor states defined (INIT, WARMUP, STABLE, etc.) but warm-up duration per sensor type not specified.",
questions: [
"What are the specific sensor models/part numbers?",
"What are the I2C addresses for each sensor type?",
"Are there alternative/fallback sensor models?"
"What are warm-up times for SHT4xI, BME680, ENS160, SEN6x?",
"Should WARMUP readings be discarded or flagged?",
"Power-on sequencing between sensors?"
],
recommendation: "Create a Sensor Catalog with approved models, I2C addresses, and driver requirements.",
esp32Impact: "ESP32-S3 has 2x I2C buses - ensure no address conflicts if using multiple sensors."
recommendation: "Define per-sensor warm-up: SHT4xI (1ms), BME680 (3s gas), ENS160 (3min initial), SEN6x (30s).",
esp32Impact: "Software timing only. Consider FreeRTOS delayed start for sensors."
},
{
id: "GAP-DAQ-002",
title: "ADC Channel Allocation Undefined",
description: "Analog sensors mentioned but no ADC channel assignment or calibration approach.",
title: "Multi-Sensor Fusion Strategy",
description: "Multiple sensors measure overlapping parameters (T/RH from SHT4xI, BME680, SEN6x). Fusion not specified.",
questions: [
"How many analog sensors are expected?",
"Which ADC channels will be used?",
"What reference voltage?",
"ADC calibration approach?"
"Which sensor is primary for temperature/humidity?",
"Cross-validation threshold for sensor disagreement?",
"Fallback priority order?"
],
recommendation: "Define ADC channel map. Use eFuse-stored calibration for ESP32-S3 ADC linearity correction.",
esp32Impact: "ESP32-S3 has 20 ADC channels, 12-bit resolution. Some GPIOs are ADC1 (usable with WiFi), some ADC2 (conflicts with WiFi)."
},
{
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."
recommendation: "Primary: SHT4xI for T/RH (highest accuracy). Cross-validate against BME680/SEN6x.",
esp32Impact: "CPU budget allows sensor fusion algorithms."
}
]
},
{
category: "Security (FG-SEC)",
severity: "critical",
severity: "medium",
gaps: [
{
id: "GAP-SEC-001",
title: "Key Management Lifecycle Undefined",
description: "Secure boot and encryption mentioned but key provisioning and rotation not specified.",
title: "Certificate Provisioning Workflow",
description: "mTLS with X.509 specified but certificate provisioning during manufacturing not detailed.",
questions: [
"How are keys provisioned during manufacturing?",
"Key rotation policy for TLS certificates?",
"Key storage location (eFuse vs NVS encrypted)?",
"Revocation mechanism?"
"Factory provisioning via UART/JTAG?",
"Certificate signing authority (CA) chain?",
"Device CSR generation or pre-signed?"
],
recommendation: "Define key provisioning during manufacturing, use eFuse for secure boot keys, NVS encrypted partition for runtime keys.",
esp32Impact: "ESP32-S3 has eFuse for permanent key storage and hardware secure boot support."
recommendation: "Factory provision root CA + device certificate via secure JTAG. Use ESP-IDF secure provisioning.",
esp32Impact: "ESP32-S3 supports secure JTAG provisioning. eFuse protection available."
},
{
id: "GAP-SEC-002",
title: "Authentication Method Not Specified",
description: "Authentication required but mechanism not defined (certificates, PSK, tokens?).",
title: "eFuse Burn Sequence Documentation",
description: "eFuse for secure boot and anti-rollback mentioned but burn sequence/order not documented.",
questions: [
"Mutual TLS with certificates?",
"Pre-shared keys?",
"Device identity provisioning?"
"eFuse programming order for production?",
"Testing strategy before permanent eFuse burn?",
"Recovery if eFuse burn fails mid-sequence?"
],
recommendation: "Use X.509 certificates with device-unique identity for mutual TLS.",
esp32Impact: "ESP32-S3 supports X.509, may need external crypto chip for many certificates."
recommendation: "Document eFuse burn sequence: 1) Flash encryption key, 2) Secure boot key, 3) JTAG disable.",
esp32Impact: "eFuse operations are irreversible. Production checklist critical."
},
{
id: "GAP-SEC-003",
title: "Anti-Rollback Policy Vague",
description: "Rollback prevention mentioned as 'optional future' but critical for security.",
title: "Debug Port Security in Production",
description: "Debug session support mentioned but production debug port security not fully addressed.",
questions: [
"Is anti-rollback mandatory?",
"What version tracking mechanism?",
"Emergency recovery procedure?"
"JTAG disabled in production builds?",
"Secure debug authentication method?",
"Debug log filtering for sensitive data?"
],
recommendation: "Implement anti-rollback using ESP32-S3 eFuse version counter.",
esp32Impact: "ESP32-S3 supports secure version tracking in eFuse (limited burn count)."
recommendation: "Disable JTAG via eFuse in production. Use authenticated debug over UART with session tokens.",
esp32Impact: "ESP32-S3 supports eFuse-based JTAG disable and secure debug."
}
]
},
{
category: "OTA Updates (FG-OTA)",
severity: "high",
severity: "low",
gaps: [
{
id: "GAP-OTA-001",
title: "Partition Scheme Not Defined",
description: "Dual-partition OTA implied but specific partition layout not documented.",
title: "Factory Partition Use Case",
description: "Factory partition marked as 'optional' in partition table. Recovery scenario unclear.",
questions: [
"What is the partition table layout?",
"Factory partition included?",
"Partition sizes for app0/app1?",
"NVS partition size?"
"When is factory partition used for recovery?",
"How to trigger factory reset from corrupted OTA?",
"Factory partition size requirements?"
],
recommendation: "Define: factory (4MB) + ota_0 (4MB) or app0 + app1 if 8MB flash. Reserve 64KB for NVS.",
esp32Impact: "8MB flash allows dual 3.5MB app partitions with ample NVS/data space."
recommendation: "Include minimal factory partition (~1MB) for emergency recovery via GPIO boot mode.",
esp32Impact: "8MB flash can accommodate factory + 2x OTA partitions."
},
{
id: "GAP-OTA-002",
title: "OTA Chunk Size Not Specified",
description: "Firmware transferred 'in chunks' but chunk size and protocol not defined.",
title: "OTA Progress Reporting Granularity",
description: "OTA status reporting mentioned but progress indication granularity not specified.",
questions: [
"What chunk size? (4KB, 16KB, other?)",
"Acknowledgment per chunk or batch?",
"Resume capability after interruption?"
"Progress update frequency (per chunk, percentage)?",
"OLED display update during OTA?",
"MQTT progress topic structure?"
],
recommendation: "Use 4KB chunks with per-chunk CRC and resume from last successful chunk.",
esp32Impact: "4KB aligns with flash page size for efficient writes."
},
{
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."
recommendation: "Report progress every 5% or 30 seconds, whichever comes first. Update OLED display.",
esp32Impact: "No hardware limitation. Balance progress updates vs. flash write performance."
}
]
},
{
category: "Persistence (FG-DATA)",
severity: "high",
severity: "medium",
gaps: [
{
id: "GAP-DATA-001",
title: "SD Card File System Not Specified",
description: "SD card storage mentioned but filesystem type not defined.",
title: "SD Card Failure Detection Criteria",
description: "SD_DEGRADED state defined but specific failure detection criteria not specified.",
questions: [
"FAT32, exFAT, or custom filesystem?",
"Maximum file size?",
"Directory structure?",
"Wear leveling approach?"
"CRC error threshold for degraded state?",
"Write timeout threshold?",
"Automatic SD card remount attempts?"
],
recommendation: "Use FAT32 for compatibility. Implement circular buffer with fixed-size files for wear distribution.",
esp32Impact: "ESP-IDF supports FAT32 on SD. SPI mode vs SDMMC mode?"
recommendation: "Degraded after 3 consecutive CRC errors or 5s write timeout. Max 3 remount attempts.",
esp32Impact: "Use ESP-IDF SDMMC error callbacks for detection."
},
{
id: "GAP-DATA-002",
title: "Data Retention Policy Incomplete",
description: "Configurable retention mentioned but no default values or limits.",
title: "Data Pool (DP) Maximum Size",
description: "DP component for runtime/persistent data mentioned but maximum size not defined.",
questions: [
"Default retention period?",
"Maximum storage allocation per data type?",
"Overwrite priority (FIFO, priority-based)?"
"Maximum RAM allocation for DP?",
"Number of data entries?",
"Priority eviction policy?"
],
recommendation: "Default 7-day retention, FIFO overwrite, 80% SD capacity for sensor data.",
esp32Impact: "No specific hardware constraint."
recommendation: "DP max 32KB RAM. LRU eviction for non-critical data. Critical data always retained.",
esp32Impact: "225KB RAM budget allows 32KB for DP while maintaining headroom."
},
{
id: "GAP-DATA-003",
title: "NVS Partition Size and Usage Undefined",
description: "NVS mentioned for machine constants but sizing not specified.",
title: "NVS Namespace Organization",
description: "NVS for machine constants but namespace organization not documented.",
questions: [
"Expected size of machine constants?",
"How many NVS entries expected?",
"NVS encryption enabled?"
"Namespace per feature group or global?",
"Key naming convention?",
"NVS corruption recovery?"
],
recommendation: "Allocate 64KB NVS minimum. Enable NVS encryption for sensitive data.",
esp32Impact: "ESP32-S3 supports NVS encryption with flash encryption enabled."
recommendation: "Namespace per feature group (nvs_daq, nvs_com, etc.). Use NVS backup partition.",
esp32Impact: "ESP32-S3 NVS supports multiple namespaces. Enable NVS encryption."
}
]
},
{
category: "Diagnostics (FG-DIAG)",
severity: "medium",
severity: "low",
gaps: [
{
id: "GAP-DIAG-001",
title: "Diagnostic Code Registry Not Provided",
description: "Structured diagnostic codes required but no code table provided.",
title: "Diagnostic Export Format",
description: "Diagnostic persistence defined but export format for remote retrieval not specified.",
questions: [
"What is the diagnostic code format?",
"What are all defined diagnostic codes?",
"Code ranges per subsystem?"
"Binary or text-based export?",
"Compression for large diagnostic logs?",
"Filtering options for remote query?"
],
recommendation: "Create diagnostic code registry: 0x1xxx=DAQ, 0x2xxx=COM, 0x3xxx=SEC, etc.",
esp32Impact: "No specific hardware constraint."
recommendation: "CBOR binary format with optional gzip compression. Filter by severity/timestamp.",
esp32Impact: "CBOR encoding consistent with COM payload format."
},
{
id: "GAP-DIAG-002",
title: "Log Storage Capacity Undefined",
description: "Persistent diagnostics mentioned but storage limits not specified.",
title: "Coredump Partition Size Validation",
description: "64KB coredump partition specified but validation against actual coredump size needed.",
questions: [
"Maximum log entries?",
"Log rotation policy?",
"Crash dump storage?"
"Typical coredump size with current stack sizes?",
"Compression enabled for coredumps?",
"Multiple coredump retention?"
],
recommendation: "1000 diagnostic entries max, FIFO rotation, separate crash dump partition.",
esp32Impact: "Consider coredump partition in flash (~64KB)."
recommendation: "Enable coredump compression. 64KB sufficient for single compressed coredump.",
esp32Impact: "ESP-IDF coredump with compression typically 20-40KB."
}
]
},
{
category: "System Management (FG-SYS)",
severity: "medium",
severity: "low",
gaps: [
{
id: "GAP-SYS-001",
title: "Watchdog Timer Configuration Not Specified",
description: "System reliability requires WDT but parameters not defined.",
title: "OLED Menu Navigation Timeout",
description: "OLED menu system defined but navigation timeout/return to default screen not specified.",
questions: [
"Task WDT timeout value?",
"Which tasks are monitored?",
"Interrupt WDT configuration?"
"Menu inactivity timeout?",
"Default screen after timeout?",
"Screen saver/burn-in prevention?"
],
recommendation: "10-second task WDT, monitor all FreeRTOS tasks, enable interrupt WDT.",
esp32Impact: "ESP32-S3 has multiple WDT: Task WDT, Interrupt WDT, RTC WDT."
recommendation: "30-second menu timeout, return to status screen. Implement pixel shifting for OLED.",
esp32Impact: "Software implementation. Consider I2C bus sharing with sensors."
},
{
id: "GAP-SYS-002",
title: "Power Management Strategy Undefined",
description: "Continuous AC power assumed but brownout/UPS handling not addressed.",
title: "Button Debounce Configuration",
description: "Physical buttons mentioned but debounce parameters not specified.",
questions: [
"Brownout detection threshold?",
"UPS/battery backup expected?",
"Graceful shutdown on power loss?"
"Debounce time (ms)?",
"Long-press duration for special functions?",
"Multi-button combinations?"
],
recommendation: "Enable brownout detector at 3.0V, implement emergency data flush on brownout interrupt.",
esp32Impact: "ESP32-S3 has programmable brownout detector."
},
{
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."
recommendation: "50ms debounce, 2s long-press for reset/service mode entry.",
esp32Impact: "Use ESP-IDF GPIO ISR with FreeRTOS debounce task."
}
]
},
{
category: "Hardware Interface (HW)",
severity: "high",
category: "Hardware Abstraction (FG-HW)",
severity: "medium",
gaps: [
{
id: "GAP-HW-001",
title: "GPIO Pin Assignment Not Documented",
description: "Multiple interfaces but no pin mapping provided.",
title: "I2C Bus Sharing Strategy",
description: "Multiple I2C devices defined but bus arbitration/sharing strategy not documented.",
questions: [
"Complete GPIO pin assignment table?",
"Reserved/strapping pins identified?",
"I2C pull-up resistor values?"
"Single I2C bus or multiple buses?",
"I2C mutex for concurrent access?",
"Clock stretching handling?"
],
recommendation: "Create comprehensive GPIO map. Avoid strapping pins (GPIO0, GPIO45, GPIO46).",
esp32Impact: "ESP32-S3 has 45 GPIOs but some have restrictions (strapping, flash, PSRAM)."
recommendation: "Use I2C_NUM_0 for sensors, I2C_NUM_1 for OLED. Mutex per bus for thread safety.",
esp32Impact: "ESP32-S3 has 2x I2C controllers. Separate buses recommended for isolation."
},
{
id: "GAP-HW-002",
title: "OLED Display Specifications Missing",
description: "OLED via I2C mentioned but no specifications.",
title: "SEN6x 5V Supply Management",
description: "SEN6x requires 5V but ESP32-S3 is 3.3V. Level shifting/power supply not addressed.",
questions: [
"Display resolution?",
"Controller chip (SSD1306, SH1106)?",
"I2C address?",
"Refresh rate requirements?"
"5V supply source (USB VBUS, regulator)?",
"Level shifter for I2C lines?",
"Power sequencing with 3.3V devices?"
],
recommendation: "Recommend SSD1306 128x64 OLED at 0x3C, widely supported.",
esp32Impact: "I2C bus speed consideration - 400kHz recommended for smooth updates."
recommendation: "Use VBUS (5V) for SEN6x. SEN6x I2C is 3.3V compatible (open-drain).",
esp32Impact: "No level shifter needed for I2C. Ensure stable 5V supply."
},
{
id: "GAP-HW-003",
title: "SD Card Interface Mode Unspecified",
description: "SD card mentioned but interface mode not defined.",
title: "ENS160 1.8V Core Supply",
description: "ENS160 requires 1.8V core voltage. Supply configuration not specified.",
questions: [
"SPI mode or SDMMC (1-bit/4-bit)?",
"Maximum SD card size supported?",
"Speed class requirements?"
"External LDO or module-integrated?",
"LDO enable sequencing?",
"Current capability for ENS160?"
],
recommendation: "Use SDMMC 4-bit mode for best performance if GPIOs available, else SPI.",
esp32Impact: "ESP32-S3 supports both. SDMMC uses dedicated pins, SPI is more flexible."
recommendation: "Use ENS160 module with integrated 1.8V LDO. Verify module specifications.",
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: [
{
id: "GAP-DQC-001",
title: "Calibration Data Format Undefined",
description: "Calibration management mentioned but data structure not specified.",
title: "Calibration Coefficient Format",
description: "Calibration management defined but coefficient storage format not specified.",
questions: [
"What calibration parameters per sensor type?",
"Polynomial coefficients, offset/gain, or lookup tables?",
"Calibration data versioning?"
"Polynomial order for calibration curves?",
"Fixed-point or floating-point coefficients?",
"Per-unit or batch calibration?"
],
recommendation: "Define per-sensor calibration structure with version, timestamp, and polynomial coefficients.",
esp32Impact: "No specific hardware constraint."
recommendation: "3rd order polynomial, 32-bit float coefficients. Per-unit calibration stored in NVS.",
esp32Impact: "ESP32-S3 FPU supports efficient float operations."
},
{
id: "GAP-DQC-002",
title: "Sensor Validity Thresholds Not Defined",
description: "Out-of-range detection mentioned but thresholds not specified.",
title: "Sensor Auto-Detection Mechanism",
description: "Sensor identification mentioned but auto-detection protocol not defined.",
questions: [
"Valid range per sensor type?",
"Rate-of-change limits?",
"Stuck sensor detection criteria?"
"I2C address scanning at boot?",
"WHO_AM_I register verification?",
"Fallback if sensor not detected?"
],
recommendation: "Define per-sensor-type: min/max physical range, max rate of change, stuck duration.",
esp32Impact: "No specific hardware constraint."
recommendation: "I2C scan + WHO_AM_I verification. Log missing sensors, continue with available.",
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() {
const { data, loading, lastUpdated, refresh, typeCounts, groupedByType, parseLog, setData } =
const { data, loading, lastUpdated, refresh, reloadFromCSV, typeCounts, groupedByType, parseLog, setData } =
useTraceabilityData();
const [showDebug, setShowDebug] = useState(false);
const [showUpload, setShowUpload] = useState(false);
@@ -156,7 +156,7 @@ export default function Dashboard() {
<RefreshCw className="h-4 w-4 mr-2" />
Update Data
</Button>
<Button variant="outline" onClick={refresh} disabled={loading}>
<Button variant="outline" onClick={reloadFromCSV} disabled={loading}>
Reload from CSV
</Button>
</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