feat(editor): a modem offers a code or joins one, and says under it how it stands (K4, K5)

The modem's sheet chooses how it finds its partner: the other modem on its
port in this project, as FILIUS' file pairs them; another project's, by
offering this modem's code (shown large, with a copy button) or by joining
a code typed in as one reads it off the board. In the action mode each
modem says under it how it stands — connected, waiting, taken, no second
modem on the port, cable missing, on the server only.

Accepted in three browsers on a real F, each in a copy of FILIUS' modem
example: Ben's notebook pings the computer behind Mia's access point; Rita
joins the same code, reads that it is taken, and gets it once Ben stops.
FILIUS' modem example runs in the page's own room as well.

The .fls export writes an offering modem as Java's server and a joining
one as its client; the code has no place there and is named as lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:34:54 +00:00
co-authored by Claude Opus 5.5
parent 7b943d3d96
commit 87c09c6c9d
12 changed files with 355 additions and 34 deletions
+9 -10
View File
@@ -232,12 +232,12 @@ function exportNode(node: TopologyNode, project: ProjectToExport, servingModems:
};
}
/** The first node of each remote link pair: its modem serves. */
/** The first node of each remote link pair in the project: its modem serves (a modem of another project serves by its role). */
function firstOfEachPair(nodes: readonly TopologyNode[], project: ProjectToExport): Set<Id> {
const seen = new Set<string>();
const serving = new Set<Id>();
for (const node of nodes) {
if (node.kind !== 'remoteLink') continue;
if (node.kind !== 'remoteLink' || project.remoteLinkConfigurations.get(node.id)?.relay !== undefined) continue;
const pair = project.remoteLinkConfigurations.get(node.id)?.pairIdentifier ?? '';
if (seen.has(pair)) continue;
seen.add(pair);
@@ -255,14 +255,13 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
const configuration = project.switchConfigurations.get(id) ?? defaultSwitchConfiguration(id);
return { ssid: configuration.ssid, retentionTime: configuration.retentionTimeMilliseconds };
}
case 'remoteLink':
return {
modem: {
mode: servingModems.has(id) ? 1 : 2,
port: modemPort(project.remoteLinkConfigurations.get(id)?.pairIdentifier),
ipAddress: 'localhost',
},
};
case 'remoteLink': {
const configuration = project.remoteLinkConfigurations.get(id);
// A modem of another project: Java's server offers, its client joins; the code has no place in Java's file.
if (configuration?.relay !== undefined) warn('the connection to another project (its code) is not kept');
const serves = configuration?.relay === undefined ? servingModems.has(id) : configuration.relay.role === 'offer';
return { modem: { mode: serves ? 1 : 2, port: modemPort(configuration?.pairIdentifier), ipAddress: 'localhost' } };
}
case 'unsupported':
return {};
default:
+22
View File
@@ -1029,6 +1029,28 @@ describe('exportProject rules', () => {
]);
});
it("writes a modem of another project as Java's server when it offers and as its client when it joins, and says the code is not kept", () => {
const modems = project([node('offer', 'remoteLink', ['o0']), node('join', 'remoteLink', ['j0']), node('a', 'remoteLink', ['a0']), node('b', 'remoteLink', ['b0'])], []);
const { topology, warnings } = exportProject({
...modems,
remoteLinkConfigurations: new Map([
// The joining modem comes first among the modems on port 12345: by its role, not as the first of a pair.
['join', { pairIdentifier: 'java-modem-port-12345', latencyMilliseconds: 20, isEnabled: true, relay: { role: 'join', code: 'K7F3QX' } }],
['offer', { pairIdentifier: 'java-modem-port-12345', latencyMilliseconds: 20, isEnabled: true, relay: { role: 'offer' } }],
['a', { pairIdentifier: 'java-modem-port-12345', latencyMilliseconds: 20, isEnabled: true }],
['b', { pairIdentifier: 'java-modem-port-12345', latencyMilliseconds: 20, isEnabled: true }],
]),
});
expect(topology.nodes.map((each) => [each.name, each.system.modem?.mode])).toEqual([
['offer', 1],
['join', 2],
// The pair in the project: its first serves, the relay modems on the same port do not count.
['a', 1],
['b', 2],
]);
expect(warnings).toEqual(["node 'offer': the connection to another project (its code) is not kept", "node 'join': the connection to another project (its code) is not kept"]);
});
it('joins wirelessly only a host with a wireless card and a switch', () => {
const { topology } = exportProject(
project(