update page
This commit is contained in:
@@ -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={
|
||||
|
||||
@@ -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
|
||||
});
|
||||
} 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 () => {
|
||||
setIsLoading(true);
|
||||
setLogs([]);
|
||||
setErrors([]);
|
||||
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
|
||||
});
|
||||
}
|
||||
} 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}>
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
323
src/lib/traceabilityUtils.ts
Normal file
323
src/lib/traceabilityUtils.ts
Normal 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;
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
659
src/pages/SelectedSensorsPage.tsx
Normal file
659
src/pages/SelectedSensorsPage.tsx
Normal 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.2kΩ/4.7kΩ 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.7kΩ 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.2kΩ 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
Reference in New Issue
Block a user