feat: add l2 l3 simulator patch panel skeleton

This commit is contained in:
HWLAB Code Queue
2026-05-21 14:40:12 +00:00
parent b1d31380cb
commit 77f70ff4ad
11 changed files with 757 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env node
import { createHealth, createBoxState } from "../../internal/sim/model.mjs";
import { createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
const boxId = process.env.HWLAB_BOX_ID ?? "boxsim_alpha";
const port = parsePort(process.env.PORT, 7201);
const state = createBoxState({ boxId });
const routes = new Map();
routes.set("GET /health/live", () => createHealth({ serviceId: "hwlab-box-simu", name: boxId }));
routes.set("GET /status", () => state);
routes.set("POST /ports/write", async ({ req }) => {
const body = await readJson(req);
if (!state.ports[body.port]) {
throw new Error(`unknown port ${body.port}`);
}
const now = new Date().toISOString();
state.ports[body.port] = {
port: body.port,
value: body.value ?? null,
source: body.source ?? "local",
updatedAt: now
};
state.updatedAt = now;
return {
accepted: true,
boxId,
port: body.port,
crossDevicePropagation: "patch-panel-only",
localLoopbackEnabled: false
};
});
listen(createJsonServer({ routes }), port);
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env node
import { createGatewayState, createHealth } from "../../internal/sim/model.mjs";
import { createJsonServer, listen, parsePort } from "../../internal/sim/http.mjs";
const gatewayId = process.env.HWLAB_GATEWAY_ID ?? "gwsimu_east";
const port = parsePort(process.env.PORT, 7101);
const endpoint = `http://127.0.0.1:${port}`;
const boxes = (process.env.HWLAB_BOX_RESOURCES ?? "res_boxsim_alpha")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
const state = createGatewayState({ gatewayId, endpoint, boxes });
const routes = new Map();
routes.set("GET /health/live", () =>
createHealth({ serviceId: "hwlab-gateway-simu", name: gatewayId })
);
routes.set("GET /status", () => state);
routes.set("GET /boxes", () => ({
gatewayId,
boxes: state.boxes
}));
listen(createJsonServer({ routes }), port);
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env node
import { createGatewayState, createHealth } from "../../internal/sim/model.mjs";
import { createJsonServer, listen, parsePort } from "../../internal/sim/http.mjs";
const gatewayId = process.env.HWLAB_GATEWAY_ID ?? "gateway_dev_stub";
const port = parsePort(process.env.PORT, 7001);
const state = createGatewayState({
gatewayId,
serviceId: "hwlab-gateway",
endpoint: `http://127.0.0.1:${port}`,
boxes: []
});
state.session.status = "starting";
const routes = new Map();
routes.set("GET /health/live", () => createHealth({ serviceId: "hwlab-gateway", name: gatewayId }));
routes.set("GET /status", () => ({
...state,
note: "L2/L3 skeleton only; real hardware integration is intentionally absent for MVP simulator work."
}));
listen(createJsonServer({ routes }), port);
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env node
import { createMvpTopology } from "../../internal/patchpanel/fixture.mjs";
import { createPatchPanel } from "../../internal/patchpanel/model.mjs";
import { createHealth } from "../../internal/sim/model.mjs";
import { createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
const port = parsePort(process.env.PORT, 7301);
const topology = createMvpTopology();
const patchPanel = createPatchPanel({ wiringConfig: topology.wiringConfig });
const routes = new Map();
routes.set("GET /health/live", () =>
createHealth({ serviceId: "hwlab-patch-panel", name: "hwlab-patch-panel" })
);
routes.set("GET /status", () => patchPanel.status());
routes.set("GET /wiring", () => patchPanel.wiringConfig);
routes.set("POST /signals/route", async ({ req }) => patchPanel.routeSignal(await readJson(req)));
listen(createJsonServer({ routes }), port);
+9
View File
@@ -0,0 +1,9 @@
# MVP Simulated Topology
`topology.json` is the L2/L3 dry-run fixture for the simulator skeleton. It
describes two `hwlab-box-simu` instances, two `hwlab-gateway-simu` instances,
and one `hwlab-patch-panel`.
Cross-device propagation is represented only through the patch panel wiring
config. Box simulators keep local port state but do not provide loopback as a
cross-device synchronization substitute.
+226
View File
@@ -0,0 +1,226 @@
{
"topologyId": "top_mvp_l2_l3_sim_patch",
"projectId": "prj_mvp_topology",
"environment": "dev",
"services": {
"boxes": [
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_alpha",
"projectId": "prj_mvp_topology",
"gatewaySessionId": "gws_gwsimu_east",
"live": true,
"resource": {
"resourceId": "res_boxsim_alpha",
"projectId": "prj_mvp_topology",
"gatewaySessionId": "gws_gwsimu_east",
"boxId": "boxsim_alpha",
"resourceType": "simulator_endpoint",
"name": "boxsim_alpha simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["uart0", "eth0", "gpio0"],
"propagation": "patch-panel-only"
},
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"ports": {
"uart0": {
"port": "uart0",
"value": null,
"source": "local",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"eth0": {
"port": "eth0",
"value": null,
"source": "local",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"gpio0": {
"port": "gpio0",
"value": null,
"source": "local",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
},
"updatedAt": "2026-01-01T00:00:00.000Z"
},
{
"serviceId": "hwlab-box-simu",
"boxId": "boxsim_beta",
"projectId": "prj_mvp_topology",
"gatewaySessionId": "gws_gwsimu_west",
"live": true,
"resource": {
"resourceId": "res_boxsim_beta",
"projectId": "prj_mvp_topology",
"gatewaySessionId": "gws_gwsimu_west",
"boxId": "boxsim_beta",
"resourceType": "simulator_endpoint",
"name": "boxsim_beta simulator endpoint",
"state": "available",
"environment": "dev",
"metadata": {
"serviceId": "hwlab-box-simu",
"ports": ["uart0", "eth0", "gpio0"],
"propagation": "patch-panel-only"
},
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"ports": {
"uart0": {
"port": "uart0",
"value": null,
"source": "local",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"eth0": {
"port": "eth0",
"value": null,
"source": "local",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"gpio0": {
"port": "gpio0",
"value": null,
"source": "local",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
},
"constraints": {
"crossDevicePropagation": "patch-panel-only",
"localLoopbackEnabled": false
},
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"gateways": [
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_east",
"projectId": "prj_mvp_topology",
"live": true,
"session": {
"gatewaySessionId": "gws_gwsimu_east",
"projectId": "prj_mvp_topology",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_east",
"endpoint": "http://127.0.0.1:7101",
"status": "connected",
"environment": "dev",
"startedAt": "2026-01-01T00:00:00.000Z",
"lastSeenAt": "2026-01-01T00:00:00.000Z",
"labels": {
"simulator": "true"
}
},
"boxes": ["res_boxsim_alpha"],
"updatedAt": "2026-01-01T00:00:00.000Z"
},
{
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_west",
"projectId": "prj_mvp_topology",
"live": true,
"session": {
"gatewaySessionId": "gws_gwsimu_west",
"projectId": "prj_mvp_topology",
"serviceId": "hwlab-gateway-simu",
"gatewayId": "gwsimu_west",
"endpoint": "http://127.0.0.1:7102",
"status": "connected",
"environment": "dev",
"startedAt": "2026-01-01T00:00:00.000Z",
"lastSeenAt": "2026-01-01T00:00:00.000Z",
"labels": {
"simulator": "true"
}
},
"boxes": ["res_boxsim_beta"],
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"patchPanel": {
"patchPanelStatusId": "pps_mvp_patch_panel",
"projectId": "prj_mvp_topology",
"gatewaySessionId": "gws_gwsimu_east",
"wiringConfigId": "wir_mvp_patch_panel",
"serviceId": "hwlab-patch-panel",
"state": "active",
"environment": "dev",
"activeConnections": [
{
"fromResourceId": "res_boxsim_alpha",
"fromPort": "uart0",
"toResourceId": "res_boxsim_beta",
"toPort": "uart0"
},
{
"fromResourceId": "res_boxsim_alpha",
"fromPort": "gpio0",
"toResourceId": "res_boxsim_beta",
"toPort": "gpio0"
}
],
"observedAt": "2026-01-01T00:00:00.000Z",
"metadata": {
"propagation": "patch-panel-only",
"connectionCount": 2
}
}
},
"wiringConfig": {
"wiringConfigId": "wir_mvp_patch_panel",
"projectId": "prj_mvp_topology",
"gatewaySessionId": "gws_gwsimu_east",
"name": "MVP simulated patch panel wiring",
"status": "active",
"connections": [
{
"from": {
"resourceId": "res_boxsim_alpha",
"port": "uart0"
},
"to": {
"resourceId": "res_boxsim_beta",
"port": "uart0"
},
"mode": "exclusive"
},
{
"from": {
"resourceId": "res_boxsim_alpha",
"port": "gpio0"
},
"to": {
"resourceId": "res_boxsim_beta",
"port": "gpio0"
},
"mode": "monitor"
}
],
"constraints": {
"propagation": "patch-panel-only",
"localLoopbackSubstituteAllowed": false
},
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"constraints": {
"patchPanelIsOnlyCrossDeviceChannel": true,
"boxSimuLocalLoopbackForCrossDeviceSync": false
},
"dryRunHints": {
"cliTarget": "fixtures/mvp-topology/topology.json",
"expectedServices": ["hwlab-box-simu", "hwlab-gateway-simu", "hwlab-patch-panel"]
}
}
+80
View File
@@ -0,0 +1,80 @@
import { createBoxState, createGatewayState } from "../sim/model.mjs";
import { createPatchPanelState, createWiringConfig } from "./model.mjs";
export const MVP_BOX_IDS = Object.freeze(["boxsim_alpha", "boxsim_beta"]);
export const MVP_GATEWAY_IDS = Object.freeze(["gwsimu_east", "gwsimu_west"]);
export function createMvpTopology({ now = "2026-01-01T00:00:00.000Z" } = {}) {
const gatewaySessionIds = MVP_GATEWAY_IDS.map((gatewayId) => `gws_${gatewayId}`);
const boxes = MVP_BOX_IDS.map((boxId, index) =>
createBoxState({
boxId,
gatewaySessionId: gatewaySessionIds[index],
now
})
);
const gateways = MVP_GATEWAY_IDS.map((gatewayId, index) =>
createGatewayState({
gatewayId,
endpoint: `http://127.0.0.1:${7101 + index}`,
boxes: [boxes[index].resource.resourceId],
now
})
);
const wiringConfig = createWiringConfig({
gatewaySessionId: gatewaySessionIds[0],
connections: [
{
from: {
resourceId: boxes[0].resource.resourceId,
port: "uart0"
},
to: {
resourceId: boxes[1].resource.resourceId,
port: "uart0"
},
mode: "exclusive"
},
{
from: {
resourceId: boxes[0].resource.resourceId,
port: "gpio0"
},
to: {
resourceId: boxes[1].resource.resourceId,
port: "gpio0"
},
mode: "monitor"
}
],
now
});
const patchPanel = createPatchPanelState({
gatewaySessionId: gatewaySessionIds[0],
wiringConfig,
now
});
return {
topologyId: "top_mvp_l2_l3_sim_patch",
projectId: "prj_mvp_topology",
environment: "dev",
services: {
boxes,
gateways,
patchPanel
},
wiringConfig,
constraints: {
patchPanelIsOnlyCrossDeviceChannel: true,
boxSimuLocalLoopbackForCrossDeviceSync: false
},
dryRunHints: {
cliTarget: "fixtures/mvp-topology/topology.json",
expectedServices: ["hwlab-box-simu", "hwlab-gateway-simu", "hwlab-patch-panel"]
}
};
}
+111
View File
@@ -0,0 +1,111 @@
import { ENVIRONMENT_DEV } from "../protocol/index.mjs";
import { DEFAULT_GATEWAY_SESSION_ID, DEFAULT_PROJECT_ID } from "../sim/model.mjs";
export function createWiringConfig({
wiringConfigId = "wir_mvp_patch_panel",
gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID,
connections,
now = new Date().toISOString()
}) {
if (!Array.isArray(connections) || connections.length === 0) {
throw new Error("wiring config requires at least one connection");
}
return {
wiringConfigId,
projectId: DEFAULT_PROJECT_ID,
gatewaySessionId,
name: "MVP simulated patch panel wiring",
status: "active",
connections,
constraints: {
propagation: "patch-panel-only",
localLoopbackSubstituteAllowed: false
},
createdAt: now,
updatedAt: now
};
}
export function createPatchPanelState({
patchPanelStatusId = "pps_mvp_patch_panel",
gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID,
wiringConfig,
now = new Date().toISOString()
}) {
return {
patchPanelStatusId,
projectId: DEFAULT_PROJECT_ID,
gatewaySessionId,
wiringConfigId: wiringConfig.wiringConfigId,
serviceId: "hwlab-patch-panel",
state: "active",
environment: ENVIRONMENT_DEV,
activeConnections: wiringConfig.connections.map((connection) => ({
fromResourceId: connection.from.resourceId,
fromPort: connection.from.port,
toResourceId: connection.to.resourceId,
toPort: connection.to.port
})),
observedAt: now,
metadata: {
propagation: "patch-panel-only",
connectionCount: wiringConfig.connections.length
}
};
}
export function createPatchPanel({ wiringConfig, now = () => new Date().toISOString() }) {
const signals = new Map();
function routeSignal({ fromResourceId, fromPort, value }) {
const observedAt = now();
const deliveries = [];
for (const connection of wiringConfig.connections) {
const forwardMatch =
connection.from.resourceId === fromResourceId && connection.from.port === fromPort;
const reverseMatch =
connection.to.resourceId === fromResourceId && connection.to.port === fromPort;
if (!forwardMatch && !reverseMatch) {
continue;
}
const target = forwardMatch ? connection.to : connection.from;
const key = `${target.resourceId}:${target.port}`;
const delivery = {
resourceId: target.resourceId,
port: target.port,
value,
sourceResourceId: fromResourceId,
sourcePort: fromPort,
observedAt
};
signals.set(key, delivery);
deliveries.push(delivery);
}
return {
accepted: true,
propagatedBy: "hwlab-patch-panel",
deliveryCount: deliveries.length,
deliveries,
observedAt
};
}
function status() {
return createPatchPanelState({
wiringConfig,
now: now()
});
}
return {
wiringConfig,
routeSignal,
status,
signals
};
}
+52
View File
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { createMvpTopology } from "./fixture.mjs";
import { createPatchPanel } from "./model.mjs";
test("MVP topology includes two box simulators, two gateway simulators, and one patch panel", () => {
const topology = createMvpTopology();
assert.equal(topology.services.boxes.length, 2);
assert.equal(topology.services.gateways.length, 2);
assert.equal(topology.services.patchPanel.serviceId, "hwlab-patch-panel");
assert.equal(topology.constraints.patchPanelIsOnlyCrossDeviceChannel, true);
for (const box of topology.services.boxes) {
assert.equal(box.serviceId, "hwlab-box-simu");
assert.equal(box.constraints.localLoopbackEnabled, false);
assert.equal(box.constraints.crossDevicePropagation, "patch-panel-only");
}
});
test("patch panel routes cross-device signals and records deliveries", () => {
const topology = createMvpTopology();
const patchPanel = createPatchPanel({
wiringConfig: topology.wiringConfig,
now: () => "2026-01-01T00:00:01.000Z"
});
const result = patchPanel.routeSignal({
fromResourceId: "res_boxsim_alpha",
fromPort: "uart0",
value: "boot>"
});
assert.equal(result.accepted, true);
assert.equal(result.propagatedBy, "hwlab-patch-panel");
assert.deepEqual(result.deliveries, [
{
resourceId: "res_boxsim_beta",
port: "uart0",
value: "boot>",
sourceResourceId: "res_boxsim_alpha",
sourcePort: "uart0",
observedAt: "2026-01-01T00:00:01.000Z"
}
]);
});
test("static dry-run fixture matches generated MVP topology", async () => {
const raw = await readFile(new URL("../../fixtures/mvp-topology/topology.json", import.meta.url), "utf8");
assert.deepEqual(JSON.parse(raw), createMvpTopology());
});
+65
View File
@@ -0,0 +1,65 @@
import { createServer } from "node:http";
export function jsonResponse(res, statusCode, body) {
const payload = JSON.stringify(body, null, 2);
res.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"content-length": Buffer.byteLength(payload)
});
res.end(payload);
}
export async function readJson(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const raw = Buffer.concat(chunks).toString("utf8").trim();
if (!raw) {
return {};
}
return JSON.parse(raw);
}
export function createJsonServer({ routes }) {
return createServer(async (req, res) => {
try {
const url = new URL(req.url ?? "/", "http://localhost");
const key = `${req.method ?? "GET"} ${url.pathname}`;
const handler = routes.get(key);
if (!handler) {
jsonResponse(res, 404, {
error: "not_found",
path: url.pathname
});
return;
}
const body = await handler({ req, url });
jsonResponse(res, 200, body);
} catch (error) {
jsonResponse(res, 500, {
error: "internal_error",
message: error instanceof Error ? error.message : String(error)
});
}
});
}
export function parsePort(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) {
return parsed;
}
return fallback;
}
export function listen(server, port, host = "0.0.0.0") {
server.listen(port, host, () => {
const address = server.address();
const bound = typeof address === "object" && address ? `${address.address}:${address.port}` : String(address);
console.log(`listening on ${bound}`);
});
}
+109
View File
@@ -0,0 +1,109 @@
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_PORTS = Object.freeze(["uart0", "eth0", "gpio0"]);
function isoNow() {
return new Date().toISOString();
}
export function createBoxResource({ boxId, gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID, now = isoNow() }) {
return {
resourceId: `res_${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: SIM_PORTS,
propagation: "patch-panel-only"
},
createdAt: now,
updatedAt: now
};
}
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, now = isoNow() }) {
const ports = Object.fromEntries(
SIM_PORTS.map((port) => [
port,
{
port,
value: null,
source: "local",
updatedAt: now
}
])
);
return {
serviceId: "hwlab-box-simu",
boxId,
projectId: DEFAULT_PROJECT_ID,
gatewaySessionId,
live: true,
resource: createBoxResource({ boxId, gatewaySessionId, 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
};
}