spot-sdk-js
Version:
Develop applications and payloads for Spot using the unofficial Boston Dynamics Spot Node.js SDK.
424 lines (354 loc) • 15.9 kB
JavaScript
const {AuthClient} = require('./auth');
const channel = require('./channel');
const {DataBufferClient} = require('./data_buffer');
const {DataServiceClient} = require('./data_service');
const {DirectoryClient} = require('./directory');
const {DirectoryRegistrationClient} = require('./directory_registration');
const {EstopClient, is_estopped} = require('./estop');
const {LeaseWallet} = require('./lease');
const {PayloadRegistrationClient, PayloadAlreadyExistsError, 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, TimeSyncError} = require('./time_sync');
const {TokenManager} = require('./token_manager');
const {TokenCache} = require('./token_cache');
const {timestamp_to_sec} = require('../bosdyn-core/util');
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`;
}
};
function sleep(period) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, period);
});
}
class Robot {
constructor(name = null){
this._name = name;
this.client_name = null;
this.address = null;
this.serial_number = null;
this.logger = console;
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;
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',
};
}
_shutdown(){
if(this._time_sync_thread){
this._time_sync_thread.stop();
this._time_sync_thread = null;
}
if(this._token_manager){
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 = other.logger;
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, channel = 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(channel == null) channel = await this.ensure_channel(service_name, true, options);
client.channel = channel;
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;
}
async ensure_channel(service_name, secure = true, options = []){
let 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 ? await this.ensure_secure_channel(authority, skip_app_token_check, options) : this.ensure_insecure_channel(authority, skip_app_token_check, 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, () => { return {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, skip_app_token_check = false, 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 = null){
console.log('Pensez à modifier authenticate dans le fichier robot.js');
const default_service_name = AuthClient.default_service_name;
const auth_channel = await 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 = null){
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 = null){
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 = null){
let printed_warning = false;
if(payload_registration_client == null){
payload_registration_client = await this.ensure_client(PayloadRegistrationClient.default_service_name);
}
let user_token = null;
while(user_token == null){
try{
user_token = await payload_registration_client.get_payload_auth_token(guid, secret, timeout);
}catch(e){
if (!printed_warning){
printed_warning = true;
console.log('[ROBOT] Payload is not authorized. Authentication will block until an operator authorizes the payload in the Admin Console.');
}
}
await sleep(100);
}
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 await 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 await 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){
this.authorities_by_name[service.name] = service.authority;
this.service_type_by_name[service.name] = service.type;
}
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){
}
await this.authenticate_from_payload_credentials(payload.GUID, secret, payload_registration_client, {timeout});
}
async start_time_sync(time_sync_interval_sec = null){
if(!this._time_sync_thread) this._time_sync_thread = TimeSyncThread(await this.ensure_client(TimeSyncClient.default_service_name));
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(){
this.start_time_sync()
return this._time_sync_thread;
}
time_sec(){
const robot_timestamp = 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 = this.time_sync.robot_timestamp_from_local_secs(Date.now());
}catch(e){
robot_timestamp = null;
}
}else{
robot_timestamp = this.time_sync.robot_timestamp_from_local_secs(timestamp_secs);
}
await client.add_operator_comment(comment, robot_timestamp, {timeout});
}
async power_on(timeout_sec = 20_000, update_frequency = 1.0, timeout = null){
const service_name = PowerClient.default_service_name;
const client = await this.ensure_client(service_name);
power_on(client, timeout_sec, update_frequency, {timeout});
}
async power_off(cut_immediately = false, timeout_sec = 20_000, update_frequency = 1.0, timeout = null){
if(cut_immediately){
const power_client = await this.ensure_client(PowerClient.default_service_name);
power_off(power_client, timeout_sec, 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);
safe_power_off(command_client, state_client, timeout_sec, 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
};