infinicode 2.8.60 → 2.8.62
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.
|
@@ -74,9 +74,11 @@ Wants=network-online.target
|
|
|
74
74
|
Type=simple
|
|
75
75
|
ExecStart=${service.command} ${service.args.map(shellQuote).join(' ')}
|
|
76
76
|
WorkingDirectory=${service.workingDir ?? homedir()}
|
|
77
|
-
Restart=always
|
|
78
|
-
RestartSec=5
|
|
79
|
-
|
|
77
|
+
Restart=always
|
|
78
|
+
RestartSec=5
|
|
79
|
+
KillMode=control-group
|
|
80
|
+
TimeoutStopSec=10
|
|
81
|
+
${envLines}
|
|
80
82
|
|
|
81
83
|
[Install]
|
|
82
84
|
WantedBy=multi-user.target
|
|
@@ -126,6 +126,8 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
126
126
|
const child = spawn(entry.command, entry.args, {
|
|
127
127
|
env: { ...process.env, ...entry.env, ROBOPARK_ROBOT_RUNTIME: '1' },
|
|
128
128
|
stdio: runtimeLog(`robot-${opts.name}-${entry.label}`, foreground),
|
|
129
|
+
// Give each launcher and its Python descendants one process group.
|
|
130
|
+
detached: process.platform !== 'win32',
|
|
129
131
|
});
|
|
130
132
|
entry.child = child;
|
|
131
133
|
child.on('error', error => console.error(chalk.red(` ${entry.label} failed to start: ${error.message}`)));
|
|
@@ -146,23 +148,34 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
146
148
|
start(entry); }, RESTART_DELAY_MS);
|
|
147
149
|
});
|
|
148
150
|
};
|
|
151
|
+
const signalChild = (entry, signal) => {
|
|
152
|
+
const pid = entry.child?.pid;
|
|
153
|
+
if (!pid)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
if (process.platform === 'win32')
|
|
157
|
+
entry.child?.kill(signal);
|
|
158
|
+
else
|
|
159
|
+
process.kill(-pid, signal);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// It may have exited between the liveness check and signal.
|
|
163
|
+
}
|
|
164
|
+
};
|
|
149
165
|
const stopChild = async (entry) => {
|
|
150
166
|
const child = entry.child;
|
|
151
167
|
if (!child)
|
|
152
168
|
return;
|
|
153
169
|
await new Promise(resolve => {
|
|
154
|
-
const timer = setTimeout(
|
|
170
|
+
const timer = setTimeout(() => {
|
|
171
|
+
signalChild(entry, 'SIGKILL');
|
|
172
|
+
resolve();
|
|
173
|
+
}, 5_000);
|
|
155
174
|
child.once('exit', () => {
|
|
156
175
|
clearTimeout(timer);
|
|
157
176
|
resolve();
|
|
158
177
|
});
|
|
159
|
-
|
|
160
|
-
child.kill('SIGTERM');
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
clearTimeout(timer);
|
|
164
|
-
resolve();
|
|
165
|
-
}
|
|
178
|
+
signalChild(entry, 'SIGTERM');
|
|
166
179
|
});
|
|
167
180
|
};
|
|
168
181
|
const recycleAfterMeshUpdate = async () => {
|
|
@@ -217,7 +230,7 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
217
230
|
const shutdown = () => {
|
|
218
231
|
stopping = true;
|
|
219
232
|
for (const entry of [...children, preview])
|
|
220
|
-
entry
|
|
233
|
+
signalChild(entry, 'SIGTERM');
|
|
221
234
|
};
|
|
222
235
|
process.once('SIGINT', shutdown);
|
|
223
236
|
process.once('SIGTERM', shutdown);
|
package/dist/robopark/setup.js
CHANGED
|
@@ -162,10 +162,10 @@ async function setupRobot(config, opts, ctx, internal) {
|
|
|
162
162
|
// repointed the mesh connection while the scheduler (enroll/heartbeat/
|
|
163
163
|
// preview-agent) silently kept talking to whatever hub auto-discovery
|
|
164
164
|
// found (e.g. a real hub reachable over Tailscale), not the one requested.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
// Keep the scheduler private on the hub. Both LAN and Tailscale robots use
|
|
166
|
+
// the authenticated mesh proxy instead of depending on port 8080 binding,
|
|
167
|
+
// firewall rules, or a separately routable scheduler address.
|
|
168
|
+
const schedulerUrl = `${hubUrl.replace(/\/+$/, '')}/robopark`;
|
|
169
169
|
// Vision (camera/motion detection -> presence-triggered session) is the
|
|
170
170
|
// production trigger — on by default. --no-vision opts out (e.g. no camera
|
|
171
171
|
// on this box, or RoboVisionAI_PI isn't installed).
|
|
@@ -77,6 +77,24 @@ function stopAllUnix() {
|
|
|
77
77
|
const removedUnits = [];
|
|
78
78
|
const errors = [];
|
|
79
79
|
const self = process.pid;
|
|
80
|
+
if (process.platform === 'linux') {
|
|
81
|
+
// Disable restart policies before killing processes so systemd cannot
|
|
82
|
+
// repopulate the ports between process discovery and cleanup.
|
|
83
|
+
const list = spawnSync('systemctl', ['list-units', '--all', '--plain', '--no-legend'], { encoding: 'utf8' });
|
|
84
|
+
const units = (list.stdout ?? '')
|
|
85
|
+
.split('\n')
|
|
86
|
+
.map(l => l.trim().split(/\s+/)[0])
|
|
87
|
+
.filter((u) => !!u && u.endsWith('.service') && /infinicode|robopark/i.test(u));
|
|
88
|
+
for (const unit of units) {
|
|
89
|
+
const disable = spawnSync('systemctl', ['disable', '--now', unit], { encoding: 'utf8' });
|
|
90
|
+
if (disable.status === 0)
|
|
91
|
+
removedUnits.push(unit);
|
|
92
|
+
else
|
|
93
|
+
errors.push(`systemctl disable --now ${unit}: ${(disable.stderr || '').trim()}`);
|
|
94
|
+
}
|
|
95
|
+
if (units.length)
|
|
96
|
+
spawnSync('systemctl', ['daemon-reload'], { encoding: 'utf8' });
|
|
97
|
+
}
|
|
80
98
|
const ps = spawnSync('ps', ['ax', '-o', 'pid=,command='], { encoding: 'utf8' });
|
|
81
99
|
const lines = (ps.stdout ?? '').split('\n');
|
|
82
100
|
const re = new RegExp(PATTERNS.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'));
|
|
@@ -96,7 +114,7 @@ function stopAllUnix() {
|
|
|
96
114
|
}
|
|
97
115
|
// Also free the well-known RoboPark ports directly — belt and suspenders
|
|
98
116
|
// against a supervised process whose command line didn't match a pattern.
|
|
99
|
-
for (const port of [47913, 47921, 47922, 8080, 5000, 5057]) {
|
|
117
|
+
for (const port of [47913, 47921, 47922, 8080, 5000, 5057, 8000]) {
|
|
100
118
|
const lsof = spawnSync('lsof', ['-t', `-i:${port}`], { encoding: 'utf8' });
|
|
101
119
|
const pids = (lsof.stdout ?? '').split('\n').map(s => s.trim()).filter(Boolean).map(Number);
|
|
102
120
|
for (const pid of pids) {
|
|
@@ -112,21 +130,6 @@ function stopAllUnix() {
|
|
|
112
130
|
// include a bare "infinicode.service" (see pi-client install scripts),
|
|
113
131
|
// which a narrower glob would silently leave running (and respawning
|
|
114
132
|
// whatever we just killed, if it has Restart=always).
|
|
115
|
-
const list = spawnSync('systemctl', ['list-units', '--all', '--plain', '--no-legend'], { encoding: 'utf8' });
|
|
116
|
-
const units = (list.stdout ?? '')
|
|
117
|
-
.split('\n')
|
|
118
|
-
.map(l => l.trim().split(/\s+/)[0])
|
|
119
|
-
.filter((u) => !!u && u.endsWith('.service') && /infinicode|robopark/i.test(u));
|
|
120
|
-
for (const unit of units) {
|
|
121
|
-
spawnSync('systemctl', ['stop', unit], { encoding: 'utf8' });
|
|
122
|
-
const disable = spawnSync('systemctl', ['disable', unit], { encoding: 'utf8' });
|
|
123
|
-
if (disable.status === 0)
|
|
124
|
-
removedUnits.push(unit);
|
|
125
|
-
else
|
|
126
|
-
errors.push(`systemctl disable ${unit}: ${(disable.stderr || '').trim()}`);
|
|
127
|
-
}
|
|
128
|
-
if (units.length)
|
|
129
|
-
spawnSync('systemctl', ['daemon-reload'], { encoding: 'utf8' });
|
|
130
133
|
}
|
|
131
134
|
return { killedPids, removedUnits, errors };
|
|
132
135
|
}
|
package/dist/robopark-cli.js
CHANGED
|
@@ -180,13 +180,10 @@ robotProgram
|
|
|
180
180
|
.action(async (opts) => {
|
|
181
181
|
if (opts.lan && opts.tailscale)
|
|
182
182
|
throw new Error('choose exactly one of --lan or --tailscale');
|
|
183
|
-
const hub = new URL(opts.hubUrl);
|
|
184
183
|
await roboparkRobotRuntime({
|
|
185
184
|
name: opts.name,
|
|
186
185
|
hubUrl: opts.hubUrl,
|
|
187
|
-
schedulerUrl: opts.schedulerUrl ??
|
|
188
|
-
? `${opts.hubUrl.replace(/\/+$/, '')}/robopark`
|
|
189
|
-
: `http://${hub.hostname}:8080`),
|
|
186
|
+
schedulerUrl: opts.schedulerUrl ?? `${opts.hubUrl.replace(/\/+$/, '')}/robopark`,
|
|
190
187
|
token: opts.token,
|
|
191
188
|
port: opts.port,
|
|
192
189
|
network: opts.tailscale ? 'tailscale' : 'lan',
|
package/package.json
CHANGED
|
@@ -7,9 +7,10 @@ import os
|
|
|
7
7
|
import sys
|
|
8
8
|
import time
|
|
9
9
|
import numpy as np
|
|
10
|
-
import threading
|
|
11
|
-
import requests
|
|
12
|
-
import base64
|
|
10
|
+
import threading
|
|
11
|
+
import requests
|
|
12
|
+
import base64
|
|
13
|
+
import struct
|
|
13
14
|
|
|
14
15
|
app = Flask(__name__)
|
|
15
16
|
CORS(app)
|
|
@@ -20,6 +21,9 @@ camera = None
|
|
|
20
21
|
audio_input_device = "default"
|
|
21
22
|
audio_output_device = "default"
|
|
22
23
|
ROBOVISION_AUDIO_URL = os.getenv("ROBOVISION_AUDIO_URL", "http://127.0.0.1:8000")
|
|
24
|
+
_camera_inventory_cache = []
|
|
25
|
+
_camera_inventory_cache_at = 0.0
|
|
26
|
+
CAMERA_INVENTORY_CACHE_SECONDS = 30.0
|
|
23
27
|
|
|
24
28
|
def _open_camera(index):
|
|
25
29
|
# On Windows, cv2.VideoCapture(index) with no explicit backend can
|
|
@@ -318,13 +322,53 @@ def caption_mode():
|
|
|
318
322
|
|
|
319
323
|
@app.route('/api/cameras', methods=['GET'])
|
|
320
324
|
def list_cameras():
|
|
325
|
+
global _camera_inventory_cache, _camera_inventory_cache_at
|
|
326
|
+
now = time.monotonic()
|
|
327
|
+
if now - _camera_inventory_cache_at < CAMERA_INVENTORY_CACHE_SECONDS:
|
|
328
|
+
return jsonify({"cameras": _camera_inventory_cache, "current": current_camera_index})
|
|
329
|
+
|
|
321
330
|
available = []
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
331
|
+
if sys.platform.startswith('linux'):
|
|
332
|
+
# Query capabilities without starting a stream. Opening every V4L2
|
|
333
|
+
# node through OpenCV also opens metadata/output nodes and can contend
|
|
334
|
+
# with the camera stream already owned by RoboVision.
|
|
335
|
+
import fcntl
|
|
336
|
+
vidioc_querycap = 0x80685600
|
|
337
|
+
video_capture = 0x00000001
|
|
338
|
+
video_capture_mplane = 0x00001000
|
|
339
|
+
device_caps_flag = 0x80000000
|
|
340
|
+
for candidate in sorted(glob.glob('/dev/video*')):
|
|
341
|
+
fd = None
|
|
342
|
+
try:
|
|
343
|
+
fd = os.open(candidate, os.O_RDONLY | os.O_NONBLOCK)
|
|
344
|
+
capability = bytearray(104)
|
|
345
|
+
fcntl.ioctl(fd, vidioc_querycap, capability, True)
|
|
346
|
+
capabilities = struct.unpack_from('=I', capability, 84)[0]
|
|
347
|
+
device_caps = struct.unpack_from('=I', capability, 88)[0]
|
|
348
|
+
effective = device_caps if capabilities & device_caps_flag else capabilities
|
|
349
|
+
if not effective & (video_capture | video_capture_mplane):
|
|
350
|
+
continue
|
|
351
|
+
card = bytes(capability[16:48]).split(b'\0', 1)[0].decode('utf-8', 'replace')
|
|
352
|
+
available.append({
|
|
353
|
+
"index": candidate,
|
|
354
|
+
"id": candidate,
|
|
355
|
+
"name": card or f"Camera {candidate}",
|
|
356
|
+
"backend": "v4l2",
|
|
357
|
+
})
|
|
358
|
+
except (OSError, ValueError):
|
|
359
|
+
continue
|
|
360
|
+
finally:
|
|
361
|
+
if fd is not None:
|
|
362
|
+
os.close(fd)
|
|
363
|
+
else:
|
|
364
|
+
for candidate in range(4):
|
|
365
|
+
cap = _open_camera(candidate)
|
|
366
|
+
if cap.isOpened():
|
|
367
|
+
available.append({"index": candidate, "id": str(candidate), "name": f"Camera {candidate}", "backend": "dshow"})
|
|
327
368
|
cap.release()
|
|
369
|
+
|
|
370
|
+
_camera_inventory_cache = available
|
|
371
|
+
_camera_inventory_cache_at = now
|
|
328
372
|
return jsonify({"cameras": available, "current": current_camera_index})
|
|
329
373
|
|
|
330
374
|
@app.route('/api/camera/switch', methods=['POST'])
|
|
@@ -374,11 +418,6 @@ def _audio_inventory():
|
|
|
374
418
|
@app.route('/api/media/inventory', methods=['GET'])
|
|
375
419
|
def media_inventory():
|
|
376
420
|
cameras = list_cameras().get_json().get("cameras", [])
|
|
377
|
-
if not cameras and sys.platform != 'win32':
|
|
378
|
-
# The active V4L2 handle may reject a second probe. The node path is
|
|
379
|
-
# still a valid selectable camera and is safer than hiding it.
|
|
380
|
-
cameras = [{"id": path, "index": path, "name": f"Camera {path}", "backend": "v4l2"}
|
|
381
|
-
for path in sorted(glob.glob('/dev/video*'))]
|
|
382
421
|
inputs, outputs, audio_state = _audio_inventory()
|
|
383
422
|
return jsonify({
|
|
384
423
|
"video": [{"id": "auto", "name": "Auto detect"}, {"id": "none", "name": "Disable camera"}] + cameras,
|