Files
pikasTech-HWLAB/internal/sim/model.mjs
T
2026-05-23 04:30:10 +00:00

367 lines
8.5 KiB
JavaScript

import { ENVIRONMENT_DEV } from "../protocol/index.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() }) {
return {
serviceId,
name,
environment: ENVIRONMENT_DEV,
live: true,
observedAt: now
};
}