@meshbench/client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -0
- package/lib/boundary.mjs +123 -0
- package/lib/checks.mjs +178 -0
- package/lib/device.mjs +97 -0
- package/lib/errors.mjs +150 -0
- package/lib/firmware.mjs +249 -0
- package/lib/launch.mjs +112 -0
- package/lib/live.mjs +103 -0
- package/lib/nodes.mjs +416 -0
- package/lib/pairing.mjs +65 -0
- package/lib/parts.mjs +190 -0
- package/lib/sessions.mjs +122 -0
- package/lib/sets.mjs +275 -0
- package/lib/sim.mjs +125 -0
- package/lib/socket.mjs +311 -0
- package/lib/subscribe.mjs +86 -0
- package/lib/values.mjs +66 -0
- package/lib/wait.mjs +68 -0
- package/lib/workbench.mjs +376 -0
- package/meshbench.mjs +82 -0
- package/package.json +31 -0
package/lib/sessions.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Which workbenches are running on this machine.
|
|
2
|
+
//
|
|
3
|
+
// `Workbench.attach()` goes to one address, which is enough while there is one
|
|
4
|
+
// session per user. Two runs side by side - a soak beside the workbench
|
|
5
|
+
// somebody is watching, two jobs on one CI runner - need a second address, and
|
|
6
|
+
// until this existed the only record of where it was lived in the head of
|
|
7
|
+
// whoever typed it.
|
|
8
|
+
//
|
|
9
|
+
// A module function rather than a method, because the question comes before a
|
|
10
|
+
// connection: a script asks what is running in order to decide what to attach
|
|
11
|
+
// to.
|
|
12
|
+
//
|
|
13
|
+
// # Telling a live session from what a dead one left behind
|
|
14
|
+
//
|
|
15
|
+
// A workbench killed with SIGKILL cannot clean up after itself, and neither
|
|
16
|
+
// obvious check survives that. A unix socket file outlives the process that
|
|
17
|
+
// bound it; a pid is reused, so a pid that exists today may name somebody
|
|
18
|
+
// else's program. Both would report a dead session as running. So the check is
|
|
19
|
+
// a connect to the address itself, which is the same check the workbench makes
|
|
20
|
+
// before it takes an address, and the leftover file is removed when nothing
|
|
21
|
+
// answers. A session's own tidying up shortens this directory; it is not what
|
|
22
|
+
// makes the answer right.
|
|
23
|
+
//
|
|
24
|
+
// Windows works the same way: there the address is a loopback host and port and
|
|
25
|
+
// the check is a TCP connect. Nothing here is unix-only.
|
|
26
|
+
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
|
|
30
|
+
import { Connection, SESSIONS_ENV, cacheDir } from "./socket.mjs";
|
|
31
|
+
|
|
32
|
+
/** How long a live session is given to describe itself. Generous, because the
|
|
33
|
+
* answer is not worth a wrong row: a session in the middle of something slow is
|
|
34
|
+
* still running and is listed either way, with its description missing. */
|
|
35
|
+
export const DETAIL_WAIT_MS = 2000;
|
|
36
|
+
|
|
37
|
+
/** The per-user directory the session files live in. */
|
|
38
|
+
export function sessionsDir() {
|
|
39
|
+
return process.env[SESSIONS_ENV] || path.join(cacheDir(), "sessions");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The workbenches running on this machine, oldest first.
|
|
43
|
+
*
|
|
44
|
+
* A session that has died is not listed, however it died, and what it left
|
|
45
|
+
* behind is removed on the way past.
|
|
46
|
+
*
|
|
47
|
+
* Each row is `{address, pid, startedAt, token, version, mode, project,
|
|
48
|
+
* nodes}`. Pass one to `Workbench.attach({session})`: that is the way to reach
|
|
49
|
+
* a second TCP session, whose token sits beside its address in its own file
|
|
50
|
+
* where the per-user rendezvous file two of them share has only one. */
|
|
51
|
+
export async function sessions() {
|
|
52
|
+
const dir = sessionsDir();
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = fs.readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
|
|
56
|
+
} catch {
|
|
57
|
+
return []; // no directory is no sessions, not a failure to report
|
|
58
|
+
}
|
|
59
|
+
const found = await Promise.all(entries.map(async (name) => {
|
|
60
|
+
const file = path.join(dir, name);
|
|
61
|
+
const row = read(file);
|
|
62
|
+
if (row === null) return null;
|
|
63
|
+
const detail = await describe(row);
|
|
64
|
+
if (detail === null) {
|
|
65
|
+
// Nothing is answering there, so nothing is running there.
|
|
66
|
+
try { fs.rmSync(file); } catch { /* somebody else got there first */ }
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return { ...row, ...detail };
|
|
70
|
+
}));
|
|
71
|
+
// Oldest first, so two runs listed twice come back in the same order. The
|
|
72
|
+
// timestamps are RFC 3339 written by one program on one machine, so the text
|
|
73
|
+
// sorts in time order, and the address settles a tie whatever happens.
|
|
74
|
+
return found.filter(Boolean).sort((a, b) =>
|
|
75
|
+
(a.startedAt + a.address).localeCompare(b.startedAt + b.address));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function read(file) {
|
|
79
|
+
let got;
|
|
80
|
+
try {
|
|
81
|
+
got = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
82
|
+
} catch {
|
|
83
|
+
// A file this package cannot read is a file it did not write, and refusing
|
|
84
|
+
// to list anything because of one would make the whole answer hostage to it.
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (!got.address) return null;
|
|
88
|
+
return {
|
|
89
|
+
address: got.address,
|
|
90
|
+
pid: got.pid || 0,
|
|
91
|
+
startedAt: String(got.started_at || ""),
|
|
92
|
+
token: String(got.token || ""),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One connection, both answers: whether anything is there, and what it is
|
|
97
|
+
* running. A connection that opens is a live session whether or not it finds a
|
|
98
|
+
* moment to describe itself. */
|
|
99
|
+
async function describe(row) {
|
|
100
|
+
let conn;
|
|
101
|
+
try {
|
|
102
|
+
conn = await Connection.open({
|
|
103
|
+
address: row.address, token: row.token,
|
|
104
|
+
connectTimeoutMs: DETAIL_WAIT_MS, callTimeoutMs: DETAIL_WAIT_MS,
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const got = (await conn.call("session.hello")) || {};
|
|
111
|
+
return {
|
|
112
|
+
version: String(got.version || ""),
|
|
113
|
+
mode: String(got.mode || ""),
|
|
114
|
+
project: String(got.project || ""),
|
|
115
|
+
nodes: got.nodes || 0,
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return {};
|
|
119
|
+
} finally {
|
|
120
|
+
conn.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
package/lib/sets.mjs
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Generated by tools/clientgen. DO NOT EDIT.
|
|
2
|
+
//
|
|
3
|
+
// The closed sets, from internal/world/scenario. Named members rather than free
|
|
4
|
+
// strings so an editor can complete them and a member that has gone is
|
|
5
|
+
// undefined at the call site - and generated so the list cannot be wrong the
|
|
6
|
+
// week somebody adds a board.
|
|
7
|
+
//
|
|
8
|
+
// Every member is a plain string, so it goes on the wire as itself and a
|
|
9
|
+
// literal is still accepted anywhere one of these is asked for. The names match
|
|
10
|
+
// the Python client's exactly, so a script moved between the two changes the
|
|
11
|
+
// dots and nothing else.
|
|
12
|
+
|
|
13
|
+
/** What a node is. */
|
|
14
|
+
export const Kind = Object.freeze({
|
|
15
|
+
/** forwards, and nothing else */
|
|
16
|
+
SIMPLE_REPEATER: "simple-repeater",
|
|
17
|
+
/** forwards, serves clients, holds state */
|
|
18
|
+
ADVANCED_REPEATER: "advanced-repeater",
|
|
19
|
+
/** a user's device - the thing a phone connects to */
|
|
20
|
+
COMPANION: "companion",
|
|
21
|
+
/**
|
|
22
|
+
* holds posts for clients to collect, and does not forward: a mesh that
|
|
23
|
+
* treats one as a repeater overstates its own reach
|
|
24
|
+
*/
|
|
25
|
+
ROOM_SERVER: "room-server",
|
|
26
|
+
/**
|
|
27
|
+
* runs no firmware and transmits nothing; captures the summed field at its
|
|
28
|
+
* antenna and hands back IQ
|
|
29
|
+
*/
|
|
30
|
+
SDR_OBSERVER: "sdr-observer",
|
|
31
|
+
/**
|
|
32
|
+
* interference that is not MeshCore, propagated through the same terrain
|
|
33
|
+
* as everything else
|
|
34
|
+
*/
|
|
35
|
+
EMITTER: "emitter",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A hardware profile this build knows about.
|
|
40
|
+
*
|
|
41
|
+
* A node's board decides its transmit ceiling, its receive chain's noise
|
|
42
|
+
* figure and the battery the energy model uses, so naming one that does not
|
|
43
|
+
* exist is refused rather than defaulted.
|
|
44
|
+
*/
|
|
45
|
+
export const Board = Object.freeze({
|
|
46
|
+
/** ESP32-S3, SX1262, by Ebyte. */
|
|
47
|
+
EBYTE_EORA_S3: "Ebyte_EoRa-S3",
|
|
48
|
+
/** ESP32, SX1262, by Ebyte. */
|
|
49
|
+
GENERIC_E22_SX1262: "Generic_E22_sx1262",
|
|
50
|
+
/** ESP32-S3, SX1262, by Heltec. */
|
|
51
|
+
HELTEC_E213: "Heltec_E213",
|
|
52
|
+
/** ESP32-S3, SX1262, by Heltec. */
|
|
53
|
+
HELTEC_E290: "Heltec_E290",
|
|
54
|
+
/** ESP32-S3, SX1262, by Heltec. */
|
|
55
|
+
HELTEC_WSL3: "Heltec_WSL3",
|
|
56
|
+
/** ESP32-S3, SX1262, by Heltec. */
|
|
57
|
+
HELTEC_WIRELESS_PAPER: "Heltec_Wireless_Paper",
|
|
58
|
+
/** ESP32-S3, SX1262, by Heltec. */
|
|
59
|
+
HELTEC_WIRELESS_TRACKER: "Heltec_Wireless_Tracker",
|
|
60
|
+
/** nRF52840, SX1262, by Heltec. */
|
|
61
|
+
HELTEC_MESH_SOLAR: "Heltec_mesh_solar",
|
|
62
|
+
/** nRF52840, SX1262, by Heltec. */
|
|
63
|
+
HELTEC_T096: "Heltec_t096",
|
|
64
|
+
/** nRF52840, SX1262, by Heltec. */
|
|
65
|
+
HELTEC_T114: "Heltec_t114",
|
|
66
|
+
/** ESP32, SX1276, by Heltec. */
|
|
67
|
+
HELTEC_V2: "Heltec_v2",
|
|
68
|
+
/** ESP32-S3, SX1262, by Heltec. */
|
|
69
|
+
HELTEC_V3: "Heltec_v3",
|
|
70
|
+
/** ESP32-S3, SX1262, by LILYGO. */
|
|
71
|
+
LILYGO_T3S3_SX1262: "LilyGo_T3S3_sx1262",
|
|
72
|
+
/** ESP32-S3, SX1262, by LILYGO. */
|
|
73
|
+
LILYGO_TBEAM_1W: "LilyGo_TBeam_1W",
|
|
74
|
+
/** ESP32-S3, SX1262, by LILYGO. */
|
|
75
|
+
LILYGO_TDECK: "LilyGo_TDeck",
|
|
76
|
+
/** ESP32-S3, SX1262, by RAKwireless. */
|
|
77
|
+
RAK_3112: "RAK_3112",
|
|
78
|
+
/** nRF52840, SX1262, by RAKwireless. */
|
|
79
|
+
RAK_4631: "RAK_4631",
|
|
80
|
+
/** ESP32-S3, SX1262, by LILYGO. */
|
|
81
|
+
STATION_G2: "Station_G2",
|
|
82
|
+
/** ESP32-S3, SX1262, by LILYGO. */
|
|
83
|
+
STATION_G3_ESP32: "Station_G3_ESP32",
|
|
84
|
+
/** ESP32, SX1262, by LILYGO. */
|
|
85
|
+
TBEAM_SX1262: "Tbeam_SX1262",
|
|
86
|
+
/** ESP32-S3, SX1262, by Seeed. */
|
|
87
|
+
XIAO_S3: "Xiao_S3",
|
|
88
|
+
/** ESP32-S3, SX1262, by Seeed. */
|
|
89
|
+
XIAO_S3_WIO: "Xiao_S3_WIO",
|
|
90
|
+
/** nRF52840, SX1262, by Seeed. */
|
|
91
|
+
XIAO_NRF52: "Xiao_nrf52",
|
|
92
|
+
/** ESP32-S3, SX1262, by Heltec. */
|
|
93
|
+
HELTEC_TRACKER_V2: "heltec_tracker_v2",
|
|
94
|
+
/** ESP32-S3, SX1262, by Heltec. */
|
|
95
|
+
HELTEC_V4: "heltec_v4",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/** Every board, for a caller offering a choice. */
|
|
99
|
+
export const Boards = Object.freeze(Object.values(Board));
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A named set of LoRa parameters for a territory.
|
|
103
|
+
*
|
|
104
|
+
* An agreement between operators rather than a configuration, which is why
|
|
105
|
+
* the list is baked in rather than fetched.
|
|
106
|
+
*/
|
|
107
|
+
export const Preset = Object.freeze({
|
|
108
|
+
/** 915.800 MHz, 250.0 kHz, SF10, CR 4/5. */
|
|
109
|
+
AUSTRALIA: "Australia",
|
|
110
|
+
/** 916.575 MHz, 62.5 kHz, SF7, CR 4/8. */
|
|
111
|
+
AUSTRALIA_NARROW: "Australia (Narrow)",
|
|
112
|
+
/** 915.075 MHz, 125.0 kHz, SF9, CR 4/5. */
|
|
113
|
+
AUSTRALIA_MID: "Australia (Mid)",
|
|
114
|
+
/** 923.125 MHz, 62.5 kHz, SF8, CR 4/8. */
|
|
115
|
+
AUSTRALIA_SA_WA: "Australia: SA, WA",
|
|
116
|
+
/** 923.125 MHz, 62.5 kHz, SF8, CR 4/5. */
|
|
117
|
+
AUSTRALIA_QLD: "Australia: QLD",
|
|
118
|
+
/** 923.125 MHz, 62.5 kHz, SF8, CR 4/8. */
|
|
119
|
+
BRAZIL: "Brazil",
|
|
120
|
+
/** 869.618 MHz, 62.5 kHz, SF8, CR 4/8. */
|
|
121
|
+
EU_UK_NARROW: "EU/UK (Narrow)",
|
|
122
|
+
/** 869.525 MHz, 250.0 kHz, SF11, CR 4/5. */
|
|
123
|
+
EU_UK_DEPRECATED: "EU/UK (Deprecated)",
|
|
124
|
+
/** 869.432 MHz, 62.5 kHz, SF7, CR 4/5. */
|
|
125
|
+
CZECH_REPUBLIC_NARROW: "Czech Republic (Narrow)",
|
|
126
|
+
/** 433.650 MHz, 250.0 kHz, SF11, CR 4/5. */
|
|
127
|
+
EU_433MHZ_LONG_RANGE: "EU 433MHz (Long Range)",
|
|
128
|
+
/** 433.650 MHz, 62.5 kHz, SF8, CR 4/8. */
|
|
129
|
+
EU_433MHZ_NARROW: "EU 433MHz (Narrow)",
|
|
130
|
+
/** 869.618 MHz, 62.5 kHz, SF7, CR 4/5. */
|
|
131
|
+
NETHERLANDS: "Netherlands",
|
|
132
|
+
/** 917.375 MHz, 250.0 kHz, SF11, CR 4/5. */
|
|
133
|
+
NEW_ZEALAND: "New Zealand",
|
|
134
|
+
/** 917.375 MHz, 62.5 kHz, SF7, CR 4/5. */
|
|
135
|
+
NEW_ZEALAND_NARROW: "New Zealand (Narrow)",
|
|
136
|
+
/** 433.375 MHz, 62.5 kHz, SF9, CR 4/6. */
|
|
137
|
+
PORTUGAL_433: "Portugal 433",
|
|
138
|
+
/** 869.618 MHz, 62.5 kHz, SF7, CR 4/6. */
|
|
139
|
+
PORTUGAL_868: "Portugal 868",
|
|
140
|
+
/** 869.618 MHz, 62.5 kHz, SF8, CR 4/8. */
|
|
141
|
+
SWITZERLAND: "Switzerland",
|
|
142
|
+
/** 910.525 MHz, 62.5 kHz, SF7, CR 4/5. */
|
|
143
|
+
USA_CANADA_RECOMMENDED: "USA/Canada (Recommended)",
|
|
144
|
+
/** 920.250 MHz, 62.5 kHz, SF8, CR 4/5. */
|
|
145
|
+
VIETNAM_NARROW: "Vietnam (Narrow)",
|
|
146
|
+
/** 920.250 MHz, 250.0 kHz, SF11, CR 4/5. */
|
|
147
|
+
VIETNAM_DEPRECATED: "Vietnam (Deprecated)",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/** Every preset, for a caller offering a choice. */
|
|
151
|
+
export const Presets = Object.freeze(Object.values(Preset));
|
|
152
|
+
|
|
153
|
+
/** What a fresh scenario uses. */
|
|
154
|
+
export const DEFAULT_PRESET = "EU/UK (Narrow)";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The MeshCore application a node runs, named as upstream names its example
|
|
158
|
+
* directory.
|
|
159
|
+
*
|
|
160
|
+
* The string every firmware verb is keyed on. The published catalogue spells
|
|
161
|
+
* some of the same things differently - "repeater", "room-server" - and
|
|
162
|
+
* those belong to the release assets; typing one at a verb pins nothing, and
|
|
163
|
+
* the run then refuses to start with no clue as to why.
|
|
164
|
+
*/
|
|
165
|
+
export const Role = Object.freeze({
|
|
166
|
+
/** forwards; both repeater kinds run it and differ only in configuration */
|
|
167
|
+
SIMPLE_REPEATER: "simple_repeater",
|
|
168
|
+
/** a user's device - the thing a phone connects to */
|
|
169
|
+
COMPANION_RADIO: "companion_radio",
|
|
170
|
+
/** holds posts for clients to collect, and does not forward */
|
|
171
|
+
SIMPLE_ROOM_SERVER: "simple_room_server",
|
|
172
|
+
/**
|
|
173
|
+
* the USB companion build; board images only, where a board publishes both
|
|
174
|
+
* transports at one version
|
|
175
|
+
*/
|
|
176
|
+
COMPANION_RADIO_USB: "companion_radio_usb",
|
|
177
|
+
/** the Bluetooth companion build; board images only */
|
|
178
|
+
COMPANION_RADIO_BLE: "companion_radio_ble",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/** Every role, for a caller offering a choice. */
|
|
182
|
+
export const Roles = Object.freeze(Object.values(Role));
|
|
183
|
+
|
|
184
|
+
/** What happened to an event. */
|
|
185
|
+
export const Class = Object.freeze({
|
|
186
|
+
/** this node transmitted it */
|
|
187
|
+
SENT: "sent",
|
|
188
|
+
/** this node decoded it, for the first time */
|
|
189
|
+
RECEIVED: "received",
|
|
190
|
+
/**
|
|
191
|
+
* missed because this node's own transmitter was keyed; LoRa is half
|
|
192
|
+
* duplex
|
|
193
|
+
*/
|
|
194
|
+
HALF_DUPLEX: "half-duplex",
|
|
195
|
+
/** would have decoded, but a stronger signal took it */
|
|
196
|
+
INTERFERENCE: "interference",
|
|
197
|
+
/**
|
|
198
|
+
* decoded its header, then a collision destroyed more symbols than the
|
|
199
|
+
* coding rate could repair
|
|
200
|
+
*/
|
|
201
|
+
COLLISION: "collision",
|
|
202
|
+
/**
|
|
203
|
+
* arrived at a demodulator already locked to another packet; a LoRa
|
|
204
|
+
* receiver decodes one at a time
|
|
205
|
+
*/
|
|
206
|
+
RECEIVER_BUSY: "receiver-busy",
|
|
207
|
+
/** too quiet: under the demodulator's threshold for its spreading factor */
|
|
208
|
+
FLOOR: "floor",
|
|
209
|
+
/**
|
|
210
|
+
* a miss whose cause the engine did not establish; never assume it was a
|
|
211
|
+
* weak signal
|
|
212
|
+
*/
|
|
213
|
+
UNCLASSIFIED: "unclassified",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/** Every class, for a caller offering a choice. */
|
|
217
|
+
export const Classes = Object.freeze(Object.values(Class));
|
|
218
|
+
|
|
219
|
+
/** A pane of a node's own window. */
|
|
220
|
+
export const Tab = Object.freeze({
|
|
221
|
+
/** the firmware's text console, which only a repeater has */
|
|
222
|
+
CONSOLE: "Console",
|
|
223
|
+
/** channels, contacts and the companion command line */
|
|
224
|
+
COMPANION: "Companion",
|
|
225
|
+
/** an observer's antenna: serve it, read the address */
|
|
226
|
+
SDR: "SDR",
|
|
227
|
+
/** what this node is: identity, radio, regions, firmware */
|
|
228
|
+
SETTINGS: "Settings",
|
|
229
|
+
/** what the chip is really doing */
|
|
230
|
+
RADIO: "Radio",
|
|
231
|
+
/** what this node stands under and which way it points */
|
|
232
|
+
ANTENNA: "Antenna",
|
|
233
|
+
/** what it has cost and what it has carried */
|
|
234
|
+
STATS: "Stats",
|
|
235
|
+
/** what it has heard and sent, in order */
|
|
236
|
+
ACTIVITY: "Activity",
|
|
237
|
+
/** hand this companion to a real client */
|
|
238
|
+
CONNECT: "Connect",
|
|
239
|
+
/**
|
|
240
|
+
* the board drawn as itself - its screen, its lamps, the buttons somebody
|
|
241
|
+
* can press; only a board that declares any of that grows it
|
|
242
|
+
*/
|
|
243
|
+
HARDWARE: "Hardware",
|
|
244
|
+
/**
|
|
245
|
+
* what the node printed: its serial port, the emulator running it, or the
|
|
246
|
+
* radio model beside it
|
|
247
|
+
*/
|
|
248
|
+
OUTPUT: "Output",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/** Every tab, for a caller offering a choice. */
|
|
252
|
+
export const Tabs = Object.freeze(Object.values(Tab));
|
|
253
|
+
|
|
254
|
+
/** How an imported deployment meets what is already loaded. */
|
|
255
|
+
export const Strategy = Object.freeze({
|
|
256
|
+
/**
|
|
257
|
+
* throw away what is loaded and take the import; what the shipped fixtures
|
|
258
|
+
* were built with
|
|
259
|
+
*/
|
|
260
|
+
REPLACE: "replace-all",
|
|
261
|
+
/** keep what is loaded and add the names it has not got */
|
|
262
|
+
ADD: "add",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/** How a served companion is reached. */
|
|
266
|
+
export const Transport = Object.freeze({
|
|
267
|
+
/**
|
|
268
|
+
* a socket on every interface, on a port the system picks; the one to
|
|
269
|
+
* point a phone or another machine at
|
|
270
|
+
*/
|
|
271
|
+
TCP: "tcp",
|
|
272
|
+
/** a pseudo-terminal, for a client that wants a serial port */
|
|
273
|
+
SERIAL: "serial",
|
|
274
|
+
});
|
|
275
|
+
|
package/lib/sim.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// The clock, and the run.
|
|
2
|
+
|
|
3
|
+
import { MeshbenchError } from "./errors.mjs";
|
|
4
|
+
import {
|
|
5
|
+
FIRMWARE_WAIT_MS, JOB_WAIT_MS, RUN_WAIT_MS, secs, waitFor,
|
|
6
|
+
} from "./wait.mjs";
|
|
7
|
+
|
|
8
|
+
/** The clock, and the run. Live. */
|
|
9
|
+
export class Sim {
|
|
10
|
+
constructor(wb) { this._wb = wb; }
|
|
11
|
+
|
|
12
|
+
/** What the clock is doing. */
|
|
13
|
+
async state() { return (await this._wb.call("sim.state")) || {}; }
|
|
14
|
+
|
|
15
|
+
async playing() { return Boolean((await this.state()).playing); }
|
|
16
|
+
|
|
17
|
+
async nowMs() { return (await this.state()).now_ms || 0; }
|
|
18
|
+
|
|
19
|
+
/** Bring the run up: wait out the warm, start every node, and play.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately not one call to `sim.start`. That verb is the play button's
|
|
22
|
+
* own handler and answers four ways - it pauses if already playing, declines
|
|
23
|
+
* while links are being measured, or starts firmware and does not play - so
|
|
24
|
+
* a script pressing it once gets whichever of those the moment happens to be
|
|
25
|
+
* in.
|
|
26
|
+
*
|
|
27
|
+
* Worse, it only starts firmware when no node is running. Pin a build onto
|
|
28
|
+
* two nodes of a fifty-eight node fixture and it considers the mesh started,
|
|
29
|
+
* plays with fifty-six of them down, and says nothing.
|
|
30
|
+
*
|
|
31
|
+
* So this asks for the three things it actually wants, in order, and checks
|
|
32
|
+
* each one. */
|
|
33
|
+
async start({ warmMs = JOB_WAIT_MS, firmwareMs = FIRMWARE_WAIT_MS } = {}) {
|
|
34
|
+
// The links first. Nothing that follows means anything against a matrix
|
|
35
|
+
// that is still being measured.
|
|
36
|
+
await this._wb.waitIdle(warmMs);
|
|
37
|
+
// Idle is not the same as measured. A warm that stopped to ask permission
|
|
38
|
+
// to download terrain finishes its own job row, so the wait above returns
|
|
39
|
+
// in a moment having waited for nothing: no link was measured, and every
|
|
40
|
+
// study after this would answer over free space.
|
|
41
|
+
const held = await this.state();
|
|
42
|
+
if (held.warm_held) {
|
|
43
|
+
const note = (held.ground || {}).note ||
|
|
44
|
+
"call terrain.allow to answer the question either way";
|
|
45
|
+
throw new MeshbenchError(
|
|
46
|
+
"the link measurement is held: no terrain has been downloaded and no " +
|
|
47
|
+
`link has been measured. ${note}`);
|
|
48
|
+
}
|
|
49
|
+
// Then every node that is not up, which firmware.start does and sim.start
|
|
50
|
+
// does only when none of them are.
|
|
51
|
+
const st = await this._wb.firmware.state();
|
|
52
|
+
if ((st.running || 0) < (st.nodes || 0)) {
|
|
53
|
+
await this._wb.firmware.start();
|
|
54
|
+
await this._wb.firmware.waitStarted(firmwareMs);
|
|
55
|
+
}
|
|
56
|
+
// Then the clock, by its own name. play cannot pause, which is the other
|
|
57
|
+
// half of what made sim.start unusable from a script.
|
|
58
|
+
if (!(await this.playing())) await this.play();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async play() { await this._wb.call("sim.play"); }
|
|
62
|
+
|
|
63
|
+
async pause() { await this._wb.call("sim.pause"); }
|
|
64
|
+
|
|
65
|
+
/** Press the play button as the window presses it, and answer as it answers.
|
|
66
|
+
*
|
|
67
|
+
* Almost never what a script means - `start()` is - and here because the
|
|
68
|
+
* verb exists and a client that hid it would send somebody back to `call`. */
|
|
69
|
+
async toggle() { return (await this._wb.call("sim.toggle")) || {}; }
|
|
70
|
+
|
|
71
|
+
/** Advance one tick, which is `step_ms` of simulated time. */
|
|
72
|
+
async step() { await this._wb.call("sim.step"); }
|
|
73
|
+
|
|
74
|
+
/** Put the clock and the counters back to the start of the run. */
|
|
75
|
+
async reset() { await this._wb.call("sim.reset"); }
|
|
76
|
+
|
|
77
|
+
/** Step a paused run, which is how a command gets the time it needs to be
|
|
78
|
+
* answered without starting the clock. */
|
|
79
|
+
async settle(steps = 60) { await this._wb.call("sim.settle", { steps }); }
|
|
80
|
+
|
|
81
|
+
/** Fix the run. Same seed, same scenario, same result - which is what makes a
|
|
82
|
+
* changed result mean something. */
|
|
83
|
+
async setSeed(seed) { await this._wb.call("sim.seed", { seed }); }
|
|
84
|
+
|
|
85
|
+
/** How much simulated time one tick advances. */
|
|
86
|
+
async setStepMs(ms) { await this._wb.call("sim.speed", { step_ms: ms }); }
|
|
87
|
+
|
|
88
|
+
/** Whether play starts MeshCore on every node, or runs the channel with
|
|
89
|
+
* nothing behind it. */
|
|
90
|
+
async setRealFirmware(on = true) { await this._wb.call("sim.kind", { real: on }); }
|
|
91
|
+
|
|
92
|
+
/** Advance the mesh's own clock by this much, and wait for it.
|
|
93
|
+
*
|
|
94
|
+
* Two clocks, one call, and they are not the same one. `simulatedMs` is the
|
|
95
|
+
* mesh's: five minutes here is five minutes of its time. `waitMs` is yours -
|
|
96
|
+
* how long you are prepared to sit here before giving up. On 155 emulated
|
|
97
|
+
* nodes five simulated minutes is a great deal more than five of yours,
|
|
98
|
+
* which is why the second is separate and generous. */
|
|
99
|
+
async run(simulatedMs, { waitMs = RUN_WAIT_MS } = {}) {
|
|
100
|
+
if (!(simulatedMs > 0)) {
|
|
101
|
+
throw new MeshbenchError("run() needs a length in simulated milliseconds");
|
|
102
|
+
}
|
|
103
|
+
await this._wb.call("sim.run", { for_ms: simulatedMs });
|
|
104
|
+
await this.waitStopped(waitMs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Wait for a run to end. `timeoutMs` is wall clock. */
|
|
108
|
+
waitStopped(timeoutMs = RUN_WAIT_MS) {
|
|
109
|
+
return waitFor(async () => {
|
|
110
|
+
const st = await this.state();
|
|
111
|
+
if (!st.playing) return [true, ""];
|
|
112
|
+
return [false, `${secs(st.now_ms || 0)} of simulated time`];
|
|
113
|
+
}, timeoutMs, "the run to finish");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Wait for the mesh's clock to reach a moment. `atMs` is simulated time;
|
|
117
|
+
* `timeoutMs` is yours. */
|
|
118
|
+
waitUntil(atMs, timeoutMs = RUN_WAIT_MS) {
|
|
119
|
+
return waitFor(async () => {
|
|
120
|
+
const st = await this.state();
|
|
121
|
+
if ((st.now_ms || 0) >= atMs) return [true, ""];
|
|
122
|
+
return [false, secs(st.now_ms || 0)];
|
|
123
|
+
}, timeoutMs, `simulated time to reach ${secs(atMs)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|