spot-sdk-js
Version:
Develop applications and payloads for Spot using the unofficial Boston Dynamics Spot Node.js SDK.
1,284 lines (1,016 loc) • 48.3 kB
JavaScript
const nj = require('numjs');
const argparse = require('argparse');
const path = require('path');
const {writeFileSync, readFileSync, mkdirSync} = require('fs');
const {InvalidLoginError} = require('./auth');
const {get_self_ip} = require('./common');
const {InvalidAppTokenError, ProxyConnectionError, InvalidRequestError} = require('./exceptions');
const {DataAcquisitionClient} = require('./data_acquisition');
const {acquire_and_process_request} = require('./data_acquisition_helpers');
const {DataBufferClient} = require('./data_buffer');
const {DataServiceClient} = require('./data_service');
const {DirectoryClient, NonexistentServiceError} = require('./directory');
const {DirectoryRegistrationClient, DirectoryRegistrationResponseError} = require('./directory_registration');
const {EstopClient, EstopEndpoint, EstopKeepAlive} = require('./estop');
const {ImageClient, UnknownImageSourceError, ImageResponseError, build_image_request, save_images_as_files} = require('./image');
const {LeaseClient} = require('./lease');
const {LicenseClient} = require('./license');
const {LocalGridClient} = require('./local_grid');
const {
PowerClient,
power_off_robot,
power_cycle_robot,
power_on_payload_ports,
power_off_payload_ports,
power_on_wifi_radio,
power_off_wifi_radio
} = require('./power');
const {PayloadClient} = require('./payload');
const {PayloadRegistrationClient, PayloadAlreadyExistsError} = require('./payload_registration');
const {RobotIdClient} = require('./robot_id');
const {RobotStateClient} = require('./robot_state');
const {SdkError, create_standard_sdk} = require('./sdk');
const {TimeSyncClient, TimeSyncEndpoint, TimeSyncError, timespec_to_robot_timespan} = require('./time_sync');
const {add_common_arguments} = require('./util');
const {TextMessage} = require('../bosdyn/api/data_buffer_pb');
const {EventsCommentsSpec} = require('../bosdyn/api/data_index_pb');
const data_acquisition_pb = require('../bosdyn/api/data_acquisition_pb');
const directory_registration_pb = require('../bosdyn/api/directory_registration_pb');
const payload_pb = require('../bosdyn/api/payload_pb');
const {duration_str, timestamp_to_datetime} = require('../bosdyn-core/util');
class Command {
constructor(subparsers, command_dict, NAME = null, NEED_AUTHENTICATION = true){
this.NAME = NAME;
this.NEED_AUTHENTICATION = NEED_AUTHENTICATION;
command_dict[this.NAME] = this;
this._parser = subparsers.add_parser(this.NAME);
}
run(robot, options){
try{
if(this.NEED_AUTHENTICATION){
robot.authenticate(options.username, options.password);
robot.sync_with_directory();
}
return this._run(robot, options);
}catch(e){
if(e instanceof ProxyConnectionError){
console.error(`Could not contact robot with hostname "${options.hostname}".`);
}else if(e instanceof InvalidAppTokenError){
console.error(`The provided app token "${options.app_token}" is invalid.`);
}else if(e instanceof InvalidLoginError){
console.error(`Username "${options.username}" and/or password are invalid.`);
}else{
console.error(e);
}
}
}
_run(robot, options){
}
};
class Subcommands extends Command {
constructor(subparsers, command_dict, NAME, subcommands){
super(subparsers, command_dict, NAME);
this.NAME = NAME;
this._subcommands = {};
const cmd_subparsers = this._parser.add_subparsers({dest: `${this.NAME}_command`});
cmd_subparsers.required = true;
for(const subcommand of subcommands){
new subcommand(cmd_subparsers, this._subcommands);
}
}
_run(robot, options){
const command_dest = `${this.NAME}_command`;
const subcommand = options[command_dest];
return this._subcommands[subcommand].run(robot, options);
}
};
function _format_dir_entry(name, service_type, authority, tokens, name_width = 23, type_width = 31, authority_width = 27){
console.log(`${name.padEnd(name_width, ' ')} | ${service_type.padEnd(type_width, ' ')} | ${authority.padEnd(authority_width, ' ')} | ${tokens}`);
}
function _token_req_str(entry){
const required = [];
if(entry.getUserTokenRequired()){
required.push('user');
}
if(required.length == 0){
return '';
}
return required.join(', ');
}
async function _show_directory_list(robot, as_proto = false){
const client = await robot.ensure_client(DirectoryClient.default_service_name);
const entries = await client.list();
if(!entries){
console.log("No services found");
return true;
}
if(as_proto){
for(const entry of entries){
console.log(entry.toObject());
}
return true;
}
const max_name_length = entries.length ? Math.max(...entries.map(e => e.getName().length)) : 0;
const max_type_length = entries.length ? Math.max(...entries.map(e => e.getType().length)) : 0;
const max_authority_length = entries.length ? Math.max(...entries.map(e => e.getAuthority().length)) : 0;
_format_dir_entry('name', 'type', 'authority', 'tokens', max_name_length + 4, max_type_length + 4, max_authority_length + 4);
console.log("-".repeat(20 + max_name_length + max_type_length + max_authority_length));
for(const entry of entries){
_format_dir_entry(entry.getName(), entry.getType(), entry.getAuthority(), _token_req_str(entry), max_name_length + 4, max_type_length + 4, max_authority_length + 4);
}
return true;
}
async function _show_directory_entry(robot, service, as_proto = false){
const client = await robot.ensure_client(DirectoryClient.default_service_name);
const entry = await client.get_entry(service);
if(as_proto){
console.log(entry);
}else{
_format_dir_entry('name', 'type', 'authority', 'tokens');
console.log("-".repeat(90))
_format_dir_entry(entry.getName(), entry.getType(), entry.getAuthority(), _token_req_str(entry));
}
return true;
}
class DirectoryCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'dir', [
DirectoryListCommand,
DirectoryGetCommand,
DirectoryRegisterCommand,
DirectoryUnregisterCommand
]);
}
};
class DirectoryListCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'list');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
}
_run(robot, options){
_show_directory_list(robot, options.proto);
return true;
}
};
class DirectoryGetCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'get');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
this._parser.add_argument('service', {help: 'service name to get entry for'});
}
_run(robot, options){
try{
_show_directory_entry(robot, options.service, options.proto);
}catch(e){
console.log(`The requested service name "${options.service}" does not exist. Available services:`);
_show_directory_list(robot, options.proto);
return false;
}
return true;
}
};
class DirectoryRegisterCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'register');
this._parser.add_argument('--service-name', {required: true, help: 'unique name of the service'});
this._parser.add_argument('--service-type', {required: true, help: 'Type of the service, e.g. bosdyn.api.RobotStateService'});
this._parser.add_argument('--service-authority', {required: true, help: 'authority of the service'});
this._parser.add_argument('--service-hostname', {required: true, help: 'hostname of the service computer'});
this._parser.add_argument('--service-port', {required: true, type: 'int', help: 'port the service is running on'});
this._parser.add_argument('--no-user-token',{action: 'store_true', required: false, help: 'disable requirement for user token'});
}
async _run(robot, options){
const directory_registration_client = await robot.ensure_client(DirectoryRegistrationClient.default_service_name);
try{
await directory_registration_client.register(options.service_name, options.service_type, options.service_authority, options.service_hostname, options.service_port, !options.no_user_token);
}catch(e){
console.error(`Failed to register service ${options.service_name}.\nResponse Status: ${directory_registration_pb.RegisterServiceResponse.Status[e.response.getStatus()]}`);
return false;
}
console.log(`Succesfully registered service ${options.service_name}`);
return true;
}
};
class DirectoryUnregisterCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'unregister');
this._parser.add_argument('--service-name', {required: true, help: 'unique name of the service'});
}
async _run(robot, options){
const directory_registration_client = await robot.ensure_client(DirectoryRegistrationClient.default_service_name);
try{
await directory_registration_client.unregister(options.service_name);
}catch(e){
console.error(`Failed to unregister service ${options.service_name}.\nResponse Status: ${directory_registration_pb.UnregisterServiceResponse.Status[e.response.getStatus()]}`);
return false;
}
console.log(`Succesfully unregistered service ${options.service_name}`);
return true;
}
};
class PayloadCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'payload', [PayloadListCommand, PayloadRegisterCommand])
}
};
async function _show_payload_list(robot, as_proto = false){
const client = await robot.ensure_client(PayloadClient.default_service_name);
const payload_protos = await client.list_payloads();
if(!payload_protos){
console.log("No payloads found");
return true;
}
if(as_proto){
for(const payload of payload_protos){
console.log(payload);
}
return true;
}
const out = [];
for(const payload of payload_protos){
out.push({Name: payload.getName(), Description: payload.getDescription(), GUID: payload.getGUID()});
}
console.table(out);
return true;
}
class PayloadListCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'list');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
}
_run(robot, options){
_show_payload_list(robot, options.proto);
return true;
}
};
async function _register_payload(robot, name, guid, secret){
const payload_pb = require('../bosdyn/api/payload_pb');
const payload_registration_client = await robot.ensure_client(PayloadRegistrationClient.default_service_name);
const payload = new payload_pb.Payload().setName(name).setGuid(guid);
let isCatch = false;
try{
await payload_registration_client.register_payload(payload, secret);
}catch(e){
isCatch = true;
console.error('\nA payload with this GUID is already registered. Check the robot Admin Console.');
}
if(!isCatch){
console.log('\nPayload successfully registered with the robot. \nBefore it can be used, the payload must be authorized in the Admin Console.');
}
return true;
}
class PayloadRegisterCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'register');
this._parser.add_argument('--payload-name', {required: true, help: 'name of the payload'});
this._parser.add_argument('--payload-guid', {required: true, help: 'guid of the payload'});
this._parser.add_argument('--payload-secret', {required: true, help: 'secret for the payload'});
}
_run(robot, options){
_register_payload(robot, options.payload_name, options.payload_guid, options.payload_secret);
}
};
class FaultCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'fault', [FaultShowCommand, FaultWatchCommand]);
}
};
async function _show_service_faults(robot){
const robot_state_client = await robot.ensure_client(RobotStateClient.default_service_name);
const service_fault_state = await robot_state_client.get_robot_state().getServiceFaultState();
console.log('\n\n\n' + '-'.repeat(80))
if(service_fault_state.getFaults().length == 0){
console.log("No active service faults.")
return;
}
for(const fault of service_fault_state.getFaults()){
console.log(`${fault.getFaultId().getFaultName()} \nService Name: ${fault.getFaultId().getServiceName()} \nPayload GUID: ${fault.getFaultId().getPayloadGuid()} \nError Message: ${fault.getErrorMessage()} \nOnset Time: ${timestamp_to_datetime(fault.getOnsetTimestamp())}`);
}
return;
}
class FaultShowCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'show');
}
_run(robot, options){
_show_service_faults(robot);
return true;
}
};
class FaultWatchCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'watch');
}
_run(robot, options){
print('Press Ctrl-C or send SIGINT to exit\n\n')
while(true){
_show_service_faults(robot);
await sleep(1000);
}
return true;
}
};
class RobotIdCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'id', false);
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
}
async _run(robot, options){
const client = await robot.ensure_client(RobotIdClient.default_service_name);
const response = await client.get_id();
if(options.proto){
console.log(response.toObject());
return true;
}
let nickname = '';
if(response.getNickname() && response.getNickname() != response.getSerialNumber()){
nickname = response.getNickname();
}
const release = response.getSoftwareRelease();
const version = release.getVersion();
console.log(`\n${response.getSerialNumber().padEnd(20)} ${response.getComputerSerialNumber().padEnd(15)} ${nickname.padEnd(10)} ${response.getSpecies()} (${response.getVersion()})`);
console.log(` Software: ${version.getMajorVersion()}.${version.getMinorVersion()}.${version.getPatchLevel()} (${release.getChangeset()} ${timestamp_to_datetime(release.getChangesetDate())})`);
console.log(` Installed: ${timestamp_to_datetime(release.getInstallDate())}`);
return true;
}
};
class DataBufferCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'log', [TextMsgCommand, OperatorCommentCommand])
}
};
class TextMsgCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'textmsg');
this._parser.add_argument('--timestamp', {action: 'store_true', help: 'achieve time-sync and send timestamp'});
this._parser.add_argument('--tag', {help: 'Tag for message'});
const parser_log_level = this._parser.add_mutually_exclusive_group();
parser_log_level.add_argument('--debug', '-D', {action: 'store_true', help: 'Log at debug-level'});
parser_log_level.add_argument('--info', '-I', {action: 'store_true', help: 'Log at info-level'});
parser_log_level.add_argument('--warn', '-W', {action: 'store_true', help: 'Log at warn-level'});
parser_log_level.add_argument('--error', '-E', {action: 'store_true', help: 'Log at error-level'});
this._parser.add_argument('message', {help: 'Message to log'});
}
async _run(robot, options){
let robot_timestamp = null;
if(options.timestamp){
try{
robot_timestamp = robot.time_sync.robot_timestamp_from_local_secs(Date.now(), timesync_timeout_sec = 1.0);
}catch(e){
console.error("Failed to send message with timestamp: {}.".format(err))
return false;
}
}
const msg_proto = new TextMessage().setMessage(options.message).setTimestamp(robot_timestamp);
if(options.debug){
msg_proto.setLevel(TextMessage.Level.LEVEL_DEBUG);
}else if(options.warn){
msg_proto.setLevel(TextMessage.Level.LEVEL_WARN);
}else if(options.error){
msg_proto.setLevel(TextMessage.Level.LEVEL_ERROR);
}else{
msg_proto.setLevel(TextMessage.Level.LEVEL_INFO);
}
if(options.tag){
msg_proto.setTag(options.tag);
}
const log_client = await robot.ensure_client(LogAnnotationClient.default_service_name);
await log_client.add_text_messages([msg_proto]);
return true;
}
};
class OperatorCommentCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'comment');
this._parser.add_argument('--timestamp', {action: 'store_true', help: 'achieve time-sync and send timestamp'});
this._parser.add_argument('message', {help: 'operator comment text'});
}
_run(robot, options){
let client_timestamp = null;
if(options.timestamp){
client_timestamp = Date.now();
try{
// robot.time_sync.wait_for_sync(1.0);
}catch(e){
console.error(`Failed to get timesync for setting comment timestamp: ${err}.`);
return false;
}
}
robot.operator_comment(options.message, client_timestamp);
return true;
}
};
class DataServiceCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'data', [GetDataBufferCommentsCommand, GetDataBufferEventsCommand, GetDataBufferStatusCommand]);
}
};
class GetDataBufferEventsCommentsCommand extends Command {
constructor(subparsers, command_dict, name = ''){
super(subparsers, command_dict, name);
this._parser.add_argument('--proto', {action: 'store_true', help: 'print in proto format'});
this._parser.add_argument('-T', '--timespan', {help: 'Time span (default all). "1h" (last hour), "10m-5m" (from 10 to 5 minutes ago).'});
this._parser.add_argument('-R', '--robot-time', {action: 'store_true', help: 'Specified timespan is in robot time'});
}
_run(robot, options){
}
pretty_print(values){
}
async _get_result(request_spec, robot, options, get_values_fn){
const client = await robot.ensure_client(DataServiceClient.default_service_name)
if(options.timespan){
let time_sync_endpoint = null;
if(!options.robot_time){
const time_sync_client = await robot.ensure_client(TimeSyncClient.default_service_name);
const time_sync_endpoint = new TimeSyncEndpoint(time_sync_client);
if(!time_sync_endpoint.establish_timesync()){
console.log("Failed to get timesync for requesting comments.");
return false;
}
}
const time_range = timespec_to_robot_timespan(options.timespan, time_sync_endpoint)
request_spec.setTimeRange(time_range);
}
const values = get_values_fn(await client.get_events_comments(request_spec));
if(options.proto){
console.log(values);
}else{
this.pretty_print(values);
}
return true;
}
};
class GetDataBufferCommentsCommand extends GetDataBufferEventsCommentsCommand {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'comments');
}
async _run(robot, options){
const request_spec = new EventsCommentsSpec().setComments(true);
function _get_comments(response){
return response.getEventsComments().getOperatorComments();
}
return await this._get_result(request_spec, robot, options, _get_comments);
}
pretty_print(values){
let last_date_shown = null;
for(const comment of values){
const dtm = new Date(comment.getTimestamp().getSeconds() + comment.getTimestamp().getSeconds() * 1e-9);
if(dtm.getTime() != last_date_shown){
console.log(`\n[${dtm.getTime()}]`);
last_date_shown = dtm.getTime();
}
console.log(` ${dtm.getTime()} ${comment.getMessage()}`);
}
}
};
class GetDataBufferEventsCommand extends GetDataBufferEventsCommentsCommand {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'events');
}
async _run(robot, options){
const request_spec = new EventsCommentsSpec().addEvents();
function _get_events(response){
return response.getEventsComments().getEvents();
}
return await this._get_result(request_spec, robot, options, _get_events);
}
static _level_name(event){
const prefix = 'LEVEL_';
let name = event.Level[event.getLevel()]
if(name.startswith(prefix)) return name.splice(prefix.length);
return name;
}
pretty_print(values){
let last_date_shown = null;
for(const event of values){
const start_secs = event.getStartTime().getSeconds() + event.getStartTime().getSeconds() * 1e-9;
const start_dt = new Date(start_secs);
if(start_dt.getTime() != last_date_shown){
console.log(`\n[${start_dt.getTime()}]`);
last_date_shown = start_dt.getTime();
}
if(event.getEndTime() && event.getEndTime() != event.getStartTime()){
const end_secs = event.getEndTime().getSeconds() + event.getEndTime().getSeconds() * 1e-9;
const end_dt = new Date(end_secs);
console.log(` ${start_dt.getTime()}-${end_dt.getTime()} (END) (${end_secs - start_secs}) ${event.getType()} ${GetDataBufferEventsCommand._level_name(event)} <${event.getSource()}> `);
}else{
const timing = event.getEndTime() ? '' : '(START)';
console.log(` ${start_dt.getTime()} ${timing} ${event.getType()} ${GetDataBufferEventsCommand._level_name(event)} <${event.getSource()}> `);
}
if(event.getDescription()){
console.log(`\t${event.getDescription()}`);
}
}
}
};
class GetDataBufferStatusCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'status');
this._parser.add_argument('--get-blob-specs', '-B', {action: 'store_true', help: 'get list of channel/msgtype/source combinations'});
}
async _run(robot, options){
const client = await robot.ensure_client(DataServiceClient.default_service_name);
const response = await client.get_data_buffer_status(options.get_blob_specs);
console.log(response);
return true;
}
};
class RobotStateCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'state', [FullStateCommand, HardwareConfigurationCommand, MetricsCommand, RobotModel]);
}
};
class FullStateCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'full');
}
async _run(robot, options){
const client = await robot.ensure_client(RobotStateClient.default_service_name);
const response = await client.get_robot_state();
console.log(response);
return true;
}
};
class HardwareConfigurationCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'hardware');
}
async _run(robot, options){
const client = await robot.ensure_client(RobotStateClient.default_service_name);
const response = await client.get_robot_hardware_configuration();
console.log(proto);
return true;
}
};
class RobotModel extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'model', false);
this._parser.add_argument('--outdir', {default: 'Model_Files', help: 'directory into which to save the files'});
}
async _run(robot, options){
const robot_state_client = await robot.ensure_client(RobotStateClient.default_service_name);
const hardware = await robot_state_client.get_robot_hardware_configuration();
const model_directory = options.outdir;
try{
mkdirSync(model_directory);
}catch(e){
}
for(const link in hardware.getSkeleton().getLinks()){
let obj_model_proto;
try{
obj_model_proto = await robot_state_client.get_robot_link_model(link.getName());
}catch(e){
console.error(e);
console.error(" Name of link: " + link.getName());
continue;
}
if(!obj_model_proto.getFileName()){
continue;
}
const sub_path = `${obj_model_proto.getFileName().split('/').slice(0, -1)}`;
const pathName = path.join(model_directory, sub_path);
try{
mkdirSync(pathName);
}catch(e){
}
const path_and_name = path.join(pathName, obj_model_proto.getFileName().split('/').slice(-1));
writeFileSync(path_and_name, obj_model_proto.getFileContents());
console.log('Link file written to ' + path_and_name);
}
writeFileSync(path.join(model_directory, "model.urdf"), hardware.getSkeleton().getUrdf());
console.log('URDF file written to ' + path.join(model_directory, "model.urdf"));
return true;
}
};
class MetricsCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'metrics');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print metrics in proto format'});
}
async _run(robot, options){
const client = await robot.ensure_client(RobotStateClient.default_service_name);
const response = await client.get_robot_metrics();
if(options.proto){
console.log(response.toObject())
return true;
}
for(const metric of response.getMetricsList()){
console.log(MetricsCommand._format_metric(metric));
}
return true;
}
static _secs_to_hms(seconds){
const isecs = parseInt(seconds);
seconds = isecs % 60;
const minutes = (isecs / 60) % 60;
const hours = isecs / 3600;
return `${hours}:${minutes}:${seconds}`;
}
static _distance_str(meters){
if(meters < 1000) return `${meters} m`;
return `${parseFloat(meters) / 1000} km`;
}
static _format_metric(metric){
if(metric.hasFloatValue()){
if(metric.getUnits() == 'm'){
return `${metric.getLabel()} ${MetricsCommand._distance_str(metric.getFloatValue())}`;
}
return `${metric.getLabel()} ${metric.getFloatValue()} ${metric.getUnits()}`;
}else if(metric.hasIntValue()){
return `${metric.getLabel()} ${metric.getIntValue()} ${metric.getUnits()}`;
}else if(metric.hasBoolValue()){
return `${metric.getLabel()} ${metric.getBoolValue()} ${metric.getUnits()}`;
}else if(metric.hasDuration()){
return `${metric.getLabel()} ${MetricsCommand._secs_to_hms(metric.getDuration().getSeconds())}`;
}else if(metric.hasStringValue()){
return `${metric.getLabel()} ${metric.getStringValue()}`;
}
return `${metric.getLabel()} ${metric.getValue()} ${metric.getUnits()}`;
}
};
class TimeSyncCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'time-sync');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
}
async _run(robot, options){
const endpoint = new TimeSyncEndpoint(await robot.ensure_client(TimeSyncClient.default_service_name));
if (!endpoint.establish_timesync(break_on_success = true)){
console.log("Failed to achieve time sync");
return false;
}
if(options.proto){
const response = await endpoint.response;
console.log(response);
return true;
}
console.log(`GRPC round-trip time: ${duration_str(endpoint.round_trip_time)}`);
console.log(`Local time to robot time: ${duration_str(endpoint.clock_skew)}`);
return true;
}
};
class LicenseCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'license');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
this._parser.add_argument(['-f', '--feature-codes'], {nargs: '+', help: 'Optional feature list for GetFeatureEnabled API.'});
}
async _run(robot, options){
const license_client = await robot.ensure_client(LicenseClient.default_service_name);
await this._get_license_info(license_client, options);
await this._get_feature_enabled(license_client, options);
return true;
}
async _get_license_info(license_client, options){
const license = await license_client.get_license_info();
if(options.proto){
console.log(license)
}else{
console.log(license.toObject());
}
}
async _get_feature_enabled(license_client, options){
if(!options.feature_codes || options.feature_codes.length == 0) return;
const feature_enabled = await license_client.get_feature_enabled(options.feature_codes);
for(const feature of feature_enabled){
if(feature_enabled[feature]){
console.log(`Feature ${feature} is enabled.`);
}else{
console.log(`Feature ${feature} is not enabled.`);
}
}
}
};
class LeaseCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'lease', [LeaseListCommand]);
}
};
class LeaseListCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'list');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
}
async _run(robot, options){
const lease_client = await robot.ensure_client(LeaseClient.default_service_name);
const resources = await lease_client.list_leases();
if(options.proto){
console.log(resources)
return true;
}
for(const resource of resources){
console.log(LeaseListCommand._format_lease_resource(resource));
}
return true;
}
static _format_lease_resource(resource){
return resource.hasOwnProperty('toString') ? resource.toString() : String(resource);
}
};
class BecomeEstopCommand extends Command {
_RPC_PRINT_CHOICES = ['timestamp', 'full'];
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'become-estop');
this._parser.add_argument('--timeout', {type: 'float', help: 'EStop timeout (seconds)', default: 10});
this._parser.add_argument('--rpc-print', {choices: this._RPC_PRINT_CHOICES, default: 'timestamp', help: 'How much of the request/response messages to print'});
}
async _run(robot, options){
const client = await robot.ensure_client(EstopClient.default_service_name);
function _timestamp_fmt_request(request){
return `(request timestamp: ${request.getHeader().getRequestTimestamp().toDate().getTime()})`;
}
function _timestamp_fmt_response(response){
return `(response timestamp: ${response.getHeader().getRequestTimestamp().toDate().getTime()})`;
}
if(options.rpc_print == 'timestamp'){
client.request_trim_for_log = _timestamp_fmt_request;
client.response_trim_for_log = _timestamp_fmt_response;
}
const endpoint = new EstopEndpoint(client, 'command-line', options.timeout);
endpoint.force_simple_setup();
// console.log('Press Ctrl-C or send SIGINT to exit');
process.on('SIGINT', () => {
//
});
const keep_alive = new EstopKeepAlive(endpoint);
// Solution temporaire le temps d'avoir un plugins pour executer des threads
setTimeout(() => {
keep_alive.stop();
}, 10000);
endpoint.deregister();
return true;
}
};
class ImageCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'image', [ListImageSourcesCommand, GetImageCommand]);
}
};
async function _show_image_sources_list(robot, as_proto = false, service_name = null){
service_name = service_name || ImageClient.default_service_name;
const client = await robot.ensure_client(service_name);
const response = await client.list_image_sources();
if(as_proto){
console.log(response.toObject());
}else{
for(const image_source of response){
console.log(`${image_source.getName().padEnd(30, ' ')} (${image_source.getRows()}x${image_source.getCols()})`);
}
}
return true;
}
class ListImageSourcesCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'list-sources');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
this._parser.add_argument('--service-name', {default: ImageClient.default_service_name, help: 'Image service to query'});
}
_run(robot, options){
_show_image_sources_list(robot, options.proto, options.service_name);
return true;
}
};
class GetImageCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'get-image');
this._parser.add_argument('--outfile', {default: null, help: 'Filename into which to save the image'});
this._parser.add_argument('--quality-percent', {type: 'int', default: 75, help: 'Percent image quality (0-100)'});
this._parser.add_argument('source_name', {metavar: 'SRC', nargs: '+', help: 'Image source name'});
this._parser.add_argument('--service-name', {help: 'Image service to query', default: ImageClient.default_service_name});
}
async _run(robot, options){
const image_requests = options.source_name.map(x => build_image_request(x, options.quality_percent));
const client = await robot.ensure_client(ImageClient.default_service_name);
let response;
try{
response = await client.get_image(image_requests);
}catch(e){
if(e instanceof UnknownImageSourceError){
console.error(`Requested image source "${options.source_name}" does not exist. Available image sources:`);
_show_image_sources_list(robot, false, options.service_name);
return false;
}else if(e instanceof ImageResponseError){
console.error(`Robot cannot generate the "${options.source_name}" at this time. Retry the command.`);
return false;
}
}
save_images_as_files(response);
return true;
}
};
class LocalGridCommands extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'local_grid', [ListLocalGridTypesCommand, GetLocalGridsCommand]);
}
};
async function _show_local_grid_sources_list(robot, as_proto = false){
const client = await robot.ensure_client(LocalGridClient.default_service_name);
const response = await client.get_local_grid_types();
if(as_proto){
console.log(response.toObject());
}else{
for(const local_grid_type of response){
console.log(local_grid_type.getName());
}
}
return true;
}
class ListLocalGridTypesCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'types');
this._parser.add_argument('--proto', {action: 'store_true', help: 'print listing in proto format'});
}
_run(robot, options){
_show_local_grid_sources_list(robot, options.proto);
return true;
}
};
class GetLocalGridsCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'get');
this._parser.add_argument('--outfile', {default: null, help: 'filename into which to save the image'});
this._parser.add_argument('types', {metavar: 'SRC', nargs: '+', help: 'image types'});
}
async _run(robot, options){
const client = await robot.ensure_client(LocalGridClient.default_service_name);
const response = await client.get_local_grids(options.types);
for(const local_grid_response of response){
console.log(local_grid_response);
}
return true;
}
};
class DataAcquisitionCommand extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'acquire', [DataAcquisitionServiceCommand, DataAcquisitionRequestCommand, DataAcquisitionStatusCommand]);
}
};
class DataAcquisitionRequestCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'request');
this._parser.add_argument('--image-source', {metavar: 'IMG_SRC', default: [], help: 'Image source name', action: 'append'});
this._parser.add_argument('--image-service', {metavar: 'SERVICE_NAME', default: [], help: 'Image service name for the image source.', action: 'append'});
this._parser.add_argument('--data-source', {metavar: 'DATA_SRC', default: [], help: 'Data source name', action: 'append'});
this._parser.add_argument('--action-name', {help: 'The action name to save the data with.', default: "quick_captures"});
this._parser.add_argument('--group-name', {help: 'The group name to save the data with.', default: "command_line"});
this._parser.add_argument('--non-blocking-request', {help: 'Return after making the acquisition request, without monitoring the status for completion.', default: false, action: 'store_true'});
}
async _run(robot, options){
if(!options.data_source && !(options.image_source && options.image_service)){
this._parser.error('A request requires either a data source name or an image source+service name.');
}
if(options.image_source.length != options.image_service.length){
this._parser.error('A request must have a 1:1 correspondence between image source and image service arguments.');
}
const captures = new data_acquisition_pb.AcquisitionRequestList();
captures.setDataCapturesList(options.data_source.map(data_name => new data_acquisition_pb.DataCapture().setName(data_name)));
const img_captures = [];
for(const [i, src_name] of options.image_source.entries()){
const img_service = options.image_service[i];
img_captures.push(new data_acquisition_pb.ImageSourceCapture().setImageService(img_service).setImageSource(src_name));
}
captures.setImageCapturesList(img_captures);
// robot.time_sync.wait_for_sync(1.0);
const data_acquisition_client = await robot.ensure_client(DataAcquisitionClient.default_service_name);
const success = await acquire_and_process_request(data_acquisition_client, captures, options.group_name, options.action_name, null, !options.non_blocking_request);
return success;
}
};
class DataAcquisitionServiceCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'info');
this._data_type_width = 15;
this._data_name_width = 35;
this._service_name_width = 30;
}
async _run(robot, options){
const client = await robot.ensure_client(DataAcquisitionClient.default_service_name);
const response = await client.get_service_info();
console.log("Data Acquisition Service's Available Capabilities\n")
console.log("-".repeat(this._data_type_width + this._data_name_width + this._service_name_width))
const out = [];
for(const data_name of response.getDataSources()){
out.push({Data_Type: "data", Data_Name: data_name.getName(), Service_Name: ""});
}
for(const img_service of response.getImageSourcesList()){
for(const img of img_service.getImageSourceNamesList()){
out.push({Data_Type: "image", Data_Name: img, Service_Name: img_service.getServiceName()});
}
}
console.table(out);
return true;
}
};
class DataAcquisitionStatusCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'status');
this._parser.add_argument('id', {type: 'int', help: 'Response id to get the status for'});
}
async _run(robot, options){
const client = await robot.ensure_client(DataAcquisitionClient.default_service_name);
const response = await client.get_status(options.id);
console.log(response.toObject());
return true;
}
};
class HostComputerIPCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'self-ip', false);
}
_run(robot, options){
console.log(`The IP address of the computer used to talk to the robot is: ${get_self_ip(robot._name)}`)
}
};
class PowerCommand extends Subcommands {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'power', [PowerRobotCommand, PowerPayloadsCommand, PowerWifiRadioCommand]);
}
};
class PowerRobotCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'robot');
this._parser.add_argument('cmd', {choices: ['cycle', 'off']});
}
async _run(robot, options){
// robot.time_sync.wait_for_sync(1.0);
const lease_client = await robot.ensure_client(LeaseClient.default_service_name);
const lease = await lease_client.acquire();
const power_client = await robot.ensure_client(PowerClient.default_service_name);
if(options.cmd == 'cycle'){
power_cycle_robot(power_client)
}else if(options.cmd == 'off'){
power_off_robot(power_client);
}
}
};
class PowerPayloadsCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'payload');
this._parser.add_argument('on_off', {choices: ['on', 'off']});
}
async _run(self, robot, options){
// robot.time_sync.wait_for_sync(1.0);
const lease_client = await robot.ensure_client(LeaseClient.default_service_name);
const lease = await lease_client.acquire();
const power_client = await robot.ensure_client(PowerClient.default_service_name);
if(options.on_off == 'on'){
power_on_payload_ports(power_client);
}else if(options.on_off == 'off'){
power_off_payload_ports(power_client);
}
}
};
class PowerWifiRadioCommand extends Command {
constructor(subparsers, command_dict){
super(subparsers, command_dict, 'wifi');
this._parser.add_argument('on_off', {choices: ['on', 'off']});
}
async _run(self, robot, options){
// robot.time_sync.wait_for_sync(1.0);
const lease_client = await robot.ensure_client(LeaseClient.default_service_name);
const lease = await lease_client.acquire();
const power_client = await robot.ensure_client(PowerClient.default_service_name);
if(options.on_off == 'on'){
power_on_wifi_radio(power_client);
}else if(options.on_off == 'off'){
power_off_wifi_radio(power_client);
}
}
};
function main(args = null){
const parser = argparse.ArgumentParser({prog: 'bosdyn.client', description: 'Command-line interface for interacting with robot services.'});
add_common_arguments(parser);
const command_dict = {};
const subparsers = parser.add_subparsers({title: 'commands', dest: 'command'});
new DirectoryCommands(subparsers, command_dict);
new PayloadCommands(subparsers, command_dict);
new FaultCommands(subparsers, command_dict);
new RobotIdCommand(subparsers, command_dict);
new LicenseCommand(subparsers, command_dict);
new RobotStateCommands(subparsers, command_dict);
new DataBufferCommands(subparsers, command_dict);
new DataServiceCommands(subparsers, command_dict);
new TimeSyncCommand(subparsers, command_dict);
new LeaseCommands(subparsers, command_dict);
new BecomeEstopCommand(subparsers, command_dict);
new ImageCommands(subparsers, command_dict);
new LocalGridCommands(subparsers, command_dict);
new DataAcquisitionCommand(subparsers, command_dict);
new HostComputerIPCommand(subparsers, command_dict);
new PowerCommand(subparsers, command_dict);
const options = (args == null) ? parser.parse_args() : parser.parse_args(args);
const sdk = create_standard_sdk('BosdynClient');
const robot = sdk.create_robot(options.hostname);
if(!options.command){
console.debug('Need to specify a command');
parser.print_help();
return false;
}
if(!command_dict[options.command].run(robot, options)){
return false;
}
return true;
}
module.exports = {
main
};