From f6e5f2db36a3c62784f1070dbbdcb2f5a1b6f44b Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 24 Mar 2026 22:20:38 +0200 Subject: [PATCH] robot os intergration v1 --- .../jibo/Jibo/Skills/@be/be/be/postinit.js | 18 + .../jibo/Jibo/Skills/@be/be/be/rosbridge.js | 321 ++++++++++++++++++ .../be/menu/menus/rosbridge-menu-entry.json | 7 + 3 files changed, 346 insertions(+) create mode 100644 V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/rosbridge.js create mode 100644 V3.1/build/opt/jibo/Jibo/Skills/@be/be/menu/menus/rosbridge-menu-entry.json diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/postinit.js b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/postinit.js index 5532e3a4..82316e1d 100644 --- a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/postinit.js +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/postinit.js @@ -98,6 +98,24 @@ exports.postInit = function (err) { // 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 }, () => { this.selectFirstSkill(this.launchFirstSkill.bind(this)); diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/rosbridge.js b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/rosbridge.js new file mode 100644 index 00000000..dc2c9d55 --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/be/rosbridge.js @@ -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('' + s + ''; + } 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('' + s + ''; + } 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(); }; diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/menu/menus/rosbridge-menu-entry.json b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/menu/menus/rosbridge-menu-entry.json new file mode 100644 index 00000000..a2837c08 --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/menu/menus/rosbridge-menu-entry.json @@ -0,0 +1,7 @@ +{ + "id": "@be/rosbridge", + "title": "ROS Bridge", + "description": "Connect robot to ROS via rosbridge websocket", + "entryPoint": "@be/rosbridge", + "hidden": true +}