'use strict';
const process = require('node:process');
const { setTimeout: sleep } = require('node:timers/promises');
const { isAsyncFunction } = require('node:util/types');
const { AuthClient } = require('./auth');
const channel = require('./channel');
const { DataBufferClient, log_event } = require('./data_buffer');
const { DirectoryClient } = require('./directory');
const { DirectoryRegistrationClient } = require('./directory_registration');
const { EstopClient, is_estopped } = require('./estop');
const { LeaseWallet } = require('./lease');
const { LoggerUtil } = require('./loggerUtil');
const { PayloadRegistrationClient, PayloadNotAuthorizedError } = require('./payload_registration');
const { PowerClient, power_on, power_off, safe_power_off, is_powered_on } = require('./power');
const { RobotCommandClient } = require('./robot_command');
const { RobotIdClient } = require('./robot_id');
const { RobotStateClient, has_arm } = require('./robot_state');
const { TimeSyncThread, TimeSyncClient } = require('./time_sync');
const { TokenCache } = require('./token_cache');
const { TokenManager } = require('./token_manager');
const { timestamp_to_sec } = require('../bosdyn-core/util');
const data_buffer_protos = require('../bosdyn/api/data_buffer_pb');
const _DEFAULT_SECURE_CHANNEL_PORT = 443;
class RobotError extends Error {
constructor(msg) {
super(msg);
this.name = 'RobotError';
}
}
class UnregisteredServiceError extends RobotError {
constructor(msg) {
super(msg);
this.name = 'UnregisteredServiceError';
}
}
class UnregisteredServiceNameError extends UnregisteredServiceError {
constructor(msg, service_name) {
super(`Service name "${service_name}" has not been registered`);
this.name = 'UnregisteredServiceNameError';
this.service_name = service_name;
}
toString() {
return `Service name "${this.service_name}" has not been registered`;
}
}
class UnregisteredServiceTypeError extends UnregisteredServiceError {
constructor(msg, service_type) {
super(`Service name "${service_type}" has not been registered`);
this.name = 'UnregisteredServiceTypeError';
this.service_type = service_type;
}
toString() {
return `Service name "${this.service_type}" has not been registered`;
}
}
/**
* Settings common to one user's access to one robot.
* This is the main point of access to all client functionality.
* The ensure_client member is used to get any client to a service exposed on the robot.
* Additionally, many helpers are exposed to provide commonly used functionality without
* explicitly accessing a particular client object.
* Note that any rpc call made to the robot can raise an RpcError subclass if there are
* errors communicating with the robot.
* Additionally, ResponseErrors will be raised if there was an error acting on the request itself.
* An InvalidRequestError indicates a programming error, where the request was malformed in some way.
* InvalidRequestErrors will never be thrown except in the case of client bugs.
* See also Sdk and BaseClient
*/
class Robot {
constructor(name = null) {
this._name = name;
this.client_name = null;
this.address = null;
this.serial_number = null;
this.logger = LoggerUtil.getLogger(this._name || 'bosdyn.Robot');
this.user_token = null;
this.token_cache = new TokenCache();
this._token_manager = null;
this._current_user = null;
this.service_clients_by_name = {};
this.channels_by_authority = {};
this.authorities_by_name = {};
this._robot_id = null;
this._has_arm = null;
// Things usually updated from an Sdk object.
this.service_client_factories_by_type = {};
this.service_type_by_name = {};
this.request_processors = [];
this.response_processors = [];
this.app_token = null;
this.cert = null;
this.lease_wallet = new LeaseWallet();
this._time_sync_thread = null;
// Set default max message length for sending and receiving. These values are used when creating channels.
this.max_send_message_length = channel.DEFAULT_MAX_MESSAGE_LENGTH;
this.max_receive_message_length = channel.DEFAULT_MAX_MESSAGE_LENGTH;
this._bootstrap_service_authorities = {
[AuthClient.default_service_name]: 'auth.spot.robot',
[DirectoryClient.default_service_name]: 'api.spot.robot',
[DirectoryRegistrationClient.default_service_name]: 'api.spot.robot',
[PayloadRegistrationClient.default_service_name]: 'payload-registration.spot.robot',
[RobotIdClient.default_service_name]: 'id.spot.robot',
};
process.stdin.resume();
process.on('exit', this.#exitHandler.bind(this, { cleanup: true }));
process.on('SIGINT', this.#exitHandler.bind(this, { exit: true }));
}
#exitHandler(options) {
if (options.cleanup) this._shutdown();
if (options.exit) process.exit(0);
}
_shutdown() {
if (this._time_sync_thread) {
console.log('oui time sync');
this._time_sync_thread.stop();
this._time_sync_thread = null;
}
if (this._token_manager) {
console.log('oui token');
this._token_manager.stop();
this._token_manager = null;
}
}
_get_token_id(username) {
return `${this.serial_number}.${username}`;
}
_update_token_cache(username = null) {
this._token_manager = this._token_manager || new TokenManager(this);
this._current_user = username || this._current_user;
if (this._current_user) {
const key = this._get_token_id(this._current_user);
this.token_cache.write(key, this.user_token);
}
}
async setup_token_cache(token_cache = null, unique_id = null) {
this.serial_number = unique_id || this.serial_number || (await this.get_id().getSerialNumber());
this.token_cache = token_cache || this.token_cache;
}
update_from(other = {}) {
this.request_processors = this.request_processors.concat(other.request_processors);
this.response_processors = this.response_processors.concat(other.response_processors);
this.service_client_factories_by_type = Object.assign(
{},
this.service_client_factories_by_type,
other.service_client_factories_by_type,
);
this.service_type_by_name = Object.assign({}, this.service_type_by_name, other.service_type_by_name);
this.cert = other.cert;
this.logger = LoggerUtil.getChild(other.logger, this._name || 'Robot');
this.max_send_message_length = other.max_send_message_length;
this.max_receive_message_length = other.max_receive_message_length;
this.client_name = other.client_name;
this.lease_wallet.set_client_name(this.client_name);
}
async ensure_client(service_name, channelToEnsure = null, options = []) {
if (this.service_clients_by_name[service_name]) return this.service_clients_by_name[service_name];
let service_type;
try {
service_type = this.service_type_by_name[service_name];
} catch (e) {
throw new UnregisteredServiceNameError(null, service_name);
}
let creation_function;
try {
creation_function = this.service_client_factories_by_type[service_type];
} catch (e) {
throw new UnregisteredServiceTypeError(null, service_type);
}
const client = new creation_function();
this.logger.debug(`[ROBOT] Created client for ${service_name}`);
if (channelToEnsure === null) channelToEnsure = await this.ensure_channel(service_name, true, options);
client.channel = channelToEnsure;
// eslint-disable-next-line
isAsyncFunction(client.update_from) ? await client.update_from(this) : client.update_from(this);
this.service_clients_by_name[service_name] = client;
return client;
}
async get_cached_robot_id() {
if (this._robot_id === null) {
const robot_id_client = await this.ensure_client('robot-id');
this._robot_id = await robot_id_client.get_id();
}
return this._robot_id;
}
async _should_send_app_token_on_each_request() {
const robot_id = await this.get_cached_robot_id();
const robot_software_version = robot_id.getSoftwareRelease().getVersion();
if (robot_software_version.getMajorVersion() <= 1 && robot_software_version.getMinorVersion() <= 1) return true;
return false;
}
/**
* Verify the right information exists before calling the ensure_secure_channel method.
* @param {string} service_name Name of the service in the directory.
* @param {boolean} [secure=true] Create a secure channel or not.
* @param {Array} options Options of the grpc channel.
* @returns {Promise<*>|*} Existing channel if found, or newly created channel if not found.
*/
async ensure_channel(service_name, secure = true, options = []) {
const option = options.length ? options.map(x => x[0]) : null;
if (option !== null) {
if (!('grpc.max_receive_message_length' in option[0])) {
options.push({ 'grpc.max_receive_message_length': this.max_receive_message_length });
}
if (!('grpc.max_send_message_length' in option[0])) {
options.push({ 'grpc.max_send_message_length': this.max_send_message_length });
}
}
let authority = this._bootstrap_service_authorities[service_name];
if (!authority) {
authority = this.authorities_by_name[service_name];
if (!authority) {
await this.sync_with_directory();
authority = this.authorities_by_name[service_name];
}
}
if (!authority) throw new UnregisteredServiceNameError(null, service_name);
const skip_app_token_check = service_name === 'robot-id';
return secure
? this.ensure_secure_channel(authority, skip_app_token_check, options)
: this.ensure_insecure_channel(authority, options);
}
async ensure_secure_channel(authority, skip_app_token_check = false, options = []) {
if (authority in this.channels_by_authority) return this.channels_by_authority[authority];
const should_send_app_token = skip_app_token_check ? false : await this._should_send_app_token_on_each_request();
const creds = channel.create_secure_channel_creds(
this.cert,
() => ({ app_token: this.app_token, user_token: this.user_token }),
should_send_app_token,
);
const channelData = channel.create_secure_channel(
this.address,
_DEFAULT_SECURE_CHANNEL_PORT,
creds,
authority,
options,
);
this.logger.debug(
`[ROBOT] Created channel to ${this.address} at port ${_DEFAULT_SECURE_CHANNEL_PORT} with authority ${authority}`,
);
this.channels_by_authority[authority] = channelData;
return channelData;
}
ensure_insecure_channel(authority, options = []) {
if (authority in this.channels_by_authority) return this.channels_by_authority[authority];
const channelData = channel.create_insecure_channel(this.address, _DEFAULT_SECURE_CHANNEL_PORT, authority, options);
this.logger.debug(
`[ROBOT] Created channel to ${this.address} at port ${_DEFAULT_SECURE_CHANNEL_PORT} with authority ${authority}`,
);
this.channels_by_authority[authority] = channelData;
return channelData;
}
async authenticate(username, password, timeout) {
console.log('Pensez à modifier authenticate dans le fichier robot.js');
const default_service_name = AuthClient.default_service_name;
const auth_channel = this.ensure_insecure_channel(this._bootstrap_service_authorities[default_service_name]);
const auth_client = await this.ensure_client(default_service_name, auth_channel);
const user_token = await auth_client.auth(username, password, this.app_token, { timeout });
this.update_user_token(user_token, username);
}
async authenticate_with_token(token, timeout) {
const auth_client = await this.ensure_client(AuthClient.default_service_name);
const user_token = await auth_client.auth_with_token(token, this.app_token, { timeout });
this.update_user_token(user_token);
}
async authenticate_from_cache(username, timeout) {
const token = this.token_cache.read(this._get_token_id(username));
const auth_client = await this.ensure_client(AuthClient.default_service_name);
const user_token = await auth_client.auth_with_token(token, this.app_token, { timeout });
this.update_user_token(user_token, username);
}
async authenticate_from_payload_credentials(guid, secret, payload_registration_client = null, timeout) {
let printed_warning = false;
if (payload_registration_client === null) {
payload_registration_client = await this.ensure_client(PayloadRegistrationClient.default_service_name);
}
let user_token = null;
/* eslint-disable no-await-in-loop */
while (user_token === null) {
try {
user_token = await payload_registration_client.get_payload_auth_token(guid, secret, timeout);
} catch (e) {
if (e instanceof PayloadNotAuthorizedError) {
if (!printed_warning) {
printed_warning = true;
// eslint-disable-next-line
console.log('[ROBOT] Payload is not authorized. Authentication will block until an operator authorizes the payload in the Admin Console.');
}
}
}
await sleep(100);
}
/* eslint-enable no-await-in-loop */
this.update_user_token(user_token);
}
update_user_token(user_token, username = null) {
this.user_token = user_token;
this._update_token_cache(username);
}
get_cached_usernames() {
const matches = this.token_cache.match(this.serial_number);
let usernames = [];
for (const match of matches) {
let username = match.split('.');
usernames.push(username);
}
return usernames.sort();
}
async get_id(id_service_name = RobotIdClient.default_service_name) {
const id_client = await this.ensure_client(id_service_name);
return id_client.get_id();
}
async list_services(
directory_service_name = DirectoryClient.default_service_name,
directory_service_authority = this._bootstrap_service_authorities[DirectoryClient.default_service_name],
) {
const directory_channel = await this.ensure_secure_channel(directory_service_authority);
const dir_client = await this.ensure_client(directory_service_name, directory_channel);
return dir_client.list();
}
async sync_with_directory(
directory_service_name = DirectoryClient.default_service_name,
directory_service_authority = this._bootstrap_service_authorities[DirectoryClient.default_service_name],
) {
const remote_services = await this.list_services(directory_service_name, directory_service_authority);
for (const service of remote_services) {
console.log(service.getName());
this.authorities_by_name[service.getName()] = service.getAuthority();
this.service_type_by_name[service.getName()] = service.getType();
}
return this.service_type_by_name;
}
async register_payload_and_authenticate(payload, secret, timeout = null) {
const payload_registration_client = await this.ensure_client(PayloadRegistrationClient.default_service_name);
try {
await payload_registration_client.register_payload(payload, secret, { timeout });
} catch (e) {
// Pass
}
await this.authenticate_from_payload_credentials(payload.GUID, secret, payload_registration_client, { timeout });
}
async start_time_sync(time_sync_interval_sec = null) {
const client = await this.ensure_client(TimeSyncClient.default_service_name);
if (!this._time_sync_thread) this._time_sync_thread = new TimeSyncThread(client);
if (time_sync_interval_sec) this._time_sync_thread.time_sync_interval_sec = time_sync_interval_sec;
if (this._time_sync_thread.stopped) this._time_sync_thread.start();
}
stop_time_sync() {
if (!this._time_sync_thread.stopped) this._time_sync_thread.stop();
}
get time_sync() {
return new Promise(resolve => {
this.start_time_sync().then(() => {
resolve(this._time_sync_thread);
});
});
}
async time_sec() {
const robot_timestamp = (await this.time_sync).robot_timestamp_from_local_secs(Date.now());
return timestamp_to_sec(robot_timestamp);
}
async operator_comment(comment, timestamp_secs = null, timeout = null) {
const client = await this.ensure_client(DataBufferClient.default_service_name);
let robot_timestamp = null;
if (timestamp_secs === null) {
try {
robot_timestamp = await (await this.time_sync).robot_timestamp_from_local_secs(Date.now());
} catch (e) {
robot_timestamp = null;
}
} else {
robot_timestamp = await (await this.time_sync).robot_timestamp_from_local_secs(timestamp_secs);
}
await client.add_operator_comment(comment, robot_timestamp, { timeout });
}
log_event(
event_type,
level,
description,
start_timestamp_secs,
end_timestamp_secs = null,
id_str = null,
parameters = null,
log_preserve_hint = data_buffer_protos.Event.LogPreserveHint.LOG_PRESERVE_HINT_NORMAL,
) {
return log_event(
this,
event_type,
level,
description,
start_timestamp_secs,
end_timestamp_secs,
id_str,
parameters,
log_preserve_hint,
);
}
async power_on(timeout_msec = 20_000, update_frequency = 1.0, timeout = null) {
const client = await this.ensure_client(PowerClient.default_service_name);
await power_on(client, timeout_msec, update_frequency, { timeout });
}
async power_off(cut_immediately = false, timeout_msec = 20_000, update_frequency = 1.0, timeout = null) {
if (cut_immediately) {
const power_client = await this.ensure_client(PowerClient.default_service_name);
await power_off(power_client, timeout_msec, update_frequency, { timeout });
} else {
const command_client = await this.ensure_client(RobotCommandClient.default_service_name);
const state_client = await this.ensure_client(RobotStateClient.default_service_name);
await safe_power_off(command_client, state_client, timeout_msec, update_frequency, { timeout });
}
}
async is_powered_on(timeout = null) {
const state_client = await this.ensure_client(RobotStateClient.default_service_name);
return is_powered_on(state_client, { timeout });
}
async is_estopped(timeout = null) {
const estop_client = await this.ensure_client(EstopClient.default_service_name);
return is_estopped(estop_client, { timeout });
}
async get_frame_tree_snapshot(timeout = null) {
const client = await this.ensure_client(RobotStateClient.default_service_name);
const current_state = await client.get_robot_state({ timeout });
return current_state.getKinematicState().getTransformsSnapshot();
}
async has_arm(timeout = null) {
if (this._has_arm) return this._has_arm;
const state_client = await this.ensure_client(RobotStateClient.default_service_name);
this._has_arm = has_arm(state_client, timeout);
return this._has_arm;
}
}
module.exports = {
RobotError,
UnregisteredServiceError,
UnregisteredServiceNameError,
UnregisteredServiceTypeError,
Robot,
};
Source