import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; import { buildMetadataFromEnv } from "../build-metadata.mjs"; export const DEFAULT_PROJECT_ID = "prj_mvp_topology"; export const DEFAULT_GATEWAY_SESSION_ID = "gws_mvp_simu"; export const SIM_PORT_DEFINITIONS = Object.freeze([ { port: "AI1", family: "AI", direction: "input", valueType: "number", unit: "V", initialValue: 0 }, { port: "AO1", family: "AO", direction: "output", valueType: "number", unit: "V", initialValue: 0 }, { port: "DI1", family: "DI", direction: "input", valueType: "boolean", initialValue: false }, { port: "DO1", family: "DO", direction: "output", valueType: "boolean", initialValue: false }, { port: "FREQ1", family: "FREQ", direction: "bidirectional", valueType: "number", unit: "Hz", initialValue: 0 }, { port: "FREQ_IN1", family: "FREQ_IN", direction: "input", valueType: "number", unit: "Hz", initialValue: 0 }, { port: "FREQ_OUT1", family: "FREQ_OUT", direction: "output", valueType: "number", unit: "Hz", initialValue: 0 }, { port: "uart0", family: "SERIAL", direction: "bidirectional", valueType: "string", initialValue: null, legacy: true }, { port: "eth0", family: "NETWORK", direction: "bidirectional", valueType: "object", initialValue: null, legacy: true }, { port: "gpio0", family: "GPIO", direction: "bidirectional", valueType: "boolean", initialValue: null, legacy: true } ]); export const SIM_PORTS = Object.freeze(SIM_PORT_DEFINITIONS.map((definition) => definition.port)); export const L2_PORT_FAMILIES = Object.freeze(["AI", "AO", "DI", "DO", "FREQ", "FREQ_IN", "FREQ_OUT"]); function isoNow() { return new Date().toISOString(); } export function selectInstanceValue( value, fallback, { index = process.env.HWLAB_INSTANCE_INDEX, hostname = process.env.HOSTNAME, requireIndexForMultiple = false, label = "instance value" } = {} ) { const items = String(value ?? "") .split(",") .map((item) => item.trim()) .filter(Boolean); if (items.length === 0) { return fallback; } if (items.length === 1) { return items[0]; } const parsedIndex = Number.parseInt(index ?? "", 10); if (Number.isInteger(parsedIndex) && parsedIndex >= 0 && parsedIndex < items.length) { return items[parsedIndex]; } const ordinal = Number.parseInt(String(hostname ?? "").match(/-(\d+)$/)?.[1] ?? "", 10); if (Number.isInteger(ordinal) && ordinal >= 0 && ordinal < items.length) { return items[ordinal]; } if (requireIndexForMultiple) { throw new Error( `${label} declares ${items.length} indexed values, but neither HWLAB_INSTANCE_INDEX nor a StatefulSet ordinal HOSTNAME selected one` ); } return items[0]; } export function stableIdPart(value) { const normalized = String(value ?? "") .trim() .replace(/[^A-Za-z0-9._:-]+/g, "_") .replace(/^_+|_+$/g, ""); return normalized || "sim"; } export function resourceIdForBox(boxId) { return `res_${stableIdPart(boxId)}`; } export function createBoxResource({ boxId, gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID, ports = SIM_PORTS, portFamilies = L2_PORT_FAMILIES, now = isoNow() }) { return { resourceId: resourceIdForBox(boxId), projectId: DEFAULT_PROJECT_ID, gatewaySessionId, boxId, resourceType: "simulator_endpoint", name: `${boxId} simulator endpoint`, state: "available", environment: ENVIRONMENT_DEV, metadata: { serviceId: "hwlab-box-simu", ports, ...(portFamilies.length > 0 ? { portFamilies } : {}), propagation: "patch-panel-only" }, createdAt: now, updatedAt: now }; } export function createBoxCapabilities({ boxId, resourceId = resourceIdForBox(boxId), projectId = DEFAULT_PROJECT_ID, portDefinitions = SIM_PORT_DEFINITIONS, now = isoNow() }) { const capabilities = []; const boxPart = stableIdPart(boxId); for (const definition of portDefinitions) { if (definition.legacy) { continue; } const portPart = definition.port.toLowerCase(); const family = definition.family.toLowerCase(); capabilities.push({ capabilityId: `cap_${boxPart}_${portPart}_read`, resourceId, projectId, name: `${family}.read`, description: `Read ${definition.port} on ${boxId}.`, direction: "input", valueType: definition.valueType, ...(definition.unit ? { unit: definition.unit } : {}), constraints: { port: definition.port, portFamily: definition.family, internalPortApi: true, patchPanelOnlyPropagation: true }, mutatesState: false, createdAt: now, updatedAt: now }); if (definition.direction === "output" || definition.direction === "bidirectional") { capabilities.push({ capabilityId: `cap_${boxPart}_${portPart}_write`, resourceId, projectId, name: `${family}.write`, description: `Write ${definition.port} on ${boxId}.`, direction: definition.direction === "bidirectional" ? "bidirectional" : "output", valueType: definition.valueType, ...(definition.unit ? { unit: definition.unit } : {}), constraints: { port: definition.port, portFamily: definition.family, internalPortApi: false, patchPanelOnlyPropagation: true }, mutatesState: true, createdAt: now, updatedAt: now }); } } return capabilities; } export function getPortDefinition(port) { return SIM_PORT_DEFINITIONS.find((definition) => definition.port === port); } export function createGatewaySession({ gatewayId, serviceId = "hwlab-gateway-simu", endpoint, now = isoNow() }) { return { gatewaySessionId: `gws_${gatewayId}`, projectId: DEFAULT_PROJECT_ID, serviceId, gatewayId, endpoint, status: "connected", environment: ENVIRONMENT_DEV, startedAt: now, lastSeenAt: now, labels: { simulator: serviceId === "hwlab-gateway-simu" ? "true" : "false" } }; } export function createBoxState({ boxId, gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID, ports: portNames = SIM_PORTS, includePortDefinitions = true, now = isoNow() }) { const selectedDefinitions = portNames .map((port) => getPortDefinition(port) ?? { port, family: port, direction: "bidirectional", valueType: "object", initialValue: null, legacy: !includePortDefinitions }); const ports = Object.fromEntries( selectedDefinitions.map((definition) => [ definition.port, { port: definition.port, ...(includePortDefinitions ? { family: definition.family, direction: definition.direction, valueType: definition.valueType, ...(definition.unit ? { unit: definition.unit } : {}), internalPortApi: true } : {}), value: definition.initialValue, source: "local", updatedAt: now } ]) ); const portFamilies = includePortDefinitions ? [...new Set(selectedDefinitions.map((definition) => definition.family))] : []; const resource = createBoxResource({ boxId, gatewaySessionId, ports: portNames, portFamilies, now }); return { serviceId: "hwlab-box-simu", boxId, projectId: DEFAULT_PROJECT_ID, ...(includePortDefinitions ? { environment: ENVIRONMENT_DEV } : {}), gatewaySessionId, live: true, resource, ...(includePortDefinitions ? { capabilities: createBoxCapabilities({ boxId, resourceId: resource.resourceId, projectId: DEFAULT_PROJECT_ID, portDefinitions: selectedDefinitions, now }) } : {}), ports, constraints: { crossDevicePropagation: "patch-panel-only", localLoopbackEnabled: false }, updatedAt: now }; } export function createGatewayState({ gatewayId, serviceId = "hwlab-gateway-simu", endpoint, boxes = [], now = isoNow() }) { return { serviceId, gatewayId, projectId: DEFAULT_PROJECT_ID, live: true, session: createGatewaySession({ gatewayId, serviceId, endpoint, now }), boxes, updatedAt: now }; } export function createHealth({ serviceId, name, now = isoNow() }) { const metadata = buildMetadataFromEnv(process.env, { serviceId }); return { serviceId, name, environment: ENVIRONMENT_DEV, live: true, status: "ok", revision: metadata.revision, commit: metadata.commit, image: metadata.image, build: metadata.build, observedAt: now }; }