robotics
Version:
Robotics.dev P2P ROS2 robot controller CLI with ROS telemetry and video streaming
374 lines (331 loc) • 12.3 kB
JavaScript
import {createRequire } from "module";
const require = createRequire(import.meta.url);
import Configstore from 'configstore';
const config = new Configstore('robotics');
var robotId = config.get('ROBOT_ID');
var apiToken = config.get('API_TOKEN');
const chalk = require('chalk');
const clear = require('clear');
const emoji = require('node-emoji');
const argv = require('minimist')(process.argv.slice(2));
const {exec, spawn} = require('child_process');
// const path = require('path');
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const getRobotId = async () => {
robotId = await config.get('ROBOT_ID');
// console.log(chalk.blue(`ROBOT ID: ${robotId}`));
}
const getApiToken = async () => {
apiToken = await config.get('API_TOKEN');
// console.log(chalk.blue(`API TOKEN: ${apiToken}`));
}
const setRobotId = async (rId) => {
const test = await config.set('ROBOT_ID', rId)
robotId = rId
console.log(chalk.blue(`ROBOT ID: ${robotId}`));
}
const setApiToken = async (token) => {
const test = await config.set('API_TOKEN', token)
console.log(chalk.blue(`API TOKEN: ${token}`));
}
const getNewRobotId = async () => {
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
const response = await fetch("https://robotics.dev/new", {
method: "POST",
headers: myHeaders,
})
.then((res) => res.json())
.then((data) => {
// console.log("robotId:", data.id);
setRobotId(data.id);
// config.set('ROBOT_ID', data.id)
// robotId = data.id
})
}
// Fetch robot details
var robotCamera;
var robotCompute;
const fetchRobotDetails = async () => {
try {
// console.log("RealSense Fetch", robotId, apiToken);
if(robotId && apiToken){
const response = await fetch(`https://robotics.dev/robot/${robotId}`, {
headers: {
"Content-Type": "application/json",
"api_token": apiToken
}
});
const data = await response.json();
// console.log('Robot Details:', data);
if(data.camera && data.camera !== ""){
robotCamera = data.camera;
}
if(data.compute && data.compute !== ""){
robotCompute = data.compute;
}
}
} catch (error) {
console.error('Error fetching robot details:', error);
}
}
if (!robotId){
getNewRobotId();
} else {
console.log(chalk.blue(`ROBOT ID: ${robotId}`));
}
clear();
console.log(emoji.get('robot'),chalk.blue('ROBOTICS.DEV'));
// console.log(argv);
function killProcessByName(name){
exec(`pkill -f ${name}`, (error, stdout, stderr) => {
// console.log(chalk.red('Service stopped.'));
if(error){
// console.error(`Error: ${error}`);
return;
}
});
}
//HELP (robotics help)
if(argv._.includes("help")){
console.log(chalk.blue('Robotics commands: '));
console.log(chalk.blue('robotics help'), chalk.green('returns this list of commands'));
console.log(chalk.blue('robotics connect (-s robotics.dev)'), chalk.green('connects robot to cloud/edge via websocket communications'));
console.log(chalk.blue('robotics disconnect'), chalk.green('disconnects robot from cloud or edge servers'));
console.log(chalk.blue('robotics id'), chalk.green("returns robot's id for connecting with cloud or edge servers"));
console.log(chalk.blue('robotics set --token=1234...6789'), chalk.green("sets developer token from robotics.dev API page"));
console.log(chalk.blue('robotics status'), chalk.green('returns list of running services'));
console.log(chalk.blue('robotics start motors (-b rpi (DEFAULT) or -b firmata (Radxa, LattePanda)) (-p 27,22,17,18 (left to right motor pins)) (-e 13,2,12,11 (left to right motor encoder pins)) (-d /dev/ttyACM0 (overrides default device id for radxa and lattepanda))'), chalk.green('starts ROS2 motors on /cmd_vel (enable rosNamespace in portal for multi-robot fleets)'));
console.log(chalk.blue('robotics stop motors'), chalk.green("stops robot's motors"));
console.log(chalk.blue('robotics start camera (-d /dev/video0)'), chalk.green("starts robot's 2d camera stream with device override. Note RealSense RGB device is /dev/video4. -r 672x672 -f 15"));
console.log(chalk.blue('robotics stop camera'), chalk.green("stops robot's 2d camera stream to cloud or edge servers"));
console.log(chalk.blue('robotics start realsense (-p enables pointcloud)'), chalk.green("starts robot's 3d realsense camera"));
console.log(chalk.blue('robotics stop realsense'), chalk.green("stops robot's 3d realsense camera"));
console.log(chalk.blue('robotics monitor'), chalk.green('starts monitoring service to automatically restart comms if it crashes'));
console.log(chalk.blue('robotics stop monitor'), chalk.green('stops the monitoring service'));
}
// CONNECT (robotics connect -s http://192.168.0.6:3001)
if(argv._.includes("connect")){
const filePath = path.join(__dirname, 'comms.js');
var options = [filePath]
if(argv.s){
options.push('--server')
options.push(argv.s.toLowerCase())
}
// Add memory limit and garbage collection flags
options.unshift('--max-old-space-size=1024'); // 1GB memory limit
options.unshift('--expose-gc'); // Enable garbage collection
function startCommsProcess() {
var commsProcess = spawn('node', options, {
detached: true,
stdio: 'ignore'
});
commsProcess.on('error', (error) => {
console.error(chalk.red(`Comms process error: ${error}`));
setTimeout(startCommsProcess, 5000); // Restart after 5 seconds
});
commsProcess.on('exit', (code, signal) => {
if (code !== 0) {
console.log(chalk.yellow(`Comms process exited with code ${code}, restarting in 5 seconds...`));
setTimeout(startCommsProcess, 5000); // Restart after 5 seconds
}
});
return commsProcess;
}
var commsProcess = startCommsProcess();
console.log(chalk.green('Robot connected.'));
commsProcess.unref();
if(robotId){
console.log(chalk.blue(`ROBOT ID: ${robotId}`));
// process.exit(0);
}
}
// DISCONNECT (robotics disconnect)
if(argv._.includes("disconnect")){
killProcessByName('comms.js');
console.log(chalk.red('Robot disconnected.'));
process.exit(0);
}
// ID
if(argv._.includes("id")){
console.log(chalk.blue(`ROBOT ID: ${config.get('ROBOT_ID')}`));
if(robotId){
// console.log(chalk.blue(`ROBOT ID: ${robotId}`));
process.exit(0);
}
}
// SET - robotics set --token=123 || robotics set --id=123
if(argv._.includes("set")){
if(argv.token){
setApiToken(argv.token);
}
if(argv.id){
setRobotId(argv.id);
}
process.exit(0);
}
// STATUS
if(argv._.includes("status")){
exec(`ps -aux | grep comms.js`, (err, stdout, stdin) => {
if (err) throw err;
console.log(stdout);
});
exec(`ps -aux | grep motors-rpi5.js`, (err, stdout, stdin) => {
if (err) throw err;
console.log(stdout);
});
exec(`ps -aux | grep motors-firmata.js`, (err, stdout, stdin) => {
if (err) throw err;
console.log(stdout);
});
exec(`ps -aux | grep camera-2d-ros.js`, (err, stdout, stdin) => {
if (err) throw err;
console.log(stdout);
});
// process.exit(0);
}
// MOTORS START (robotics motors start || robotics start motors -b rpi -p 27,22,17,18)
if(argv._.includes("motors") && argv._.includes("start")){
var filePath1 = path.join(__dirname, 'motors-rpi5.js');
// var filePath1 = path.join(__dirname, 'motors-rpi5-odom.js');
var filePath2 = path.join(__dirname, 'motors-firmata.js');
await getRobotId();
await getApiToken();
await fetchRobotDetails();
var options;
if(robotCompute === 'Raspberry Pi'){
options = [filePath1];
} else {
options = [filePath2];
}
if(argv.b && argv.b === 'rpi'){
options = [filePath1];
} else if(argv.b && argv.b === 'firmata'){
options = [filePath2]
// options = ['motors-firmata.js'];
}
if(argv.d){
options.push('--device')
options.push(argv.d)
}
if(argv.p){
options.push('--pins')
options.push(argv.p)
}
if(argv.e){
options.push('--encoderpins')
options.push(argv.e)
}
var commsProcess = spawn('node', options, {
detached: true,
stdio: 'ignore'
});
console.log(chalk.green('Robot motors started.'));
speak('Robot motors started.');
commsProcess.unref();
process.exit(0);
}
// MOTORS STOP (robotics motors stop || robotics stop motors)
if(argv._.includes("motors") && argv._.includes("stop")){
killProcessByName('motors-rpi5.js');
killProcessByName('motors-firmata.js');
console.log(chalk.red('Robot motors stopped.'));
speak('Robot motors stopped.')
process.exit(0);
}
// CAMERA START (robotics camera start || robotics start camera -s http://192.168.0.6:3001 -d /dev/video0)
if(argv._.includes("camera") && argv._.includes("start")){
const filePath = path.join(__dirname, 'camera-2d-ros.js');
var options = [filePath]
// if(argv.s){
// options.push('--server')
// options.push(argv.s.toLowerCase())
// }
if(argv.d){
options.push('--device')
options.push(argv.d)
}
if(argv.r){
options.push('--resolution')
options.push(argv.r)
}
if(argv.f){
options.push('--fps')
options.push(argv.f)
}
var commsProcess = spawn('node', options, {
detached: true,
stdio: 'ignore'
});
console.log(chalk.green('Robot camera started.'));
speak('Robot camera started.');
commsProcess.unref();
process.exit(0);
}
// CAMERA STOP (robotics camera stop || robotics stop camera)
if(argv._.includes("camera") && argv._.includes("stop")){
killProcessByName('camera-2d-ros.js');
console.log(chalk.red('Robot camera stopped.'));
speak('Robot camera stopped.');
process.exit(0);
}
// REALSENSE START (robotics realsense start || robotics start realsense -p enables pointcloud)
if(argv._.includes("realsense") && argv._.includes("start")){
await getRobotId();
await getApiToken();
await fetchRobotDetails();
// const filePath = path.join(__dirname, 'camera-2d-ros.js');
// var options = [filePath]
// var options = ['launch', 'realsense2_camera', 'rs_launch.py'];
//ros2 launch realsense2_camera rs_launch.py depth_module.depth_profile:=640x480x6 depth_module.infra_profile:=640x480x6 rgb_camera.color_profile:=640x480x6 enable_rgbd:=true enable_sync:=true align_depth.enable:=true enable_color:=true enable_depth:=true
var options;
if(robotCamera === 'D421'){
options = ['launch', 'realsense2_camera', 'rs_launch.py', 'depth_module.depth_profile:=424x240x6', 'depth_module.infra_profile:=424x240x6', 'enable_depth:=true'];
} else {
options = ['launch', 'realsense2_camera', 'rs_launch.py', 'depth_module.depth_profile:=424x240x6', 'depth_module.infra_profile:=424x240x6','rgb_camera.color_profile:=320x180x6', 'enable_rgbd:=true', 'enable_color:=true', 'enable_depth:=true', 'enable_accel:=true', 'enable_gyro:=true', 'unite_imu_method:=2'];
}
if(argv.p){
options.push('pointcloud.enable:=true')
}
var commsProcess = spawn('ros2', options, {
detached: true,
stdio: 'ignore'
});
console.log(chalk.green(`Robot realsense started on ${robotCamera} camera`));
speak(`Robot realsense started on ${robotCamera} camera`);
commsProcess.unref();
process.exit(0);
}
// CAMERA STOP (robotics camera stop || robotics stop camera)
if(argv._.includes("realsense") && argv._.includes("stop")){
killProcessByName('realsense2_camera_node');
console.log(chalk.red('Robot realsense stopped.'));
speak('Robot realsense stopped.');
process.exit(0);
}
// MONITOR START (robotics monitor)
if(argv._.includes("monitor") && !argv._.includes("stop")){
const filePath = path.join(__dirname, 'monitor-comms.js');
var options = [filePath];
var monitorProcess = spawn('node', options, {
detached: true,
stdio: 'ignore'
});
console.log(chalk.green('Monitoring service started.'));
monitorProcess.unref();
process.exit(0);
}
// MONITOR STOP (robotics stop monitor)
if(argv._.includes("monitor") && argv._.includes("stop")){
killProcessByName('monitor-comms.js');
console.log(chalk.red('Monitoring service stopped.'));
process.exit(0);
}
//speak("Initializing robot.");
function speak(msg){
exec(`espeak "${msg}"`)
}