velxio/frontend/src/services/ComponentRegistry.ts

306 lines
12 KiB
TypeScript
Raw Normal View History

/**
* Component Registry
*
* Singleton service that loads and provides access to component metadata.
* Loads from components-metadata.json generated at build time.
*/
import type {
ComponentMetadata,
ComponentCategory,
ComponentMetadataCollection,
} from '../types/component-metadata';
export class ComponentRegistry {
private static instance: ComponentRegistry;
private metadata: Map<string, ComponentMetadata> = new Map();
private categories: Map<ComponentCategory, ComponentMetadata[]> = new Map();
private allComponents: ComponentMetadata[] = [];
private loaded = false;
private _loadPromise: Promise<void> | null = null;
private constructor() {}
/**
* Get singleton instance
*/
static getInstance(): ComponentRegistry {
if (!ComponentRegistry.instance) {
ComponentRegistry.instance = new ComponentRegistry();
}
return ComponentRegistry.instance;
}
/**
* Load metadata from JSON file
*/
async load(): Promise<void> {
if (this.loaded) return;
if (this._loadPromise) return this._loadPromise;
this._loadPromise = this._doLoad();
return this._loadPromise;
}
/**
* Returns the load promise so consumers can await registry readiness
*/
get loadPromise(): Promise<void> {
return this._loadPromise ?? this.load();
}
get isLoaded(): boolean {
return this.loaded;
}
private async _doLoad(): Promise<void> {
try {
// `cache: 'no-store'` so adding a new component (or rebuilding the JSON)
// shows up after a single page refresh — without this, the browser keeps
// serving the stale copy until you do a hard reload.
const response = await fetch('/components-metadata.json', { cache: 'no-store' });
if (!response.ok) {
throw new Error(`Failed to load metadata: ${response.statusText}`);
}
const data: ComponentMetadataCollection = await response.json();
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
// Inject Raspberry Pi 3 / 4 / 5 metadata. All three share the
// same 40-pin GPIO header; the simulator backend picks a
// different QEMU CPU model per board (Cortex-A53/A72/A76).
data.components.push({
id: 'raspberry-pi-3',
tagName: 'velxio-raspberry-pi-3',
name: 'Raspberry Pi 3',
category: 'boards',
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
description: 'Raspberry Pi 3 Model B with 40-pin GPIO. QEMU virt + Cortex-A53 backend.',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#E60049" rx="4"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="10" fill="#FFF">RPi3</text></svg>',
properties: [],
defaultValues: {},
pinCount: 40,
tags: ['raspberry', 'pi', 'rp3', 'board', 'qemu', 'linux'],
});
feat(pi3 phase 3.1+3.2): Pi 3/4/5 family via PI_CONFIGS Backend: extract per-board config into a PI_CONFIGS dict keyed by board_type. Pi 3/4/5 share the same arm64 image set (kernel + initramfs + rootfs) and differ only in QEMU -cpu and -m: raspberry-pi-3 → cortex-a53 + 1G (BCM2837, ARMv8 64-bit) raspberry-pi-4 → cortex-a72 + 2G (BCM2711, ARMv8 64-bit) raspberry-pi-5 → cortex-a76 + 2G (BCM2712, ARMv8 64-bit) PiInstance now carries board_type so the per-board lookup happens once at start_instance time. Unknown board_type falls back to DEFAULT_PI_BOARD ('raspberry-pi-3') instead of erroring out (for back-compat with older clients). Pre-warm hook walks every unique image_set in PI_CONFIGS so the provider only downloads each set once even when several Pi models are registered. Frontend: - BoardKind union gains 'raspberry-pi-4' and 'raspberry-pi-5'. - BOARD_KIND_LABELS + BOARD_KIND_FQBN entries for both new boards (FQBN null since they use the Pi VFS + Python toolchain like Pi 3). - ComponentRegistry inserts two new component metadata entries cloning the Pi 3 board art with different thumbnail colours. Tag name reused so the same velxio-raspberry-pi-3 web element draws the board on the canvas — the 40-pin GPIO layout is identical across Pi 3/4/5. - boardProtocols.ts: Pi 3/4/5 share the BCM physical→GPIO table (PI3_BCM) since the 40-pin header layout is identical. - loadExample.ts: where 'raspberry-pi-3' is special-cased (VFS ingest, .cpp vs .ino filename), now matches Pi 3/4/5 alike. - Interconnect.isPi3Bridge() recognises all three Pi family members so Arduino↔Pi serial routing keeps working. - RaspberryPi3Bridge constructor gained a boardKind parameter defaulting to 'raspberry-pi-3'. The WebSocket 'start_pi' message now ships the actual board kind so the backend knows which PI_CONFIGS entry to use. - useSimulatorStore.addBoard wires bridge construction for all three Pi family members. Pi Zero/Pi 1/Pi 2 (armhf) come in Phase 3.3 — separate kernel package + armhf rootfs build, no change here. Smoke-tested inside the prod container: Pi 4 (cortex-a72) → reached agetty login on hvc0 Pi 5 (cortex-a76) → reached agetty login on hvc0 Both show 'aarch64' in uname -m.
2026-05-18 20:41:18 +07:00
data.components.push({
id: 'raspberry-pi-4',
tagName: 'velxio-raspberry-pi-3', // reuse Pi 3 board art (40-pin layout identical)
name: 'Raspberry Pi 4',
category: 'boards',
description: 'Raspberry Pi 4 Model B with 40-pin GPIO. QEMU virt + Cortex-A72 backend.',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#83B81A" rx="4"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="10" fill="#FFF">RPi4</text></svg>',
properties: [],
defaultValues: {},
pinCount: 40,
tags: ['raspberry', 'pi', 'rp4', 'board', 'qemu', 'linux'],
});
data.components.push({
id: 'raspberry-pi-5',
tagName: 'velxio-raspberry-pi-3', // reuse art for now (Phase 3 polish: Pi 5 PCB SVG)
name: 'Raspberry Pi 5',
category: 'boards',
description: 'Raspberry Pi 5 with 40-pin GPIO. QEMU virt + Cortex-A76 backend (no raspi5 machine in QEMU yet).',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" fill="#76323F" rx="4"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="10" fill="#FFF">RPi5</text></svg>',
properties: [],
defaultValues: {},
pinCount: 40,
tags: ['raspberry', 'pi', 'rp5', 'board', 'qemu', 'linux'],
});
// Inject SPICE probe instruments — these are Velxio-specific React
// components (not wokwi web elements), so they have no auto-generated
// metadata but still need a registry entry so the picker can offer
// them and the canvas can resolve them by id.
data.components.push({
id: 'instr-voltmeter',
tagName: 'velxio-instr-voltmeter',
name: 'Voltmeter',
category: 'analog',
description:
'SPICE probe — displays the voltage between V+ and V-. Used in electrical-mode circuits.',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="6" fill="#1f1f1f" stroke="#ffa500" stroke-width="2"/><text x="50%" y="42%" text-anchor="middle" font-family="monospace" font-size="9" fill="#ffa500">V METER</text><text x="50%" y="68%" text-anchor="middle" font-family="monospace" font-size="11" fill="#ffa500" font-weight="bold">3.30 V</text></svg>',
properties: [],
defaultValues: {},
pinCount: 2,
tags: ['voltmeter', 'meter', 'probe', 'instrument', 'spice', 'multimeter', 'dmm'],
});
data.components.push({
id: 'instr-ammeter',
tagName: 'velxio-instr-ammeter',
name: 'Ammeter',
category: 'analog',
description:
'SPICE probe — measures the current through its body (connect in series). Used in electrical-mode circuits.',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect width="64" height="64" rx="6" fill="#1f1f1f" stroke="#4dd0e1" stroke-width="2"/><text x="50%" y="42%" text-anchor="middle" font-family="monospace" font-size="9" fill="#4dd0e1">A METER</text><text x="50%" y="68%" text-anchor="middle" font-family="monospace" font-size="11" fill="#4dd0e1" font-weight="bold">12.4 mA</text></svg>',
properties: [],
defaultValues: {},
pinCount: 2,
tags: ['ammeter', 'meter', 'probe', 'instrument', 'spice', 'current', 'multimeter', 'dmm'],
});
// Custom Chip — user-supplied WASM compiled from C. Pin layout is
// dynamic (read from the per-instance chip.json properties), so
// pinCount=0 is just a placeholder for the picker grid.
data.components.push({
id: 'custom-chip',
tagName: 'velxio-custom-chip',
name: 'Custom Chip',
category: 'logic',
description:
'Write your own chip in C and compile to WebAssembly. Includes a gallery of examples (EEPROM, RTC, shift register, ADC, UART, …).',
thumbnail:
'<svg width="64" height="64" xmlns="http://www.w3.org/2000/svg"><rect x="6" y="14" width="52" height="36" rx="3" fill="#1a1a1a" stroke="#888" stroke-width="2"/><rect x="2" y="20" width="6" height="3" fill="#c0c0c0"/><rect x="2" y="28" width="6" height="3" fill="#c0c0c0"/><rect x="2" y="36" width="6" height="3" fill="#c0c0c0"/><rect x="2" y="44" width="6" height="3" fill="#c0c0c0"/><rect x="56" y="20" width="6" height="3" fill="#c0c0c0"/><rect x="56" y="28" width="6" height="3" fill="#c0c0c0"/><rect x="56" y="36" width="6" height="3" fill="#c0c0c0"/><rect x="56" y="44" width="6" height="3" fill="#c0c0c0"/><text x="32" y="36" text-anchor="middle" font-family="monospace" font-size="9" font-weight="bold" fill="#e0e0e0">CHIP</text></svg>',
properties: [
{ name: 'chipName', type: 'string', defaultValue: 'My Chip' },
{ name: 'sourceC', type: 'string', defaultValue: '' },
{ name: 'chipJson', type: 'string', defaultValue: '{"name":"My Chip","pins":["IN","OUT","GND","VCC"]}' },
{ name: 'wasmBase64', type: 'string', defaultValue: '' },
],
defaultValues: {
chipName: 'My Chip',
sourceC: '',
chipJson: '{"name":"My Chip","pins":["IN","OUT","GND","VCC"]}',
wasmBase64: '',
},
pinCount: 0,
tags: ['custom', 'chip', 'wasm', 'c', 'wokwi', 'eeprom', 'rtc', 'logic'],
});
this.processMetadata(data.components);
this.loaded = true;
console.log(`Loaded ${this.allComponents.length} components from metadata`);
} catch (error) {
console.error('Failed to load component metadata:', error);
// Continue with empty registry - app should still work with manual component addition
}
}
/**
* Process and index metadata
*/
private processMetadata(components: ComponentMetadata[]): void {
this.allComponents = components;
this.metadata.clear();
this.categories.clear();
// Index by ID
components.forEach((component) => {
this.metadata.set(component.id, component);
// Group by category
const categoryComponents = this.categories.get(component.category) || [];
categoryComponents.push(component);
this.categories.set(component.category, categoryComponents);
});
}
/**
* Get all components
*/
getAllComponents(): ComponentMetadata[] {
return [...this.allComponents];
}
/**
* Merge additional components into the registry from an external source.
*
* Used by private overlays (e.g. the velxio.dev pro overlay) to add
* premium components after the default `/components-metadata.json` has
* loaded. Components with an existing `id` are replaced; new ones are
* appended. Categories and search index are rebuilt.
*/
mergeComponents(extras: ComponentMetadata[]): void {
if (!extras || extras.length === 0) return;
const byId = new Map(this.allComponents.map((c) => [c.id, c]));
for (const extra of extras) {
byId.set(extra.id, extra);
}
this.processMetadata(Array.from(byId.values()));
}
/**
* Get components by category
*/
getByCategory(category: ComponentCategory): ComponentMetadata[] {
return this.categories.get(category) || [];
}
/**
* Get component by ID
*/
getById(id: string): ComponentMetadata | undefined {
return this.metadata.get(id);
}
/**
* Search components by query (name, description, tags)
*/
search(query: string): ComponentMetadata[] {
if (!query.trim()) {
return this.getAllComponents();
}
const lowerQuery = query.toLowerCase();
return this.allComponents.filter((component) => {
return (
component.name.toLowerCase().includes(lowerQuery) ||
component.id.toLowerCase().includes(lowerQuery) ||
component.description?.toLowerCase().includes(lowerQuery) ||
component.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
);
});
}
/**
* Get all available categories
*/
getCategories(): ComponentCategory[] {
return Array.from(this.categories.keys());
}
/**
* Reload metadata (for hot-reload in dev mode)
*/
async reload(): Promise<void> {
this.loaded = false;
await this.load();
}
/**
* Get component count
*/
getComponentCount(): number {
return this.allComponents.length;
}
/**
* Get category display name
*/
static getCategoryDisplayName(category: ComponentCategory): string {
const displayNames: Record<ComponentCategory, string> = {
boards: 'Boards',
sensors: 'Sensors',
displays: 'Displays',
input: 'Input',
output: 'Output',
motors: 'Motors',
communication: 'Communication',
passive: 'Passive',
feat: expand SPICE component catalog (fases 9 + 10) Adds 44 SPICE mappers, 58 custom metadata entries, and 12 visual Web Components covering logic gates, transistors, op-amps, regulators, sources, electromechanical parts and integrated-circuit packaging. Fase 9 — component catalog expansion ------------------------------------ - 7 logic gates (AND/OR/NAND/NOR/XOR/XNOR + NOT) as SPICE B-sources - 8 multi-input gates (AND/OR/NAND/NOR with 3 and 4 inputs) - 9 transistors: 5 BJTs (incl. PNP 2N3906/BC557) + 4 MOSFETs (incl. P-channel IRF9540/FQP27P06). NMOS refactored from Level=3 W=0.1 (hangs ngspice) to Level=1 with sane W/L - 5 op-amps: LM358, LM741, TL072, LM324 with per-chip saturation rails + opamp-ideal - 4 linear regulators (7805, 7812, 7905, LM317) with dropout - 3 batteries (9V, AA, coin-cell) with realistic ESR - Signal generator (sine / square / DC) - 2 Schottky diodes (1N5817, 1N5819) + photodiode (lux-driven current source) Fase 10 — electromechanical + ICs --------------------------------- - Relay (SPDT): coil + L + S-switch with native hysteresis + flyback diode, inverted-control trick for the NC contact - Optocouplers 4N25 and PC817 (LED + CCCS with CTR=0.5 / 1.0) - 7 74HC ICs as DIP-14 packages emitting 4 or 6 B-sources per component (first mapper pattern emitting multiple device cards) - 3 flip-flops (D, T, JK) — digital-sim only (edge detection is not representable in ngspice .op) - L293D dual H-bridge motor driver Infrastructure -------------- - scripts/component-overrides.json gains a _customComponents[] array that lets new Velxio-only parts survive metadata regeneration (previously applyOverrides() could only patch wokwi-elements components that had already been scanned) - scripts/generate-component-metadata.ts injects custom entries before the patch loop - New ComponentCategory values: 'logic', 'analog', 'electromech' - frontend/src/components/DynamicComponent.tsx PASSIVE tracing extended from just ['resistor','resistor-us'] to 9 two-terminal passives with per-part pin name maps - New CI workflow test-circuit.yml runs the sandbox on push/PR - frontend-tests.yml regenerates metadata and fails if committed JSON is stale - Documented 2 new ngspice gotchas in circuit-emulation-gotchas.md: unicode in netlist titles silently hangs the parser, and MOSFET Level=3 + W=0.1m causes .op to hang - 164/164 sandbox tests passing in ~9 s (was 88 pre-fase-9) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 06:44:18 +07:00
logic: 'Logic Gates',
analog: 'Analog',
electromech: 'Electromechanical',
other: 'Other',
};
return displayNames[category] || category;
}
}
// Auto-load on module import
const registry = ComponentRegistry.getInstance();
registry.load();
export default registry;