spot-sdk-js
Version:
Develop applications and payloads for Spot using the unofficial Boston Dynamics Spot Node.js SDK.
235 lines (199 loc) • 10.4 kB
JavaScript
const {ResponseError} = require('./exceptions');
const util = require('./util');
const Struct = require('google-protobuf/google/protobuf/struct_pb')
const data_acquisition_pb = require('../bosdyn/api/data_acquisition_pb');
const data_acquisition_store_pb = require('../bosdyn/api/data_acquisition_store_pb');
function sleep(period) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, period);
});
}
async function issue_acquire_data_request(data_acq_client, acquisition_requests, group_name, action_name, metadata = null){
const action_id = new data_acquisition_pb.CaptureActionId().setActionName(action_name).setGroupName(group_name);
let request_id = null;
try{
request_id = await data_acq_client.acquire_data(acquisition_requests, action_name, action_id.getGroupName(), null, metadata);
}catch(e){
console.error(`[DATA ACQUISITION HELPERS] Exception raised by issue_acquire_data_request: ${e}`);
}
return {request_id, action_id};
}
async function acquire_and_process_request(data_acquisition_client, acquisition_requests, group_name, action_name, metadata = null, block_until_complete = true){
const {request_id, action_id} = await issue_acquire_data_request(data_acquisition_client, acquisition_requests, group_name, action_name, metadata);
if(!request_id) return false;
if(!block_until_complete) return true;
console.log(`[DATA ACQUISITION HELPERS] Waiting for acquisition (id: ${request_id}) to complete.`);
while(true){
let get_status_response = null;
try{
get_status_response = await data_acquisition_client.get_status(request_id);
}catch(e){
console.error(`[DATA ACQUISITION HELPERS] Exception: ${e}`);
return false;
}
console.log(`[DATA ACQUISITION HELPERS] Current status is: ${data_acquisition_pb.GetStatusResponse.Status[get_status_response.getStatus()]}`);
if(get_status_response.getStatus() == data_acquisition_pb.GetStatusResponse.Status.STATUS_COMPLETE) return true;
if(get_status_response.getStatus() == data_acquisition_pb.GetStatusResponse.Status.STATUS_TIMEDOUT){
console.log(`[DATA ACQUISITION HELPERS] Unrecoverable request timeout: ${get_status_response}`);
return false;
}
if(get_status_response.getStatus() == data_acquisition_pb.GetStatusResponse.Status.STATUS_DATA_ERROR){
console.log(`[DATA ACQUISITION HELPERS] Data error was received: ${get_status_response}`);
return false;
}
if(get_status_response.getStatus() == data_acquisition_pb.GetStatusResponse.Status.STATUS_REQUEST_ID_DOES_NOT_EXIST){
console.log(`[DATA ACQUISITION HELPERS] The acquisition request id "${request_id}" is unknown: ${get_status_response}`);
return false;
}
await sleep(200);
}
return true;
}
async function cancel_acquisition_request(data_acq_client, request_id){
if(!request_id) return;
try{
let is_cancelled_response = await data_acq_client.cancel_acquisition(request_id);
console.log(`[DATA ACQUISITION HELPERS] Status of the request to cancel the data-acquisition in progress: ${data_acquisition_pb.CancelAcquisitionResponse.Status[is_cancelled_response.getStatus()]}`);
}catch(e){
console.error(`[DATA ACQUISITION HELPERS] ResponseError raised when cancelling: ${e}`);
return;
}
while(true){
let get_status_response = null;
try{
get_status_response = await data_acq_client.get_status(request_id);
}catch(e){
console.error(`[DATA ACQUISITION HELPERS] Exception: ${e}`);
break;
}
console.log(`[DATA ACQUISITION HELPERS] Request ${request_id} status: ${data_acquisition_pb.GetStatusResponse.Status[get_status_response.getStatus()]}`);
if(get_status_response.getStatus() == data_acquisition_pb.GetStatusResponse.Status.STATUS_ACQUISITION_CANCELLED){
console.log("[DATA ACQUISITION HELPERS] The request is fully cancelled.");
break;
}
}
}
function clean_filename(filename){
return filename.replaceAll(/[:*?<>|]/gi, '');
}
function make_time_query_params(start_time_secs, end_time_secs, robot){
const from_timestamp = robot.time_sync.robot_timestamp_from_local_secs(start_time_secs);
const to_timestamp = robot.time_sync.robot_timestamp_from_local_secs(end_time_secs);
// console.log(from_timestamp.toString(), to_timestamp.toString());
const time_range = new data_acquisition_store_pb.TimeRangeQuery().setFromTimestamp(from_timestamp).setToTimestamp(to_timestamp);
const query_params = new data_acquisition_store_pb.DataQueryParams().setTimeRange(time_range);
return query_params;
}
async function make_time_query_params_from_group_name(group_name, data_store_client){
const action_id = new data_acquisition_pb.CaptureActionId().setGroupName(group_name);
const action_ids = new data_acquisition_store_pb.ActionIdQuery().addActionIds(action_id);
let query_params = new data_acquisition_store_pb.DataQueryParams().setActionIds(action_ids);
let saved_capture_actions = [];
try{
saved_capture_actions = await data_store_client.list_capture_actions(query_params);
}catch(e){
console.error(`[DATA ACQUISITION HELPERS] Failed to list the capture action ids for group_name ${group_name}: ${e}`);
return null;
}
let start_time = [null, null];
let end_time = [null, null];
for(const action_id of saved_capture_actions){
const timestamp = action_id.getTimestamp();
const time_secs = timestamp.getSeconds() + timestamp.getNanos() / 1e9;
if(time_secs == 0) continue;
if(start_time[0] == null || time_secs < start_time[0]){
start_time = [time_secs, timestamp];
}
if(end_time[0] == null || time_secs > end_time[0]){
end_time = [time_secs, timestamp];
}
}
if(start_time[0] == null && end_time[0] == null){
console.error(`[DATA ACQUISITION HELPERS] Could not find a start/end time from the list of capture action ids: ${saved_capture_actions}`);
return null;
}
console.assert(start_time[0] <= end_time[0]);
start_time[1].setSeconds(start_time[1].getSeconds() - 3000);
end_time[1].setSeconds(end_time[1].getSeconds() + 3000);
console.info(`[DATA ACQUISITION HELPERS] Downloading data with a start time of ${start_time[0]} seconds and end time of ${end_time[0]} seconds.`);
const time_range = new data_acquisition_store_pb.TimeRangeQuery().setFromTimestamp(start_time[1]).setToTimestamp(end_time[1]);
query_params = new data_acquisition_store_pb.DataQueryParams().setTimeRange(time_range);
return query_params;
}
function download_data_REST(query_params, hostname, token, destination_folder = './', additional_params = null){
const fetch = require('node-fetch');
const path = require('path');
const {mkdirSync} = require('fs');
function url(url, data = null){
let str = '';
if(data){
for (const [key, value] of Object.entries(data)) {
str += `${key}=${value}`;
}
}
return `${url}${str == '' ? '' : `?${str}`}`;
}
try{
const absolute_path = path.resolve(destination_folder);
const folder = path.join(path.dirname(absolute_path), clean_filename(path.basename(absolute_path)), 'REST');
mkdirSync(folder, {recursive: true});
const get_params = additional_params || {};
if('time_range' in query_params){
const timeGetFrom = query_params.getTimeRange().getFromTimestamp();
const timeGetTo = query_params.getTimeRange().getToTimestamp();
get_params['from_nsec'] = timeGetFrom.getSeconds() * 1e9 + timeGetFrom.getNanos();
get_params['to_nsec'] = timeGetTo.getSeconds() * 1e9 + timeGetTo.getNanos();
}
const chunk_size = 10 * (1024 ** 2);
const options = {
method: 'GET',
headers: {"Authorization": `Bearer ${token}`}
}
fetch(url(`https://${hostname}/v1/data-buffer/daq-data/`, get_params), options)
.then(res => res.buffer())
.then(res => {
console.log(`[DATA ACQUISITION HELPERS] Download request HTTPS status code: ${res.status}`);
if(res.status == 204){
const timeGetFrom = query_params.getTimeRange().getFromTimestamp();
const timeGetTo = query_params.getTimeRange().getToTimestamp();
console.log(`[DATA ACQUISITION HELPERS] No content available for the specified download time range (in seconds): [${(timeGetFrom.getSeconds() * 1e9 + timeGetFrom.getNanos()) / 1e9}, ${(timeGetTo.getSeconds() * 1e9 + timeGetTo.getNanos()) / 1e9}]`);
return false;
}
let download_file = path.join(folder, "download.zip");
const content = res.headers.get('Content-Disposition');
let start_ind = -1;
if(content.length < 2){
console.log("[DATA ACQUISITION HELPERS] ERROR: Content-Disposition is not set correctly");
return false;
}else{
start_ind = content.indexOf('\"');
}
if(start_ind == -1){
console.error("[DATA ACQUISITION HELPERS] ERROR: Content-Disposition does not have a \" character");
return false;
}else{
start_ind += 1;
download_file = path.join(folder, clean_filename(content.slice(start_ind, -1)));
}
writeFileSync(download_file, res);
}).catch(e => {
console.error(e);
return false;
});
}catch(e){
console.log(e);
return false;
}
return true;
}
module.exports = {
issue_acquire_data_request,
acquire_and_process_request,
cancel_acquisition_request,
clean_filename,
make_time_query_params,
make_time_query_params_from_group_name,
download_data_REST
};