robot os intergration v1
This commit is contained in:
@@ -98,6 +98,24 @@ exports.postInit = function (err) {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Optional: rosbridge connector (connects to external rosbridge websocket)
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
var rosbridge = require('./rosbridge');
|
||||||
|
if (rosbridge && typeof rosbridge.init === 'function') {
|
||||||
|
rosbridge.init(this, jibo);
|
||||||
|
if (rlog && typeof rlog.raw === 'function') rlog.raw('[BE] rosbridge connector initialized');
|
||||||
|
if (rlog && typeof rlog.info === 'function') rlog.info('be', 'rosbridge connector initialized');
|
||||||
|
else this.log.info('rosbridge connector initialized');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (rlog && typeof rlog.raw === 'function') rlog.raw('[BE] rosbridge module not present or failed to init (ok)');
|
||||||
|
else this.log.info('rosbridge module not present or failed to init (ok)');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (rlog && typeof rlog.raw === 'function') rlog.raw('[BE] rosbridge init failed: ' + String(e && (e.stack || e.message || e)));
|
||||||
|
this.log.warn('rosbridge init failed (non-fatal):', e.message || e);
|
||||||
|
}
|
||||||
|
|
||||||
jibo.face.views.changeView({ removeAll: true, leaveEmpty: true }, () => {
|
jibo.face.views.changeView({ removeAll: true, leaveEmpty: true }, () => {
|
||||||
this.selectFirstSkill(this.launchFirstSkill.bind(this));
|
this.selectFirstSkill(this.launchFirstSkill.bind(this));
|
||||||
|
|||||||
321
V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/rosbridge.js
Normal file
321
V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/rosbridge.js
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
"use strict";
|
||||||
|
// Simple ROS bridge client for Jibo BE
|
||||||
|
// - Connects to a rosbridge websocket and subscribes to /jibo_remote
|
||||||
|
// - Handles do_enter_rosbridge_skill and do_exit_rosbridge_skill commands
|
||||||
|
|
||||||
|
var WebSocket = null;
|
||||||
|
try { WebSocket = require('ws'); } catch (e) { WebSocket = null; }
|
||||||
|
var urlLib = require('url');
|
||||||
|
|
||||||
|
var DEFAULT_WS = process.env.ROSBRIDGE_WS || 'ws://192.168.1.5:9090';
|
||||||
|
|
||||||
|
var state = {
|
||||||
|
ws: null,
|
||||||
|
subId: null,
|
||||||
|
reconnectTimer: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Robot logger (available on BE runtime)
|
||||||
|
var rlog = null;
|
||||||
|
try {
|
||||||
|
if (typeof global !== 'undefined' && global.__rlog) rlog = global.__rlog;
|
||||||
|
if (!rlog) rlog = require('./robot-logger');
|
||||||
|
} catch (e) { rlog = null; }
|
||||||
|
|
||||||
|
function rlogRaw(s) {
|
||||||
|
try { if (rlog && typeof rlog.raw === 'function') return rlog.raw(String(s || '')); } catch (e) {}
|
||||||
|
try { console.log(String(s || '')); } catch (e) {}
|
||||||
|
}
|
||||||
|
function rlogInfo(tag, text, data) {
|
||||||
|
try {
|
||||||
|
if (rlog && typeof rlog.info === 'function') return rlog.info(String(tag || 'rosbridge'), String(text || ''), data || {});
|
||||||
|
} catch (e) {}
|
||||||
|
try { console.log('[INFO]', String(tag || 'rosbridge'), String(text || ''), data || ''); } catch (e) {}
|
||||||
|
}
|
||||||
|
function rlogWarn(tag, text, data) {
|
||||||
|
try {
|
||||||
|
if (rlog && typeof rlog.warn === 'function') return rlog.warn(String(tag || 'rosbridge'), String(text || ''), data || {});
|
||||||
|
} catch (e) {}
|
||||||
|
try { console.warn('[WARN]', String(tag || 'rosbridge'), String(text || ''), data || ''); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWsUrl(s) {
|
||||||
|
try { return String(s || '').trim(); } catch (e) { return DEFAULT_WS; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWs(obj) {
|
||||||
|
try {
|
||||||
|
if (!state.ws || state.ws.readyState !== 1) {
|
||||||
|
rlogWarn('rosbridge', 'ws not open, drop send', { obj: obj });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var payload = JSON.stringify(obj);
|
||||||
|
rlogInfo('rosbridge', 'ws.send', { payload: obj });
|
||||||
|
state.ws.send(payload);
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(topic, type) {
|
||||||
|
// rosbridge subscribe message
|
||||||
|
var id = 'sub_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
|
||||||
|
state.subId = id;
|
||||||
|
rlogInfo('rosbridge', 'subscribe', { id: id, topic: topic, type: type });
|
||||||
|
sendWs({ op: 'subscribe', id: id, type: type || '', topic: topic });
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsubscribe() {
|
||||||
|
if (!state.subId) return;
|
||||||
|
rlogInfo('rosbridge', 'unsubscribe', { id: state.subId });
|
||||||
|
sendWs({ op: 'unsubscribe', id: state.subId });
|
||||||
|
state.subId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect(wsUrl, onMessage) {
|
||||||
|
var url = parseWsUrl(wsUrl || DEFAULT_WS);
|
||||||
|
rlogInfo('rosbridge', 'connect attempt', { url: url });
|
||||||
|
try {
|
||||||
|
if (WebSocket) {
|
||||||
|
state.ws = new WebSocket(url);
|
||||||
|
} else {
|
||||||
|
// try built-in if ws not present (not ideal)
|
||||||
|
var Ws = require('websocket').w3cwebsocket;
|
||||||
|
state.ws = new Ws(url);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rlogWarn('rosbridge', 'connect failed', { err: String(e) });
|
||||||
|
scheduleReconnect(wsUrl, onMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.ws.onopen = function () {
|
||||||
|
rlogInfo('rosbridge', 'ws open');
|
||||||
|
try {
|
||||||
|
subscribe('/jibo_remote', '/jibo_msgs/JiboRemote');
|
||||||
|
} catch (e) { rlogWarn('rosbridge', 'subscribe failed on open /jibo_remote', { err: String(e) }); }
|
||||||
|
try {
|
||||||
|
subscribe('/jibo', '/jibo_msgs/JiboAction');
|
||||||
|
} catch (e) { rlogWarn('rosbridge', 'subscribe failed on open /jibo', { err: String(e) }); }
|
||||||
|
};
|
||||||
|
|
||||||
|
state.ws.onmessage = function (evt) {
|
||||||
|
rlogRaw('[rosbridge] raw message: ' + (evt && evt.data ? String(evt.data) : ''));
|
||||||
|
var data = null;
|
||||||
|
try { data = JSON.parse(evt.data); } catch (e) { rlogWarn('rosbridge', 'json parse failed', { err: String(e), raw: String(evt && evt.data) }); return; }
|
||||||
|
// rosbridge wraps messages with { op: 'publish', topic: '...', msg: {...} }
|
||||||
|
if (data && data.op === 'publish') {
|
||||||
|
rlogInfo('rosbridge', 'publish received', { topic: data.topic, msg: data.msg });
|
||||||
|
if (data.msg) onMessage && onMessage(data.msg, data.topic);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rlogInfo('rosbridge', 'ws message', { op: data && data.op, data: data });
|
||||||
|
};
|
||||||
|
|
||||||
|
state.ws.onclose = function (ev) { rlogWarn('rosbridge', 'ws closed', { code: ev && ev.code, reason: ev && ev.reason }); scheduleReconnect(wsUrl, onMessage); };
|
||||||
|
state.ws.onerror = function (err) { rlogWarn('rosbridge', 'ws error', { err: String(err) }); };
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect(wsUrl, onMessage) {
|
||||||
|
if (state.reconnectTimer) return;
|
||||||
|
rlogInfo('rosbridge', 'scheduling reconnect', { delayMs: 5000 });
|
||||||
|
state.reconnectTimer = setTimeout(function () {
|
||||||
|
state.reconnectTimer = null;
|
||||||
|
rlogInfo('rosbridge', 'reconnecting now');
|
||||||
|
connect(wsUrl, onMessage);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
rlogInfo('rosbridge', 'close requested');
|
||||||
|
try { unsubscribe(); } catch (e) { rlogWarn('rosbridge', 'unsubscribe failed', { err: String(e) }); }
|
||||||
|
try { if (state.ws) state.ws.close(); } catch (e) { rlogWarn('rosbridge', 'ws close failed', { err: String(e) }); }
|
||||||
|
state.ws = null;
|
||||||
|
if (state.reconnectTimer) { clearTimeout(state.reconnectTimer); state.reconnectTimer = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.init = function (beRuntime, jibo) {
|
||||||
|
// Prefer robot-logger when available
|
||||||
|
var rlog = null;
|
||||||
|
try {
|
||||||
|
if (beRuntime && beRuntime.rlog) rlog = beRuntime.rlog;
|
||||||
|
if (!rlog && typeof global !== 'undefined' && global.__rlog) rlog = global.__rlog;
|
||||||
|
if (!rlog) rlog = require('./robot-logger');
|
||||||
|
} catch (e) {
|
||||||
|
rlog = null;
|
||||||
|
}
|
||||||
|
var log = rlog || (beRuntime && beRuntime.log) || console;
|
||||||
|
var wsUrl = (beRuntime && beRuntime.config && beRuntime.config.rosbridge && beRuntime.config.rosbridge.ws) || DEFAULT_WS;
|
||||||
|
|
||||||
|
function logInfo(text, data) {
|
||||||
|
try {
|
||||||
|
if (rlog && typeof rlog.info === 'function') return rlog.info('rosbridge', String(text || ''), data || {});
|
||||||
|
if (log && typeof log.info === 'function') return log.info(String(text || ''));
|
||||||
|
console.log(String(text || ''));
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
function logWarn(text, data) {
|
||||||
|
try {
|
||||||
|
if (rlog && typeof rlog.warn === 'function') return rlog.warn('rosbridge', String(text || ''), data || {});
|
||||||
|
if (log && typeof log.warn === 'function') return log.warn(String(text || ''));
|
||||||
|
console.warn(String(text || ''));
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMsg(msg, topic) {
|
||||||
|
try {
|
||||||
|
if (msg.do_enter_rosbridge_skill) {
|
||||||
|
logInfo('enter request', msg);
|
||||||
|
// Launch a named skill if provided
|
||||||
|
var skillName = msg.launch_skill || msg.skill || '@be/main-menu';
|
||||||
|
try {
|
||||||
|
// Attempt lifecycle-based redirect for a proper skill switch
|
||||||
|
var path = require('path');
|
||||||
|
var SkillSwitchData = null;
|
||||||
|
try {
|
||||||
|
if (typeof global !== 'undefined' && global && global.be && global.be.constructor) {
|
||||||
|
SkillSwitchData = global.be.constructor.SkillSwitchData;
|
||||||
|
}
|
||||||
|
} catch (e) { SkillSwitchData = null; }
|
||||||
|
function _interop(m) { return (m && (m.__esModule || m.default)) ? (m.default || m) : m; }
|
||||||
|
var SkillSwitchDataCtor = null;
|
||||||
|
if (SkillSwitchData) SkillSwitchDataCtor = _interop(SkillSwitchData);
|
||||||
|
if (!SkillSwitchDataCtor) {
|
||||||
|
try { SkillSwitchDataCtor = require(path.join(jibo.utils.PathUtils.findRoot(), 'SkillSwitchData')); } catch (e) { try { const Root = require(path.join(jibo.utils.PathUtils.findRoot(), 'index.js')); SkillSwitchDataCtor = (Root && (Root.SkillSwitchData || (Root.default && Root.default.SkillSwitchData))) || undefined; } catch (e2) { SkillSwitchDataCtor = null; } }
|
||||||
|
}
|
||||||
|
var skillObj = beRuntime && beRuntime.skills ? beRuntime.skills[skillName] : null;
|
||||||
|
if (skillObj && SkillSwitchDataCtor) {
|
||||||
|
var ssd = new (SkillSwitchDataCtor)(skillObj, {});
|
||||||
|
try { require('./lifecycle').redirect.call(beRuntime, ssd); logInfo('requested skill redirect', { skill: skillName }); } catch (e) { logWarn('skill redirect failed', { err: String(e), skill: skillName }); }
|
||||||
|
} else {
|
||||||
|
logWarn('skill not found or SkillSwitchDataCtor missing', { skill: skillName });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logWarn('enter handling failed', { err: String(e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg.do_exit_rosbridge_skill) {
|
||||||
|
logInfo('exit request', msg);
|
||||||
|
try {
|
||||||
|
// Redirect to idle via lifecycle
|
||||||
|
var path2 = require('path');
|
||||||
|
var SkillSwitchData2 = null;
|
||||||
|
try {
|
||||||
|
if (typeof global !== 'undefined' && global && global.be && global.be.constructor) {
|
||||||
|
SkillSwitchData2 = global.be.constructor.SkillSwitchData;
|
||||||
|
}
|
||||||
|
} catch (e) { SkillSwitchData2 = null; }
|
||||||
|
var SkillSwitchDataCtor2 = SkillSwitchData2 ? _interop(SkillSwitchData2) : null;
|
||||||
|
if (!SkillSwitchDataCtor2) {
|
||||||
|
try { SkillSwitchDataCtor2 = require(path2.join(jibo.utils.PathUtils.findRoot(), 'SkillSwitchData')); } catch (e) { try { const Root = require(path2.join(jibo.utils.PathUtils.findRoot(), 'index.js')); SkillSwitchDataCtor2 = (Root && (Root.SkillSwitchData || (Root.default && Root.default.SkillSwitchData))) || undefined; } catch (e2) { SkillSwitchDataCtor2 = null; } }
|
||||||
|
}
|
||||||
|
var idleSkill = beRuntime && beRuntime.idle ? beRuntime.idle : null;
|
||||||
|
if (idleSkill && SkillSwitchDataCtor2) {
|
||||||
|
var ssd2 = new (SkillSwitchDataCtor2)(idleSkill, {});
|
||||||
|
try { require('./lifecycle').redirect.call(beRuntime, ssd2); logInfo('requested redirect to idle'); } catch (e) { logWarn('idle redirect failed', { err: String(e) }); }
|
||||||
|
} else {
|
||||||
|
logWarn('idle redirect failed - missing idleSkill or ctor');
|
||||||
|
}
|
||||||
|
} catch (e) { logWarn('exit handling failed', { err: String(e) }); }
|
||||||
|
}
|
||||||
|
// Handle /jibo actions (e.g., TTS)
|
||||||
|
if (topic === '/jibo' || topic === 'jibo') {
|
||||||
|
try {
|
||||||
|
if (msg.do_tts || msg.do_tts === true || msg.tts_text) {
|
||||||
|
var t = msg.tts_text || msg.tts || msg.text || '';
|
||||||
|
if (t && t.length) {
|
||||||
|
logInfo('jibo action TTS', { text: t });
|
||||||
|
try {
|
||||||
|
// Normalize: if payload is JSON object string like '{"text":"..."}', extract.
|
||||||
|
try {
|
||||||
|
if (typeof t === 'string' && t.trim().charAt(0) === '{' && t.indexOf('"text"') !== -1) {
|
||||||
|
var parsed = JSON.parse(t);
|
||||||
|
if (parsed && parsed.text) t = parsed.text;
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore parse error */ }
|
||||||
|
|
||||||
|
// Detect ESML/SSML-like input and request SSML mode when present
|
||||||
|
function _isEsml(s) {
|
||||||
|
try {
|
||||||
|
if (!s || typeof s !== 'string') return false;
|
||||||
|
var ls = s.toLowerCase();
|
||||||
|
return ls.indexOf('<es') !== -1 || ls.indexOf('<speak') !== -1 || ls.indexOf('<ssml') !== -1;
|
||||||
|
} catch (e) { return false; }
|
||||||
|
}
|
||||||
|
function _ensureSpeakWrapper(s) {
|
||||||
|
try {
|
||||||
|
if (!s) return s;
|
||||||
|
var trimmed = s.trim();
|
||||||
|
if (trimmed.toLowerCase().indexOf('<speak') === 0) return s;
|
||||||
|
return '<speak>' + s + '</speak>';
|
||||||
|
} catch (e) { return s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
var useEsml = _isEsml(t);
|
||||||
|
if (useEsml && jibo && jibo.tts && typeof jibo.tts.speak === 'function') {
|
||||||
|
var payload = _ensureSpeakWrapper(String(t));
|
||||||
|
jibo.tts.speak(payload, { mode: jibo.tts.TTSMode ? jibo.tts.TTSMode.SSML : undefined });
|
||||||
|
} else if (jibo && jibo.tts && typeof jibo.tts.speak === 'function') {
|
||||||
|
jibo.tts.speak(String(t), { mode: jibo.tts.TTSMode ? jibo.tts.TTSMode.TEXT : undefined });
|
||||||
|
} else if (beRuntime && beRuntime.api && typeof beRuntime.api.speak === 'function') {
|
||||||
|
beRuntime.api.speak({ text: String(t), mode: useEsml ? 'ssml' : 'text' });
|
||||||
|
} else if (jibo && jibo.api && typeof jibo.api.speak === 'function') {
|
||||||
|
jibo.api.speak({ text: String(t), mode: useEsml ? 'ssml' : 'text' });
|
||||||
|
} else {
|
||||||
|
logWarn('no speak API available', { t: t });
|
||||||
|
}
|
||||||
|
} catch (e) { logWarn('tts speak failed', { err: String(e) }); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { logWarn('failed handling /jibo action', { err: String(e), topic: topic, msg: msg }); }
|
||||||
|
}
|
||||||
|
if (msg.tts_text) {
|
||||||
|
try {
|
||||||
|
// Normalize and speak similar to /jibo handling
|
||||||
|
var txt = msg.tts_text;
|
||||||
|
try {
|
||||||
|
if (typeof txt === 'string' && txt.trim().charAt(0) === '{' && txt.indexOf('"text"') !== -1) {
|
||||||
|
var p2 = JSON.parse(txt);
|
||||||
|
if (p2 && p2.text) txt = p2.text;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
// Reuse ESML detection logic
|
||||||
|
function _isEsml2(s) {
|
||||||
|
try {
|
||||||
|
if (!s || typeof s !== 'string') return false;
|
||||||
|
var ls = s.toLowerCase();
|
||||||
|
return ls.indexOf('<es') !== -1 || ls.indexOf('<speak') !== -1 || ls.indexOf('<ssml') !== -1;
|
||||||
|
} catch (e) { return false; }
|
||||||
|
}
|
||||||
|
function _ensureSpeakWrapper2(s) {
|
||||||
|
try {
|
||||||
|
if (!s) return s;
|
||||||
|
var trimmed = s.trim();
|
||||||
|
if (trimmed.toLowerCase().indexOf('<speak') === 0) return s;
|
||||||
|
return '<speak>' + s + '</speak>';
|
||||||
|
} catch (e) { return s; }
|
||||||
|
}
|
||||||
|
var useEsml2 = _isEsml2(txt);
|
||||||
|
if (useEsml2 && jibo && jibo.tts && typeof jibo.tts.speak === 'function') {
|
||||||
|
var payload2 = _ensureSpeakWrapper2(String(txt));
|
||||||
|
jibo.tts.speak(payload2, { mode: jibo.tts.TTSMode ? jibo.tts.TTSMode.SSML : undefined });
|
||||||
|
} else if (jibo && jibo.tts && typeof jibo.tts.speak === 'function') {
|
||||||
|
jibo.tts.speak(String(txt), { mode: jibo.tts.TTSMode ? jibo.tts.TTSMode.TEXT : undefined });
|
||||||
|
} else if (beRuntime && beRuntime.api && typeof beRuntime.api.speak === 'function') {
|
||||||
|
beRuntime.api.speak({ text: String(txt), mode: useEsml2 ? 'ssml' : 'text' });
|
||||||
|
} else if (jibo && jibo.api && typeof jibo.api.speak === 'function') {
|
||||||
|
jibo.api.speak({ text: String(txt), mode: useEsml2 ? 'ssml' : 'text' });
|
||||||
|
} else {
|
||||||
|
logWarn('no speak API available for tts_text', { tts_text: txt });
|
||||||
|
}
|
||||||
|
} catch (e) { logWarn('tts speak failed', { err: String(e) }); }
|
||||||
|
}
|
||||||
|
} catch (e) { logWarn('rosbridge handleMsg error', { err: String(e) }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(wsUrl, handleMsg);
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: close,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.shutdown = function () { close(); };
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"id": "@be/rosbridge",
|
||||||
|
"title": "ROS Bridge",
|
||||||
|
"description": "Connect robot to ROS via rosbridge websocket",
|
||||||
|
"entryPoint": "@be/rosbridge",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user