@hla4ts/create-spacekit 0.1.0
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 +73 -0
- package/package.json +29 -0
- package/src/cli.ts +364 -0
- package/src/index.ts +11 -0
- package/src/scaffold.test.ts +56 -0
- package/src/scaffold.ts +199 -0
- package/templates/lunar-rover/.env +14 -0
- package/templates/lunar-rover/README.md +37 -0
- package/templates/lunar-rover/package.json +17 -0
- package/templates/lunar-rover/src/config.ts +98 -0
- package/templates/lunar-rover/src/index.ts +57 -0
- package/templates/lunar-rover/tsconfig.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @hla4ts/create-spacekit
|
|
2
|
+
|
|
3
|
+
Scaffold a minimal `@hla4ts/spacekit` lunar rover app.
|
|
4
|
+
|
|
5
|
+
The generated project is intentionally simpler than the repo's validation
|
|
6
|
+
examples. It creates one `PhysicalEntity`, drives it with `SpacekitApp.run()`,
|
|
7
|
+
and keeps the common RTI/Spacekit knobs in `.env`.
|
|
8
|
+
|
|
9
|
+
## What It Generates
|
|
10
|
+
|
|
11
|
+
- a Bun + TypeScript project
|
|
12
|
+
- a single-file rover runtime built on `@hla4ts/spacekit`
|
|
13
|
+
- `.env` defaults for RTI host/port/TLS, federation, federate, frame, timing,
|
|
14
|
+
and rover motion
|
|
15
|
+
|
|
16
|
+
## Local Workspace Usage
|
|
17
|
+
|
|
18
|
+
From this repo checkout:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun run --filter @hla4ts/create-spacekit cli -- my-rover
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Intended Published Usage
|
|
25
|
+
|
|
26
|
+
The package is structured with a `bin` entry so it is ready for:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bunx @hla4ts/create-spacekit my-rover
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That one-line path still depends on publishing the package to the npm registry.
|
|
33
|
+
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
- `--template lunar-rover`
|
|
37
|
+
- `--list-templates`
|
|
38
|
+
- `--federation-name <name>`
|
|
39
|
+
- `--federate-name <name>`
|
|
40
|
+
- `--federate-type <type>`
|
|
41
|
+
- `--rti-host <host>`
|
|
42
|
+
- `--rti-port <port>`
|
|
43
|
+
- `--rti-use-tls`
|
|
44
|
+
- `--parent-reference-frame <frame>`
|
|
45
|
+
- `--rover-name <name>`
|
|
46
|
+
- `--rover-type <type>`
|
|
47
|
+
- `--rover-speed-mps <value>`
|
|
48
|
+
- `--lookahead-micros <value>`
|
|
49
|
+
- `--update-period-micros <value>`
|
|
50
|
+
- `--reference-frame-timeout-ms <value>`
|
|
51
|
+
- `--reference-frame-missing-mode <mode>`
|
|
52
|
+
- `--workspace-deps`
|
|
53
|
+
- `--yes`
|
|
54
|
+
|
|
55
|
+
`--workspace-deps` is for local monorepo validation. It writes
|
|
56
|
+
`"@hla4ts/spacekit": "workspace:*"` into the generated project instead of the
|
|
57
|
+
published semver range.
|
|
58
|
+
|
|
59
|
+
`--reference-frame-missing-mode` controls what the generated rover does when
|
|
60
|
+
its parent reference frame has not been published yet. The template default is
|
|
61
|
+
`continue`.
|
|
62
|
+
|
|
63
|
+
## Generated App
|
|
64
|
+
|
|
65
|
+
After scaffolding:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd my-rover
|
|
69
|
+
bun install
|
|
70
|
+
bun run start
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Edit `.env` to point at your RTI and federation.
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hla4ts/create-spacekit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a minimal @hla4ts/spacekit rover app",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"spacekit": "src/cli.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"README.md",
|
|
13
|
+
"src",
|
|
14
|
+
"templates"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"cli": "bun run src/cli.ts",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "bun test"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "latest",
|
|
26
|
+
"typescript": "^5.9.0"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import readline from "node:readline/promises";
|
|
3
|
+
import { stdin, stdout } from "node:process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_TEMPLATE,
|
|
7
|
+
TEMPLATE_DESCRIPTIONS,
|
|
8
|
+
listTemplates,
|
|
9
|
+
scaffoldApp,
|
|
10
|
+
type TemplateId,
|
|
11
|
+
type TemplateValues,
|
|
12
|
+
} from "./scaffold.ts";
|
|
13
|
+
|
|
14
|
+
interface CliOptions {
|
|
15
|
+
targetDir?: string;
|
|
16
|
+
template: TemplateId;
|
|
17
|
+
workspaceDeps: boolean;
|
|
18
|
+
yes: boolean;
|
|
19
|
+
values: Partial<TemplateValues>;
|
|
20
|
+
listTemplates: boolean;
|
|
21
|
+
help: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await main();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
console.error(message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main(): Promise<void> {
|
|
33
|
+
const options = parseArgs(process.argv.slice(2));
|
|
34
|
+
|
|
35
|
+
if (options.help) {
|
|
36
|
+
printHelp();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.listTemplates) {
|
|
41
|
+
for (const template of listTemplates()) {
|
|
42
|
+
console.log(`${template.id}\t${template.description}`);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!options.targetDir) {
|
|
48
|
+
printHelp("Target directory is required.");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isTemplateId(options.template)) {
|
|
53
|
+
throw new Error(`Unknown template "${options.template}". Run --list-templates to inspect choices.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const usePrompts = stdin.isTTY && stdout.isTTY && !options.yes;
|
|
57
|
+
const values = usePrompts
|
|
58
|
+
? await promptForMissingValues(options.targetDir, options.template, options.values)
|
|
59
|
+
: options.values;
|
|
60
|
+
if (options.workspaceDeps) {
|
|
61
|
+
values.spacekitDependency = "workspace:*";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await scaffoldApp({
|
|
65
|
+
targetDir: options.targetDir,
|
|
66
|
+
template: options.template,
|
|
67
|
+
values,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const relativeTarget = path.relative(process.cwd(), result.targetDir) || ".";
|
|
71
|
+
const displayTarget =
|
|
72
|
+
relativeTarget === "." || (!relativeTarget.startsWith("..") && !path.isAbsolute(relativeTarget))
|
|
73
|
+
? relativeTarget
|
|
74
|
+
: result.targetDir;
|
|
75
|
+
console.log(`Created ${result.template} app in ${result.targetDir}`);
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log("Next steps:");
|
|
78
|
+
console.log(` cd ${displayTarget}`);
|
|
79
|
+
console.log(" bun install");
|
|
80
|
+
console.log(" bun run start");
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log(
|
|
83
|
+
"This package is bunx-ready, but the one-line zero-clone flow still depends on npm publication:"
|
|
84
|
+
);
|
|
85
|
+
console.log(` bunx @hla4ts/create-spacekit ${path.basename(result.targetDir)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseArgs(argv: string[]): CliOptions {
|
|
89
|
+
const options: CliOptions = {
|
|
90
|
+
template: DEFAULT_TEMPLATE,
|
|
91
|
+
workspaceDeps: false,
|
|
92
|
+
yes: false,
|
|
93
|
+
values: {},
|
|
94
|
+
listTemplates: false,
|
|
95
|
+
help: false,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
99
|
+
const arg = argv[index];
|
|
100
|
+
if (!arg) continue;
|
|
101
|
+
|
|
102
|
+
if (arg === "--help" || arg === "-h") {
|
|
103
|
+
options.help = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (arg === "--list-templates") {
|
|
107
|
+
options.listTemplates = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (arg === "--yes" || arg === "-y") {
|
|
111
|
+
options.yes = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (arg === "--workspace-deps") {
|
|
115
|
+
options.workspaceDeps = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (arg === "--template") {
|
|
119
|
+
options.template = readValue(argv, ++index, arg) as TemplateId;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (arg === "--federation-name") {
|
|
123
|
+
options.values.federationName = readValue(argv, ++index, arg);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (arg === "--federate-name") {
|
|
127
|
+
options.values.federateName = readValue(argv, ++index, arg);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (arg === "--federate-type") {
|
|
131
|
+
options.values.federateType = readValue(argv, ++index, arg);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (arg === "--rti-host") {
|
|
135
|
+
options.values.rtiHost = readValue(argv, ++index, arg);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (arg === "--rti-port") {
|
|
139
|
+
options.values.rtiPort = readValue(argv, ++index, arg);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (arg === "--rti-use-tls") {
|
|
143
|
+
options.values.rtiUseTls = "true";
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--parent-reference-frame") {
|
|
147
|
+
options.values.parentReferenceFrame = readValue(argv, ++index, arg);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (arg === "--rover-name") {
|
|
151
|
+
options.values.roverName = readValue(argv, ++index, arg);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg === "--rover-type") {
|
|
155
|
+
options.values.roverType = readValue(argv, ++index, arg);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (arg === "--rover-speed-mps") {
|
|
159
|
+
options.values.roverSpeedMps = readValue(argv, ++index, arg);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (arg === "--lookahead-micros") {
|
|
163
|
+
options.values.lookaheadMicros = readValue(argv, ++index, arg);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (arg === "--update-period-micros") {
|
|
167
|
+
options.values.updatePeriodMicros = readValue(argv, ++index, arg);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (arg === "--reference-frame-timeout-ms") {
|
|
171
|
+
options.values.referenceFrameTimeoutMs = readValue(argv, ++index, arg);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (arg === "--reference-frame-missing-mode") {
|
|
175
|
+
options.values.referenceFrameMissingMode = readValue(argv, ++index, arg);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (arg.startsWith("--")) {
|
|
180
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!options.targetDir) {
|
|
184
|
+
options.targetDir = arg;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return options;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function promptForMissingValues(
|
|
195
|
+
targetDir: string,
|
|
196
|
+
template: TemplateId,
|
|
197
|
+
values: Partial<TemplateValues>
|
|
198
|
+
): Promise<Partial<TemplateValues>> {
|
|
199
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
200
|
+
const defaults = {
|
|
201
|
+
federationName: "SpaceFederation",
|
|
202
|
+
federateName: "LunarRover-1",
|
|
203
|
+
federateType: "LunarRover",
|
|
204
|
+
rtiHost: "localhost",
|
|
205
|
+
rtiPort: "15164",
|
|
206
|
+
rtiUseTls: "false",
|
|
207
|
+
parentReferenceFrame: "AitkenBasinLocalFixed",
|
|
208
|
+
roverName: "LunarRover-1",
|
|
209
|
+
roverType: "LunarRover",
|
|
210
|
+
roverSpeedMps: "0.5",
|
|
211
|
+
lookaheadMicros: "1000000",
|
|
212
|
+
updatePeriodMicros: "1000000",
|
|
213
|
+
referenceFrameTimeoutMs: "30000",
|
|
214
|
+
referenceFrameMissingMode: "continue",
|
|
215
|
+
} satisfies Partial<TemplateValues>;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
console.log(`Scaffolding template "${template}" into ${path.resolve(targetDir)}`);
|
|
219
|
+
console.log(TEMPLATE_DESCRIPTIONS[template]);
|
|
220
|
+
console.log("");
|
|
221
|
+
|
|
222
|
+
const federationName = await ask(
|
|
223
|
+
rl,
|
|
224
|
+
"Federation name",
|
|
225
|
+
values.federationName ?? defaults.federationName
|
|
226
|
+
);
|
|
227
|
+
const federateName = await ask(
|
|
228
|
+
rl,
|
|
229
|
+
"Federate name",
|
|
230
|
+
values.federateName ?? defaults.federateName
|
|
231
|
+
);
|
|
232
|
+
const federateType = await ask(
|
|
233
|
+
rl,
|
|
234
|
+
"Federate type",
|
|
235
|
+
values.federateType ?? defaults.federateType
|
|
236
|
+
);
|
|
237
|
+
const rtiHost = await ask(rl, "RTI host", values.rtiHost ?? defaults.rtiHost);
|
|
238
|
+
const rtiPort = await ask(rl, "RTI port", values.rtiPort ?? defaults.rtiPort);
|
|
239
|
+
const rtiUseTls = await askBool(rl, "Use TLS", values.rtiUseTls ?? defaults.rtiUseTls);
|
|
240
|
+
const parentReferenceFrame = await ask(
|
|
241
|
+
rl,
|
|
242
|
+
"Parent reference frame",
|
|
243
|
+
values.parentReferenceFrame ?? defaults.parentReferenceFrame
|
|
244
|
+
);
|
|
245
|
+
const roverName = await ask(
|
|
246
|
+
rl,
|
|
247
|
+
"Rover object name",
|
|
248
|
+
values.roverName ?? federateName ?? defaults.roverName
|
|
249
|
+
);
|
|
250
|
+
const roverType = await ask(
|
|
251
|
+
rl,
|
|
252
|
+
"Rover type",
|
|
253
|
+
values.roverType ?? federateType ?? defaults.roverType
|
|
254
|
+
);
|
|
255
|
+
const roverSpeedMps = await ask(
|
|
256
|
+
rl,
|
|
257
|
+
"Rover speed (m/s)",
|
|
258
|
+
values.roverSpeedMps ?? defaults.roverSpeedMps
|
|
259
|
+
);
|
|
260
|
+
const lookaheadMicros = await ask(
|
|
261
|
+
rl,
|
|
262
|
+
"Lookahead (micros)",
|
|
263
|
+
values.lookaheadMicros ?? defaults.lookaheadMicros
|
|
264
|
+
);
|
|
265
|
+
const updatePeriodMicros = await ask(
|
|
266
|
+
rl,
|
|
267
|
+
"Update period (micros)",
|
|
268
|
+
values.updatePeriodMicros ?? defaults.updatePeriodMicros
|
|
269
|
+
);
|
|
270
|
+
const referenceFrameTimeoutMs = await ask(
|
|
271
|
+
rl,
|
|
272
|
+
"Reference frame timeout (ms)",
|
|
273
|
+
values.referenceFrameTimeoutMs ?? defaults.referenceFrameTimeoutMs
|
|
274
|
+
);
|
|
275
|
+
const referenceFrameMissingMode = await ask(
|
|
276
|
+
rl,
|
|
277
|
+
"Missing frame mode (error/continue)",
|
|
278
|
+
values.referenceFrameMissingMode ?? defaults.referenceFrameMissingMode
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
...values,
|
|
283
|
+
federationName,
|
|
284
|
+
federateName,
|
|
285
|
+
federateType,
|
|
286
|
+
rtiHost,
|
|
287
|
+
rtiPort,
|
|
288
|
+
rtiUseTls,
|
|
289
|
+
parentReferenceFrame,
|
|
290
|
+
roverName,
|
|
291
|
+
roverType,
|
|
292
|
+
roverSpeedMps,
|
|
293
|
+
lookaheadMicros,
|
|
294
|
+
updatePeriodMicros,
|
|
295
|
+
referenceFrameTimeoutMs,
|
|
296
|
+
referenceFrameMissingMode,
|
|
297
|
+
};
|
|
298
|
+
} finally {
|
|
299
|
+
rl.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function ask(
|
|
304
|
+
rl: readline.Interface,
|
|
305
|
+
label: string,
|
|
306
|
+
fallback: string | undefined
|
|
307
|
+
): Promise<string> {
|
|
308
|
+
const resolvedFallback = fallback ?? "";
|
|
309
|
+
const response = await rl.question(`${label} (${resolvedFallback}): `);
|
|
310
|
+
return response.trim() === "" ? resolvedFallback : response.trim();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function askBool(
|
|
314
|
+
rl: readline.Interface,
|
|
315
|
+
label: string,
|
|
316
|
+
fallback: string | undefined
|
|
317
|
+
): Promise<string> {
|
|
318
|
+
const response = await ask(rl, `${label} [y/N]`, fallback === "true" ? "y" : "n");
|
|
319
|
+
const normalized = response.toLowerCase();
|
|
320
|
+
return ["y", "yes", "true", "1"].includes(normalized) ? "true" : "false";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function readValue(argv: string[], index: number, flag: string): string {
|
|
324
|
+
const value = argv[index];
|
|
325
|
+
if (!value) {
|
|
326
|
+
throw new Error(`Missing value for ${flag}`);
|
|
327
|
+
}
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function isTemplateId(value: string): value is TemplateId {
|
|
332
|
+
return value in TEMPLATE_DESCRIPTIONS;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function printHelp(errorMessage?: string): void {
|
|
336
|
+
if (errorMessage) {
|
|
337
|
+
console.error(errorMessage);
|
|
338
|
+
console.error("");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log("Usage:");
|
|
342
|
+
console.log(" spacekit <target-dir> [options]");
|
|
343
|
+
console.log("");
|
|
344
|
+
console.log("Options:");
|
|
345
|
+
console.log(" --template <name> Template to use (default: lunar-rover)");
|
|
346
|
+
console.log(" --list-templates Print the available templates");
|
|
347
|
+
console.log(" --federation-name <name> Default federation name in .env");
|
|
348
|
+
console.log(" --federate-name <name> Default federate name in .env");
|
|
349
|
+
console.log(" --federate-type <type> Default federate type in .env");
|
|
350
|
+
console.log(" --rti-host <host> Default RTI host in .env");
|
|
351
|
+
console.log(" --rti-port <port> Default RTI port in .env");
|
|
352
|
+
console.log(" --rti-use-tls Enable TLS in .env");
|
|
353
|
+
console.log(" --parent-reference-frame <name> Parent frame for the rover entity");
|
|
354
|
+
console.log(" --rover-name <name> Rover object instance name");
|
|
355
|
+
console.log(" --rover-type <type> Rover object type");
|
|
356
|
+
console.log(" --rover-speed-mps <value> Rover speed along the X axis");
|
|
357
|
+
console.log(" --lookahead-micros <value> Spacekit lookahead");
|
|
358
|
+
console.log(" --update-period-micros <value> Spacekit tick period");
|
|
359
|
+
console.log(" --reference-frame-timeout-ms <ms> How long to wait for the frame tree");
|
|
360
|
+
console.log(" --reference-frame-missing-mode <m> error | continue");
|
|
361
|
+
console.log(" --workspace-deps Use workspace:* for @hla4ts/spacekit");
|
|
362
|
+
console.log(" --yes, -y Skip prompts and use defaults/flags");
|
|
363
|
+
console.log(" --help, -h Show this help text");
|
|
364
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { sanitizePackageName, scaffoldApp } from "./scaffold.ts";
|
|
6
|
+
|
|
7
|
+
describe("@hla4ts/create-spacekit", () => {
|
|
8
|
+
test("sanitizePackageName normalizes arbitrary folder names", () => {
|
|
9
|
+
expect(sanitizePackageName("My Lunar Rover!")).toBe("my-lunar-rover");
|
|
10
|
+
expect(sanitizePackageName(" ")).toBe("hla4ts-spacekit-app");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("scaffoldApp creates a configurable lunar rover project", async () => {
|
|
14
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hla4ts-scaffold-"));
|
|
15
|
+
const targetDir = path.join(tempRoot, "mission-rover");
|
|
16
|
+
|
|
17
|
+
await scaffoldApp({
|
|
18
|
+
targetDir,
|
|
19
|
+
values: {
|
|
20
|
+
spacekitDependency: "workspace:*",
|
|
21
|
+
referenceFrameMissingMode: "continue",
|
|
22
|
+
federationName: "MoonOps",
|
|
23
|
+
federateName: "Rover-Blue",
|
|
24
|
+
federateType: "ScoutRover",
|
|
25
|
+
rtiHost: "10.0.0.12",
|
|
26
|
+
roverSpeedMps: "1.25",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const packageJson = await fs.readFile(path.join(targetDir, "package.json"), "utf8");
|
|
31
|
+
const envFile = await fs.readFile(path.join(targetDir, ".env"), "utf8");
|
|
32
|
+
const indexFile = await fs.readFile(path.join(targetDir, "src", "index.ts"), "utf8");
|
|
33
|
+
|
|
34
|
+
expect(packageJson).toContain('"name": "mission-rover"');
|
|
35
|
+
expect(packageJson).toContain('"@hla4ts/spacekit": "workspace:*"');
|
|
36
|
+
expect(envFile).toContain("FEDERATION_NAME=MoonOps");
|
|
37
|
+
expect(envFile).toContain("FEDERATE_NAME=Rover-Blue");
|
|
38
|
+
expect(envFile).toContain("ROVER_NAME=Rover-Blue");
|
|
39
|
+
expect(envFile).toContain("ROVER_TYPE=ScoutRover");
|
|
40
|
+
expect(envFile).toContain("REFERENCE_FRAME_MISSING_MODE=continue");
|
|
41
|
+
expect(envFile).toContain("RTI_HOST=10.0.0.12");
|
|
42
|
+
expect(envFile).toContain("ROVER_SPEED_MPS=1.25");
|
|
43
|
+
expect(indexFile).toContain('import { PhysicalEntity, SpacekitApp');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("scaffoldApp refuses non-empty target directories", async () => {
|
|
47
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hla4ts-scaffold-"));
|
|
48
|
+
const targetDir = path.join(tempRoot, "existing");
|
|
49
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
50
|
+
await fs.writeFile(path.join(targetDir, "keep.txt"), "existing", "utf8");
|
|
51
|
+
|
|
52
|
+
await expect(scaffoldApp({ targetDir })).rejects.toThrow(
|
|
53
|
+
"Refusing to scaffold into a non-empty directory"
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_TEMPLATE = "lunar-rover";
|
|
6
|
+
|
|
7
|
+
export const TEMPLATE_DESCRIPTIONS = {
|
|
8
|
+
"lunar-rover":
|
|
9
|
+
"Minimal Spacekit lunar rover with .env-driven RTI, federation, and timing config.",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type TemplateId = keyof typeof TEMPLATE_DESCRIPTIONS;
|
|
13
|
+
|
|
14
|
+
export interface TemplateValues {
|
|
15
|
+
appName: string;
|
|
16
|
+
packageName: string;
|
|
17
|
+
spacekitDependency: "^0.1.0",
|
|
18
|
+
referenceFrameMissingMode: string;
|
|
19
|
+
federationName: string;
|
|
20
|
+
federateName: string;
|
|
21
|
+
federateType: string;
|
|
22
|
+
rtiHost: string;
|
|
23
|
+
rtiPort: string;
|
|
24
|
+
rtiUseTls: string;
|
|
25
|
+
parentReferenceFrame: string;
|
|
26
|
+
roverName: string;
|
|
27
|
+
roverType: string;
|
|
28
|
+
roverSpeedMps: string;
|
|
29
|
+
lookaheadMicros: string;
|
|
30
|
+
updatePeriodMicros: string;
|
|
31
|
+
referenceFrameTimeoutMs: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ScaffoldAppOptions {
|
|
35
|
+
targetDir: string;
|
|
36
|
+
template?: TemplateId;
|
|
37
|
+
values?: Partial<TemplateValues>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ScaffoldAppResult {
|
|
41
|
+
template: TemplateId;
|
|
42
|
+
targetDir: string;
|
|
43
|
+
packageName: string;
|
|
44
|
+
filesWritten: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function scaffoldApp(options: ScaffoldAppOptions): Promise<ScaffoldAppResult> {
|
|
48
|
+
const template = options.template ?? DEFAULT_TEMPLATE;
|
|
49
|
+
assertTemplate(template);
|
|
50
|
+
|
|
51
|
+
const targetDir = path.resolve(options.targetDir);
|
|
52
|
+
await ensureEmptyTargetDirectory(targetDir);
|
|
53
|
+
|
|
54
|
+
const appName = deriveAppName(targetDir);
|
|
55
|
+
const values = {
|
|
56
|
+
...defaultTemplateValues(appName),
|
|
57
|
+
...(options.values ?? {}),
|
|
58
|
+
};
|
|
59
|
+
values.appName = values.appName.trim() || appName;
|
|
60
|
+
values.packageName = sanitizePackageName(values.packageName || values.appName);
|
|
61
|
+
values.roverName =
|
|
62
|
+
options.values?.roverName === undefined
|
|
63
|
+
? values.federateName
|
|
64
|
+
: values.roverName.trim() || values.federateName;
|
|
65
|
+
values.roverType =
|
|
66
|
+
options.values?.roverType === undefined
|
|
67
|
+
? values.federateType
|
|
68
|
+
: values.roverType.trim() || values.federateType;
|
|
69
|
+
|
|
70
|
+
const templateDir = resolveTemplateDir(template);
|
|
71
|
+
const files = await collectTemplateFiles(templateDir);
|
|
72
|
+
|
|
73
|
+
let filesWritten = 0;
|
|
74
|
+
for (const sourceFile of files) {
|
|
75
|
+
const relativePath = path.relative(templateDir, sourceFile);
|
|
76
|
+
const destinationPath = path.join(targetDir, relativePath);
|
|
77
|
+
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
78
|
+
const content = await fs.readFile(sourceFile, "utf8");
|
|
79
|
+
const rendered = renderTemplate(content, values);
|
|
80
|
+
await fs.writeFile(destinationPath, rendered, "utf8");
|
|
81
|
+
filesWritten += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
template,
|
|
86
|
+
targetDir,
|
|
87
|
+
packageName: values.packageName,
|
|
88
|
+
filesWritten,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function listTemplates(): Array<{ id: TemplateId; description: string }> {
|
|
93
|
+
return (Object.entries(TEMPLATE_DESCRIPTIONS) as Array<[TemplateId, string]>).map(
|
|
94
|
+
([id, description]) => ({ id, description })
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function sanitizePackageName(value: string): string {
|
|
99
|
+
const normalized = value
|
|
100
|
+
.trim()
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
103
|
+
.replace(/^-+|-+$/g, "");
|
|
104
|
+
|
|
105
|
+
return normalized.length > 0 ? normalized : "hla4ts-spacekit-app";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function defaultTemplateValues(appName: string): TemplateValues {
|
|
109
|
+
return {
|
|
110
|
+
appName,
|
|
111
|
+
packageName: sanitizePackageName(appName),
|
|
112
|
+
spacekitDependency: "^0.1.0",
|
|
113
|
+
referenceFrameMissingMode: "continue",
|
|
114
|
+
federationName: "SpaceFederation",
|
|
115
|
+
federateName: "LunarRover-1",
|
|
116
|
+
federateType: "LunarRover",
|
|
117
|
+
rtiHost: "localhost",
|
|
118
|
+
rtiPort: "15164",
|
|
119
|
+
rtiUseTls: "false",
|
|
120
|
+
parentReferenceFrame: "AitkenBasinLocalFixed",
|
|
121
|
+
roverName: "LunarRover-1",
|
|
122
|
+
roverType: "LunarRover",
|
|
123
|
+
roverSpeedMps: "0.5",
|
|
124
|
+
lookaheadMicros: "1000000",
|
|
125
|
+
updatePeriodMicros: "1000000",
|
|
126
|
+
referenceFrameTimeoutMs: "30000",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertTemplate(template: string): asserts template is TemplateId {
|
|
131
|
+
if (!(template in TEMPLATE_DESCRIPTIONS)) {
|
|
132
|
+
throw new Error(`Unknown template "${template}". Run --list-templates to inspect choices.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function ensureEmptyTargetDirectory(targetDir: string): Promise<void> {
|
|
137
|
+
try {
|
|
138
|
+
const stat = await fs.stat(targetDir);
|
|
139
|
+
if (!stat.isDirectory()) {
|
|
140
|
+
throw new Error(`Target path exists and is not a directory: ${targetDir}`);
|
|
141
|
+
}
|
|
142
|
+
const entries = await fs.readdir(targetDir);
|
|
143
|
+
if (entries.length > 0) {
|
|
144
|
+
throw new Error(`Refusing to scaffold into a non-empty directory: ${targetDir}`);
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (isMissing(error)) {
|
|
148
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function collectTemplateFiles(root: string): Promise<string[]> {
|
|
156
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
157
|
+
const files: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const fullPath = path.join(root, entry.name);
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
files.push(...(await collectTemplateFiles(fullPath)));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (entry.isFile()) {
|
|
166
|
+
files.push(fullPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return files.sort();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderTemplate(content: string, values: TemplateValues): string {
|
|
174
|
+
return content.replace(/\{\{([A-Z_]+)\}\}/g, (_match, rawKey: string) => {
|
|
175
|
+
const key = toTemplateValueKey(rawKey);
|
|
176
|
+
return values[key] ?? _match;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function toTemplateValueKey(rawKey: string): keyof TemplateValues {
|
|
181
|
+
const key = rawKey.toLowerCase().replace(/_([a-z])/g, (_match, letter: string) =>
|
|
182
|
+
letter.toUpperCase()
|
|
183
|
+
);
|
|
184
|
+
return key as keyof TemplateValues;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function deriveAppName(targetDir: string): string {
|
|
188
|
+
const base = path.basename(targetDir);
|
|
189
|
+
return base === "." || base.length === 0 ? "hla4ts-spacekit-app" : base;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveTemplateDir(template: TemplateId): string {
|
|
193
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
194
|
+
return path.resolve(path.dirname(currentFile), "..", "templates", template);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isMissing(error: unknown): boolean {
|
|
198
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
199
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FEDERATE_NAME={{FEDERATE_NAME}}
|
|
2
|
+
FEDERATION_NAME={{FEDERATION_NAME}}
|
|
3
|
+
FEDERATE_TYPE={{FEDERATE_TYPE}}
|
|
4
|
+
ROVER_NAME={{ROVER_NAME}}
|
|
5
|
+
ROVER_TYPE={{ROVER_TYPE}}
|
|
6
|
+
PARENT_REFERENCE_FRAME={{PARENT_REFERENCE_FRAME}}
|
|
7
|
+
RTI_HOST={{RTI_HOST}}
|
|
8
|
+
RTI_PORT={{RTI_PORT}}
|
|
9
|
+
RTI_USE_TLS={{RTI_USE_TLS}}
|
|
10
|
+
LOOKAHEAD_MICROS={{LOOKAHEAD_MICROS}}
|
|
11
|
+
UPDATE_PERIOD_MICROS={{UPDATE_PERIOD_MICROS}}
|
|
12
|
+
REFERENCE_FRAME_MISSING_MODE={{REFERENCE_FRAME_MISSING_MODE}}
|
|
13
|
+
REFERENCE_FRAME_TIMEOUT_MS={{REFERENCE_FRAME_TIMEOUT_MS}}
|
|
14
|
+
ROVER_SPEED_MPS={{ROVER_SPEED_MPS}}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
Minimal lunar rover example generated by `spacekit` from `@hla4ts/create-spacekit`.
|
|
4
|
+
|
|
5
|
+
This app uses `@hla4ts/spacekit` directly:
|
|
6
|
+
|
|
7
|
+
- `SpacekitApp.run()` owns the join/bootstrap/tick loop
|
|
8
|
+
- `PhysicalEntity` publishes a single rover object
|
|
9
|
+
- `.env` contains the RTI, federation, rover, and timing defaults
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
- Bun installed
|
|
14
|
+
- an HLA 4 RTI reachable at the configured host/port
|
|
15
|
+
- SpaceMaster and a root reference frame publisher if you are running a SEE late-join federation
|
|
16
|
+
|
|
17
|
+
## Configure
|
|
18
|
+
|
|
19
|
+
Edit `.env` if needed:
|
|
20
|
+
|
|
21
|
+
- `RTI_HOST`, `RTI_PORT`, `RTI_USE_TLS`
|
|
22
|
+
- `FEDERATION_NAME`, `FEDERATE_NAME`, `FEDERATE_TYPE`
|
|
23
|
+
- `ROVER_NAME`, `ROVER_TYPE`, `PARENT_REFERENCE_FRAME`
|
|
24
|
+
- `ROVER_SPEED_MPS`, `LOOKAHEAD_MICROS`, `UPDATE_PERIOD_MICROS`
|
|
25
|
+
- `REFERENCE_FRAME_MISSING_MODE` (`continue` by default)
|
|
26
|
+
|
|
27
|
+
## Run
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun install
|
|
31
|
+
bun run start
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The rover moves forward along the X axis at the configured speed and logs each
|
|
35
|
+
published tick. By default the generated app uses `continue` mode for missing
|
|
36
|
+
reference frames, so it warns and keeps running if the configured parent frame
|
|
37
|
+
has not been published yet.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "bun run src/index.ts",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@hla4ts/spacekit": "{{SPACEKIT_DEPENDENCY}}"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/bun": "latest",
|
|
15
|
+
"typescript": "^5.9.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSpaceFomConfig,
|
|
3
|
+
type MissingReferenceFrameMode,
|
|
4
|
+
type SpacekitAppOptions,
|
|
5
|
+
} from "@hla4ts/spacekit";
|
|
6
|
+
|
|
7
|
+
export interface LunarRoverConfig {
|
|
8
|
+
app: SpacekitAppOptions;
|
|
9
|
+
roverName: string;
|
|
10
|
+
roverType: string;
|
|
11
|
+
roverStatus: string;
|
|
12
|
+
parentReferenceFrame: string;
|
|
13
|
+
roverSpeedMetersPerSec: number;
|
|
14
|
+
startPosition: [number, number, number];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function loadConfig(): LunarRoverConfig {
|
|
18
|
+
const config = createSpaceFomConfig({
|
|
19
|
+
envDefaults: {
|
|
20
|
+
federationName: "{{FEDERATION_NAME}}",
|
|
21
|
+
federateName: "{{FEDERATE_NAME}}",
|
|
22
|
+
federateType: "{{FEDERATE_TYPE}}",
|
|
23
|
+
rtiHost: "{{RTI_HOST}}",
|
|
24
|
+
rtiPort: parseNumber("{{RTI_PORT}}", 15164),
|
|
25
|
+
rtiUseTls: parseBoolean("{{RTI_USE_TLS}}", false),
|
|
26
|
+
lookaheadMicros: parseBigInt("{{LOOKAHEAD_MICROS}}", 1000000n),
|
|
27
|
+
updatePeriodMicros: parseBigInt("{{UPDATE_PERIOD_MICROS}}", 1000000n),
|
|
28
|
+
referenceFrameTimeoutMs: parseNumber("{{REFERENCE_FRAME_TIMEOUT_MS}}", 30000),
|
|
29
|
+
},
|
|
30
|
+
extras: (env) => ({
|
|
31
|
+
roverName: env.ROVER_NAME ?? "{{ROVER_NAME}}",
|
|
32
|
+
roverType: env.ROVER_TYPE ?? "{{ROVER_TYPE}}",
|
|
33
|
+
roverStatus: env.ROVER_STATUS ?? "ACTIVE",
|
|
34
|
+
parentReferenceFrame: env.PARENT_REFERENCE_FRAME ?? "{{PARENT_REFERENCE_FRAME}}",
|
|
35
|
+
roverSpeedMetersPerSec: parseNumber(
|
|
36
|
+
env.ROVER_SPEED_MPS,
|
|
37
|
+
parseNumber("{{ROVER_SPEED_MPS}}", 0.5)
|
|
38
|
+
),
|
|
39
|
+
startPosition: [
|
|
40
|
+
parseNumber(env.ROVER_START_X, 0),
|
|
41
|
+
parseNumber(env.ROVER_START_Y, 0),
|
|
42
|
+
parseNumber(env.ROVER_START_Z, 0),
|
|
43
|
+
] as [number, number, number],
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
config.app.missingReferenceFrameMode = parseReferenceFrameMissingMode(
|
|
47
|
+
process.env.REFERENCE_FRAME_MISSING_MODE ?? "{{REFERENCE_FRAME_MISSING_MODE}}"
|
|
48
|
+
);
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseNumber(value: string | undefined, fallback: number): number {
|
|
53
|
+
if (value === undefined || value.trim() === "") {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
const parsed = Number(value);
|
|
57
|
+
if (!Number.isFinite(parsed)) {
|
|
58
|
+
throw new Error(`Invalid numeric environment value: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseBigInt(value: string | undefined, fallback: bigint): bigint {
|
|
64
|
+
if (value === undefined || value.trim() === "") {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return BigInt(value);
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error(`Invalid bigint environment value: ${value}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
|
|
75
|
+
if (value === undefined || value.trim() === "") {
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
const normalized = value.toLowerCase();
|
|
79
|
+
if (["true", "1", "yes", "y"].includes(normalized)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (["false", "0", "no", "n"].includes(normalized)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Invalid boolean environment value: ${value}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseReferenceFrameMissingMode(
|
|
89
|
+
value: string | undefined
|
|
90
|
+
): MissingReferenceFrameMode {
|
|
91
|
+
if (!value || value.trim() === "") {
|
|
92
|
+
return "continue";
|
|
93
|
+
}
|
|
94
|
+
if (value === "error" || value === "continue") {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Invalid reference frame missing mode: ${value}`);
|
|
98
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PhysicalEntity, SpacekitApp, type SpaceTimeCoordinateState } from "@hla4ts/spacekit";
|
|
2
|
+
import { loadConfig } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
async function main(): Promise<void> {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const app = new SpacekitApp(config.app);
|
|
7
|
+
const rover = new PhysicalEntity(config.roverName, {
|
|
8
|
+
type: config.roverType,
|
|
9
|
+
status: config.roverStatus,
|
|
10
|
+
parent_reference_frame: config.parentReferenceFrame,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
await app.run({
|
|
14
|
+
entities: [rover],
|
|
15
|
+
onTick: ({ initial, tick, timeMicros, timeSeconds, sendTimeMicros }) => {
|
|
16
|
+
if (initial) return;
|
|
17
|
+
|
|
18
|
+
rover.name = config.roverName;
|
|
19
|
+
rover.type = config.roverType;
|
|
20
|
+
rover.status = config.roverStatus;
|
|
21
|
+
rover.parent_reference_frame = config.parentReferenceFrame;
|
|
22
|
+
rover.state = buildRoverState(config, timeSeconds);
|
|
23
|
+
|
|
24
|
+
if (!app.shouldLogTick(tick)) return;
|
|
25
|
+
|
|
26
|
+
app.logger.info("Rover tick.", {
|
|
27
|
+
tick,
|
|
28
|
+
logicalTimeMicros: timeMicros.toString(),
|
|
29
|
+
sendTimeMicros: (sendTimeMicros ?? timeMicros).toString(),
|
|
30
|
+
position: rover.state.translation.position.map((value) => value.toFixed(3)),
|
|
31
|
+
velocity: rover.state.translation.velocity.map((value) => value.toFixed(3)),
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildRoverState(
|
|
38
|
+
config: ReturnType<typeof loadConfig>,
|
|
39
|
+
timeSeconds: number
|
|
40
|
+
): SpaceTimeCoordinateState {
|
|
41
|
+
const [startX, startY, startZ] = config.startPosition;
|
|
42
|
+
const positionX = startX + config.roverSpeedMetersPerSec * timeSeconds;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
translation: {
|
|
46
|
+
position: [positionX, startY, startZ],
|
|
47
|
+
velocity: [config.roverSpeedMetersPerSec, 0, 0],
|
|
48
|
+
},
|
|
49
|
+
rotation: {
|
|
50
|
+
attitude: { scalar: 1, vector: [0, 0, 0] },
|
|
51
|
+
angularVelocity: [0, 0, 0],
|
|
52
|
+
},
|
|
53
|
+
time: timeSeconds,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
void main();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["bun-types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noImplicitReturns": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"allowImportingTsExtensions": true,
|
|
20
|
+
"experimentalDecorators": true,
|
|
21
|
+
"verbatimModuleSyntax": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src/**/*"]
|
|
24
|
+
}
|