spot-sdk-js
Version:
Develop applications and payloads for Spot using the unofficial Boston Dynamics Spot Node.js SDK.
567 lines (465 loc) • 20.4 kB
JavaScript
const Duration = require('google-protobuf/google/protobuf/duration_pb');
const estop_service_grpc_pb = require('../bosdyn/api/estop_service_grpc_pb');
const estop_pb = require('../bosdyn/api/estop_pb');
const {BaseClient, common_header_errors, handle_common_header_errors, handle_unset_status_error, error_factory} = require('./common');
const {ResponseError, RpcError, TimedOutError} = require('./exceptions');
const StopLevel = estop_pb.EstopStopLevel;
class ValueError extends Error {
constructor(msg){
super(msg);
this.name = 'ValueError';
}
};
class EstopResponseError extends ResponseError {
constructor(msg){
super(msg);
this.name = 'EstopResponseError';
}
};
class EndpointUnknownError extends EstopResponseError {
constructor(msg){
super(msg);
this.name = 'EndpointUnknownError';
}
};
class IncorrectChallengeResponseError extends EstopResponseError {
constructor(msg){
super(msg);
this.name = 'IncorrectChallengeResponseError';
}
};
class EndpointMismatchError extends EstopResponseError {
constructor(msg){
super(msg);
this.name = 'EndpointMismatchError';
}
};
class ConfigMismatchError extends EstopResponseError {
constructor(msg){
super(msg);
this.name = 'ConfigMismatchError';
}
};
class InvalidEndpointError extends EstopResponseError {
constructor(msg){
super(msg);
this.name = 'InvalidEndpointError';
}
};
class InvalidIdError extends EstopResponseError {
constructor(msg){
super(msg);
this.name = 'InvalidIdError';
}
};
class MotorsOnError extends EstopResponseError {
constructor(msg){
super(msg);
this.name = 'MotorsOnError';
}
};
class EstopClient extends BaseClient {
static default_service_name = 'estop';
static service_type = 'bosdyn.api.EstopService';
constructor(name = `EstopClient - PID: ${process.pid}`){
super(estop_service_grpc_pb.EstopServiceClient, name);
}
async register(target_config_id, endpoint, args){
const req = EstopClient._build_register_request(target_config_id, endpoint);
return await this.call(this._stub.registerEstopEndpoint, req, _new_endpoint_from_register_response, _register_endpoint_error_from_response, args);
}
register_async(target_config_id, endpoint, args){
const req = EstopClient._build_register_request(target_config_id, endpoint);
return this.call_async(this._stub.registerEstopEndpoint, req, _new_endpoint_from_register_response, _register_endpoint_error_from_response, args);
}
async deregister(target_config_id, endpoint, args){
const req = EstopClient._build_deregister_request(target_config_id, endpoint);
return await this.call(this._stub.deregisterEstopEndpoint, req, null, _deregister_endpoint_error_from_response, args);
}
deregister_async(target_config_id, endpoint, args){
var req = EstopClient._build_deregister_request(target_config_id, endpoint);
return this.call_async(this._stub.deregisterEstopEndpoint, req, null, _deregister_endpoint_error_from_response, args);
}
async get_config(args){
return await this.call(this._stub.getEstopConfig, new estop_pb.GetEstopConfigRequest(), _active_config_from_config_response, common_header_errors, args);
}
get_config_async(args){
return this.call_async(this._stub.getEstopConfig, new estop_pb.GetEstopConfigRequest(), _active_config_from_config_response, common_header_errors, args);
}
async set_config(config, target_config_id, args){
const req = new estop_pb.SetEstopConfigRequest().setConfig(config).setTargetConfigId(target_config_id);
return await this.call(this._stub.setEstopConfig, req, _active_config_from_config_response, _set_config_error_from_response, args);
}
set_config_async(config, target_config_id, args){
const req = new estop_pb.SetEstopConfigRequest().setConfig(config).setTargetConfigId(target_config_id);
return this.call_async(this._stub.setEstopConfig, req, _active_config_from_config_response, _set_config_error_from_response, args);
}
async get_status(args){
return await this.call(this._stub.getEstopSystemStatus, new estop_pb.GetEstopSystemStatusRequest(), _estop_sys_status_from_response, common_header_errors, args);
}
get_status_async(args){
return this.call_async(this._stub.getEstopSystemStatus, new estop_pb.GetEstopSystemStatusRequest(), _estop_sys_status_from_response, common_header_errors, args);
}
async check_in(stop_level, endpoint, challenge, response, suppress_incorrect = false, args){
const req = EstopClient._build_check_in_request(stop_level, endpoint, challenge, response);
const err_from_resp = EstopClient._choose_check_in_err_func(suppress_incorrect);
return await this.call(this._stub.estopCheckIn, req, _challenge_from_check_in_response, err_from_resp, args);
}
check_in_async(stop_level, endpoint, challenge, response, suppress_incorrect = false, args){
const req = EstopClient._build_check_in_request(stop_level, endpoint, challenge, response);
const err_from_resp = EstopClient._choose_check_in_err_func(suppress_incorrect);
return this.call_async(this._stub.estopCheckIn, req, _challenge_from_check_in_response, err_from_resp, args);
}
static _build_check_in_request(stop_level, endpoint, challenge, response){
if(endpoint instanceof EstopEndpoint) endpoint = endpoint.to_proto();
const req = new estop_pb.EstopCheckInRequest()
.setEndpoint(endpoint)
.setChallenge(challenge)
.setResponse(response)
setStopLevel(stop_level);
return req;
}
static _build_register_request(target_config_id, endpoint){
if(endpoint instanceof EstopEndpoint) endpoint = endpoint.to_proto();
const req = new estop_pb.RegisterEstopEndpointRequest()
.setTargetConfigId(target_config_id)
.setNewEndpoint(endpoint)
.setTargetEndpoint(new estop_pb.EstopEndpoint().setRole(endpoint.role));
return req;
}
static _build_deregister_request(target_config_id, endpoint){
if(endpoint instanceof EstopEndpoint) endpoint = endpoint.to_proto();
const req = estop_pb.DeregisterEstopEndpointRequest()
.setTargetEndpoint(endpoint)
.setTargetConfigId(target_config_id);
return req;
}
static _choose_check_in_err_func(suppress_incorrect){
return suppress_incorrect ? _check_in_error_from_response_no_incorrect : _check_in_error_from_response;
}
};
class EstopEndpoint {
REQUIRED_ROLE = 'PDB_rooted';
constructor(client, name, estop_timeout, role = this.REQUIRED_ROLE, first_checkin = true, estop_cut_power_timeout = null){
this.client = client;
this.role = role;
this.estop_timeout = estop_timeout;
this.estop_cut_power_timeout = estop_cut_power_timeout;
this._challenge = null;
this._name = name;
this._unique_id = null;
this._config_id = null;
/*this._lock = threading.Lock();
this._locked_first_checkin = first_checkin;*/
// Manque un package de thread pour cette partie !
this.logger = console;
}
toString(){
return this.estop_cut_power_timeout == null ? `${this._name} (timeout ${Math.floor(this.estop_timeout)}s` : `${this._name} (timeout ${Math.floor(this.estop_timeout)}s, cut_power_timeout ${this.estop_cut_power_timeout}s)`;
}
_first_checkin(){
return this._locked_first_checkin;
}
_set_first_checkin(val){
this._locked_first_checkin = val;
}
_set_challenge_without_exception_from_future(fut){
let new_challenge = null;
try{
new_challenge = fut.result();
}catch(e){
if(e instanceof EstopResponseError){
new_challenge = _challenge_from_check_in_response(exc.response);
}else if(e instanceof Error){
this.logger.warn(`Could not set challenge (error: ${e.constructor.name}`)
}
}
if(new_challenge != null) this.set_challenge(new_challenge);
}
set_challenge(challenge){
this._challenge = challenge;
}
get_challenge(){
return this._challenge;
}
force_simple_setup(){
const new_config = new estop_pb.EstopConfig();
const new_config_endpoint = new_config.addEndpoints(this.to_proto());
let active_config = this.client.get_config();
active_config = this.client.set_config(new_config, active_config.unique_id);
this._unique_id = active_config.endpoints[0].unique_id;
this.register(active_config.unique_id);
}
stop(args){
this.logger.debug('[ESTOP] Stopping')
this.check_in_at_level(StopLevel.ESTOP_LEVEL_CUT, args);
}
settle_then_cut(args){
this.logger.debug('[ESTOP] Stopping with SETTLE_THEN_CUT');
this.check_in_at_level(StopLevel.ESTOP_LEVEL_SETTLE_THEN_CUT, args);
}
allow(args){
this.logger.debug('[ESTOP] Releasing')
this.check_in_at_level(StopLevel.ESTOP_LEVEL_NONE, args);
}
check_in_at_level(level, args){
try{
this.set_challenge(this.client.check_in(level, this, this.get_challenge(), this._response(), this._first_checkin(), args));
}catch(e){
this.set_challenge(_challenge_from_check_in_response(e.response));
throw e;
}
this._set_first_checkin(false);
}
deregister(args){
this.logger.debug('[ESTOP] Deregistering');
this.client.deregister(this._config_id, this);
}
register(target_config_id, args){
this.logger.debug(`[ESTOP] Registering to ${target_config_id}`);
var new_endpoint = this.client.register(target_config_id, this);
this._config_id = target_config_id;
this.from_proto(new_endpoint);
this.logger.debug('[ESTOP] Doing check-in to seed challenge...');
this.stop();
}
stop_async(args){
this.logger.debug('[ESTOP] Stopping (async)')
return this.check_in_at_level_async(StopLevel.ESTOP_LEVEL_CUT, args);
}
settle_then_cut_async(args){
this.logger.debug('[ESTOP] Stopping with SETTLE_THEN_CUT (async)');
return this.check_in_at_level_async(StopLevel.ESTOP_LEVEL_SETTLE_THEN_CUT, args);
}
allow_async(args){
this.logger.debug('[ESTOP] Releasing (async)')
return this.check_in_at_level_async(StopLevel.ESTOP_LEVEL_NONE, args);
}
check_in_at_level_async(level, args){
var fut = this.client.check_in_async(level, self, this.get_challenge(), this._response(), suppress_incorrect = this._first_checkin(), args);
fut.add_done_callback(fut => this._set_challenge_without_exception_from_future(fut));
fut.add_done_callback(fut => this._set_first_checkin(false));
return fut;
}
deregister_async(self, args){
this.logger.debug('[ESTOP] Deregistering (async)');
return this.client.deregister_async(this._config_id, this);
}
from_proto(proto){
if(this._name != proto.getName()){
this.logger.info(`[ESTOP] Changing name to ${proto.getName()}`);
this._name = proto.getName();
this.logger = console; // logging.getLogger(self._name)
}
this.role = proto.getRole();
this.estop_timeout = proto.getTimeout().getSeconds() + proto.getTimeout().getNanos() * 1e-9
this._unique_id = proto.getUniqueId();
if(proto.getCutPowerTimeout() == null){
this.estop_cut_power_timeout = null;
}else{
this.estop_cut_power_timeout = proto.getCutPowerTimeout().getSeconds() + proto.getCutPowerTimeout().getNanos() * 1e-9
}
}
to_proto(){
const t_seconds = Math.floor(this.estop_timeout);
const t_nanos = Math.floor((this.estop_timeout - t_seconds) * 1e9);
if(this.estop_cut_power_timeout == null){
const req = new estop_pb.EstopEndpoint()
.setRole(this.role)
.setName(this._name)
.setUniqueId(this._unique_id)
.setTimeout(new Duration.Duration([t_seconds, t_nanos]));
return req;
}else{
const cpt_seconds = Math.floor(this.estop_cut_power_timeout);
const cpt_nanos = Math.floor((this.estop_cut_power_timeout - cpt_seconds) * 1e9);
const req = new estop_pb.EstopEndpoint()
.setRole(this.role)
.setName(this._name)
.setUniqueId(this._unique_id)
.setTimeout(new Duration.Duration([t_seconds, t_nanos]))
.setCutPowerTimeout(new Duration.Duration([cpt_seconds, cpt_nanos]));
return req;
}
}
_response(){
const challenge = this.get_challenge();
return challenge == null ? null : response_from_challenge(challenge);
}
get unique_id(){
return this._unique_id;
}
};
class EstopKeepAlive {
KeepAliveStatus = {
OK: 0,
ERROR: 1,
DISABLED: 2
}
constructor(endpoint, rpc_timeout_seconds = null, rpc_interval_seconds = null, keep_running_cb = null){
this._endpoint = endpoint;
// this._lock = threading.Lock()
// this._end_check_in_signal = threading.Event()
this._desired_stop_level = StopLevel.ESTOP_LEVEL_NONE;
this._rpc_timeout = rpc_timeout_seconds || this._endpoint.estop_timeout;
this._check_in_period = rpc_interval_seconds || this._endpoint.estop_timeout / 3.0;
if(this._rpc_timeout <= 0) throw new ValueError(`[ESTOP] Invalid rpc_timeout_seconds "${this._rpc_timeout}"`);
if(this._check_in_period < 0) throw new ValueError(`[ESTOP] Invalid rpc_interval_seconds "${this._check_in_period}"`);
this._keep_running = keep_running_cb || (() => true);
this.logger.debug(`[ESTOP] New ${this.constructor.name} for endpoint "${this._endpoint}"`);
this.status_queue = queue.Queue()
this._update_status(this.KeepAliveStatus.OK)
try{
this._check_in();
}catch(e){
this.logger.warn(`[ESTOP] Estop initial check-in exception:\n${e}\n`);
}
/*this._thread = threading.Thread(target=this._periodic_check_in)
this._thread.daemon = True
this._thread.start()*/
}
shutdown(){
this.logger.debug('[ESTOP] Shutting down');
this._end_periodic_check_in();
// this._thread.join()
}
get logger(){
return this._endpoint.logger;
}
allow(){
this._desired_stop_level = StopLevel.ESTOP_LEVEL_NONE;
this._check_in();
}
settle_then_cut(){
this._desired_stop_level = StopLevel.ESTOP_LEVEL_SETTLE_THEN_CUT;
this._check_in(rpc_timeout = this._endpoint.estop_timeout);
}
stop(){
this._desired_stop_level = StopLevel.ESTOP_LEVEL_CUT;
this._check_in(rpc_timeout = this._endpoint.estop_timeout);
}
_end_periodic_check_in(){
this.logger.debug('[ESTOP] Stopping check-in');
this._end_check_in_signal.set();
}
_error(msg, exception = null, disable = false){
this._update_status(this.KeepAliveStatus.ERROR, msg);
this.logger.error(msg);
if(disable){
this._end_periodic_check_in();
this._update_status(this.KeepAliveStatus.DISABLED, msg);
}
}
_ok(){
this._update_status(this.KeepAliveStatus.OK);
this.logger.debug('[ESTOP] Check-in successful');
}
_update_status(status, msg = ''){
// this.status_queue.put((status, msg), block=False)
}
_check_in(rpc_timeout = null){
rpc_timeout = rpc_timeout || this._rpc_timeout;
this._endpoint.check_in_at_level(this._desired_stop_level, {rpc_timeout});
}
_periodic_check_in(){
this.logger.info('[ESTOP] Starting estop check-in');
while (true){
var exec_start = Date.now();
if(!this._keep_running()) break;
var is_error = false;
try{
this._check_in();
}catch(e){
is_error = true;
if(e instanceof TimedOutError){
this._error(`[ESTOP] RPC took longer than ${this._rpc_timeout} seconds`, e);
}else if(e instanceof RpcError){
this._error(`[ESTOP] Transport exception during check-in: \n${e}\n (resuming check-in)`, e);
}else if(e instanceof EndpointUnknownError){
this._error(e.toString(), e, true);
}else{
this.logger.warn(`[ESTOP] Generic exception during check-in: \n${e}\n (resuming check-in)`)
}
}
if(!is_error) this._ok();
var exec_sec = Date.now() - exec_start;
if(this._end_check_in_signal.wait(this._check_in_period - exec_sec)) break;
}
this.logger.info('[ESTOP] Estop check-in stopped');
}
get endpoint(){
return this._endpoint;
}
get client(){
return this._endpoint.client;
}
};
function is_estopped(estop_client, args){
var response = estop_client.get_status(args);
return response.stop_level != StopLevel.ESTOP_LEVEL_NONE;
}
function response_from_challenge(challenge){
return challenge.getValue();
}
const _CHECK_IN_STATUS_TO_ERROR = {
[estop_pb.EstopCheckInResponse.Status.STATUS_OK]: [null, null],
[estop_pb.EstopCheckInResponse.StatusSTATUS_ENDPOINT_UNKNOWN]: [EndpointUnknownError, 'The endpoint specified in the request is not registered.'],
[estop_pb.EstopCheckInResponse.Status.STATUS_INCORRECT_CHALLENGE_RESPONSE]: [IncorrectChallengeResponseError, 'The challenge and/or response was incorrect.']
}
const _SET_CONFIG_STATUS_TO_ERROR = {
[estop_pb.SetEstopConfigResponse.Status.STATUS_SUCCESS]: [null, null],
[estop_pb.SetEstopConfigResponse.Status.STATUS_INVALID_ID]: [InvalidIdError, 'Tried to replace a EstopConfig, but provided bad ID.'],
[estop_pb.SetEstopConfigResponse.Status.STATUS_MOTORS_ON]: [MotorsOnError, 'The operation is not allowed while motors are on.']
}
const _DEREGISTER_ENDPOINT_STATUS_TO_ERROR = {
[estop_pb.DeregisterEstopEndpointResponse.Status.STATUS_SUCCESS]: [null, null],
[estop_pb.DeregisterEstopEndpointResponse.Status.STATUS_ENDPOINT_MISMATCH]: [EndpointMismatchError, 'Target endpoint did not match.'],
[estop_pb.DeregisterEstopEndpointResponse.Status.STATUS_CONFIG_MISMATCH]: [ConfigMismatchError, 'Registered to the wrong configuration.']
}
const _REGISTER_ENDPOINT_STATUS_TO_ERROR = {
[estop_pb.RegisterEstopEndpointResponse.Status.STATUS_SUCCESS]: [null, null],
[estop_pb.RegisterEstopEndpointResponse.Status.STATUS_ENDPOINT_MISMATCH]: [EndpointMismatchError, 'Target endpoint did not match.'],
[estop_pb.RegisterEstopEndpointResponse.Status.STATUS_CONFIG_MISMATCH]: [ConfigMismatchError, 'Registered to the wrong configuration.'],
[estop_pb.RegisterEstopEndpointResponse.Status.STATUS_INVALID_ENDPOINT]: [InvalidEndpointError, 'New endpoint was invalid']
}
function _check_in_error_from_response(response){
return error_factory(response, response.getStatus(), Object.keys(estop_pb.EstopCheckInResponse.Status), _CHECK_IN_STATUS_TO_ERROR);
}
function _check_in_error_from_response_no_incorrect(resp){
if(resp.getStatus() == estop_pb.EstopCheckInResponse.Status.STATUS_INCORRECT_CHALLENGE_RESPONSE) return null;
return _check_in_error_from_response(resp);
}
function _set_config_error_from_response(response){
return error_factory(response, response.getStatus(), Object.keys(estop_pb.SetEstopConfigResponse.Status), _SET_CONFIG_STATUS_TO_ERROR);
}
function _deregister_endpoint_error_from_response(response){
return error_factory(response, response.getStatus(), Object.keys(estop_pb.DeregisterEstopEndpointResponse.Status), _DEREGISTER_ENDPOINT_STATUS_TO_ERROR);
}
function _register_endpoint_error_from_response(response){
return error_factory(response, response.getStatus(), Object.keys(estop_pb.RegisterEstopEndpointResponse.Status), _REGISTER_ENDPOINT_STATUS_TO_ERROR);
}
function _new_endpoint_from_register_response(response){
return response.getNewEndpoint();
}
function _active_config_from_config_response(response){
return response.getActiveConfig();
}
function _challenge_from_check_in_response(response){
return response.getChallenge();
}
function _estop_sys_status_from_response(response){
return response.getStatus();
}
module.exports = {
EstopResponseError,
EndpointUnknownError,
IncorrectChallengeResponseError,
EndpointMismatchError,
ConfigMismatchError,
InvalidEndpointError,
InvalidIdError,
EstopClient,
EstopEndpoint,
EstopKeepAlive,
is_estopped
};