#!/usr/bin/env node /** * WL-PRO 轮组机器人 遥控脚本(无外部依赖,Node >= 22 自带 WebSocket) * * 控制通道与网页一致的协议:ws://:81/ 发送 JSON 状态帧: * {dir, mode, roll, height, linear, angular, stable, joy_x, joy_y} * dir: stop | forward | back | left | right | jump * * 用法示例: * node robot-ctl.js --host 203.0.113.10 --port 81 stop * node robot-ctl.js --host 192.168.4.1 forward --ms 800 * node robot-ctl.js jump # 原地跳一下 * node robot-ctl.js joy 0 100 --ms 1200 # 摇杆:x=0 y=100 持续 1.2s * node robot-ctl.js height 60 # 站高 60mm(32~85) * node robot-ctl.js roll 10 # 横滚 10°(-30~30) * node robot-ctl.js linear 100 --ms 500 # 线速度 100mm/s * node robot-ctl.js seq "height 50; forward 800; stop" * node robot-ctl.js probe # 只连接并打印收到的帧 * node robot-ctl.js raw '{"dir":"jump"}' * * 通用参数:--host --port --timeout --rate --wet * --wet:允许真正发出动作(不写它也能动,仅为显式确认用法保留) */ const DEFAULT_STATE = { roll: 0, height: 38, linear: 0, angular: 0, stable: 0, mode: 'basic', dir: 'stop', joy_x: 0, joy_y: 0, }; function parseArgs(argv) { const opts = { host: '192.168.4.1', port: 81, timeout: 6000, rate: 150, ms: null, wet: false }; const tokens = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--host') opts.host = argv[++i]; else if (a === '--port') opts.port = Number(argv[++i]); else if (a === '--timeout') opts.timeout = Number(argv[++i]); else if (a === '--rate') opts.rate = Number(argv[++i]); else if (a === '--ms') opts.ms = Number(argv[++i]); else if (a === '--wet') opts.wet = true; else tokens.push(a); } opts.cmd = tokens[0]; opts.args = tokens.slice(1); return opts; } class Robot { constructor(opts) { this.opts = opts; this.state = { ...DEFAULT_STATE }; this.ws = null; this.log = (...m) => console.log('[robot]', ...m); } url() { return `ws://${this.opts.host}:${this.opts.port}/`; } connect() { if (typeof WebSocket === 'undefined') { return Promise.reject(new Error('当前 Node 无全局 WebSocket,需要 Node >= 22')); } return new Promise((resolve, reject) => { const ws = new WebSocket(this.url()); this.ws = ws; const timer = setTimeout(() => { try { ws.close(); } catch {} reject(new Error(`连接超时 ${this.url()}`)); }, this.opts.timeout); ws.onopen = () => { clearTimeout(timer); this.log('已连接', this.url()); resolve(); }; ws.onerror = (e) => { clearTimeout(timer); reject(new Error(`连接失败 ${this.url()} (${(e && (e.message || e.type)) || 'error'})`)); }; ws.onclose = (e) => { this.log('连接关闭', e?.code ?? '', e?.reason ?? ''); }; ws.onmessage = (m) => { const s = typeof m.data === 'string' ? m.data : '[binary]'; this.log('收到:', s.slice(0, 400)); }; }); } send(extra = {}) { if (!this.ws || this.ws.readyState !== 1) throw new Error('未连接'); Object.assign(this.state, extra); const frame = { dir: this.state.dir, mode: this.state.mode, roll: this.state.roll, height: this.state.height, linear: this.state.linear, angular: this.state.angular, stable: this.state.stable, joy_x: this.state.joy_x, joy_y: this.state.joy_y, }; this.ws.send(JSON.stringify(frame)); return frame; } sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } /** 推一段时间状态帧(用于持续动作/摇杆) */ async stream(extra, ms, rate) { const step = Math.max(30, rate || this.opts.rate); const t0 = Date.now(); this.send(extra); while (Date.now() - t0 < ms) { await this.sleep(Math.min(step, Math.max(0, ms - (Date.now() - t0)))); if (Date.now() - t0 >= ms) break; this.send(extra); } } stop() { if (this.ws && this.ws.readyState === 1) { this.send({ dir: 'stop', joy_x: 0, joy_y: 0 }); } } close() { try { this.stop(); } catch {} try { if (this.ws) this.ws.close(); } catch {} } } const DIRS = new Set(['forward', 'back', 'left', 'right', 'jump']); async function runOne(robot, cmd, args, opts) { const num = (v, d) => (v === undefined || v === '' ? d : Number(v)); switch (cmd) { case 'stop': robot.send({ dir: 'stop', joy_x: 0, joy_y: 0 }); robot.log('停止'); return; case 'probe': { const ms = Number(args[0] ?? 3000); robot.send({ dir: 'stop' }); robot.log(`已同步状态,监听 ${ms}ms 内机器人回传…`); await robot.sleep(ms); return; } case 'raw': { const json = args.join(' '); const obj = JSON.parse(json); const frame = robot.send(obj); robot.log('已发:', JSON.stringify(frame)); return; } case 'height': robot.send({ height: num(args[0], 38) }); robot.log(`站高 = ${robot.state.height}mm`); return; case 'roll': robot.send({ roll: num(args[0], 0) }); robot.log(`横滚 = ${robot.state.roll}°`); return; case 'linear': case 'angular': { const v = num(args[0], 0); const ms = opts.ms ?? num(args[1], 800); await robot.stream({ [cmd]: v }, ms, opts.rate); robot.send({ [cmd]: 0 }); robot.log(`${cmd} = ${v} 持续 ${ms}ms 后归零`); return; } case 'stable': robot.send({ stable: num(args[0], 0) ? 1 : 0 }); robot.log(`stable = ${robot.state.stable}`); return; case 'joy': { const x = num(args[0], 0), y = num(args[1], 0); const ms = opts.ms ?? num(args[2], 800); await robot.stream({ joy_x: x, joy_y: y }, ms, opts.rate); robot.send({ joy_x: 0, joy_y: 0, dir: 'stop' }); robot.log(`摇杆 x=${x} y=${y} 持续 ${ms}ms 后回中`); return; } case 'seq': { const script = args.join(' '); for (const part of script.split(';').map((s) => s.trim()).filter(Boolean)) { const seg = part.split(/\s+/); await runOne(robot, seg[0], seg.slice(1), opts); await robot.sleep(120); } return; } default: { if (DIRS.has(cmd)) { const ms = opts.ms ?? (cmd === 'jump' ? 300 : 800); await robot.stream({ dir: cmd }, ms, opts.rate); robot.send({ dir: 'stop' }); robot.log(`${cmd} 持续 ${ms}ms 后停止`); return; } throw new Error(`未知命令: ${cmd}(可用: stop forward back left right jump joy height roll linear angular stable seq raw probe)`); } } } (async () => { const opts = parseArgs(process.argv.slice(2)); if (!opts.cmd) { console.log('用法: node robot-ctl.js [--host IP] [--port 81] [--ms N] <命令> [参数…]'); console.log('命令: stop | forward | back | left | right | jump | joy [ms] |'); console.log(' height | roll | linear [ms] | angular [ms] |'); console.log(' stable <0|1> | seq "; " | raw | probe [ms]'); process.exit(1); } const robot = new Robot(opts); let code = 0; const cleanup = () => { try { robot.close(); } catch {} }; process.on('SIGINT', () => { cleanup(); process.exit(130); }); process.on('uncaughtException', (e) => { console.error('[robot] 错误:', e.message); cleanup(); process.exit(1); }); try { await robot.connect(); await runOne(robot, opts.cmd, opts.args, opts); await robot.sleep(120); } catch (e) { console.error('[robot] 失败:', e.message); code = 1; } finally { cleanup(); await new Promise((r) => setTimeout(r, 150)); process.exit(code); } })();