@robotical/ricjs
Version:
Javascript/TS library for Robotical RIC
784 lines (721 loc) • 23.9 kB
text/typescript
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// RICJS
// Communications Library
//
// Rob Dobson & Chris Greening 2020-2022
// (C) 2020-2022
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
import {
RICSysModInfoWiFi,
RICWifiConnState,
RICWifiConnStatus,
} from "./RICWifiTypes";
import RICAddOnManager from "./RICAddOnManager";
import RICLog from "./RICLog";
import RICMsgHandler from "./RICMsgHandler";
import {
RICConfiguredAddOns,
RICCalibInfo,
RICFileList,
RICFriendlyName,
RICHWElem,
RICHWElemList_Str,
RICOKFail,
RICReportMsg,
RICSysModInfoBLEMan,
RICSystemInfo,
RICWifiScanResults,
RICHWElemList,
} from "./RICTypes";
export default class RICSystem {
// Message handler
private _ricMsgHandler: RICMsgHandler;
// Add-on manager
private _addOnManager: RICAddOnManager;
// System info
private _systemInfo: RICSystemInfo | null = null;
// RIC naming
private _ricFriendlyName: RICFriendlyName | null = null;
// HWElems (connected to RIC) - excluding AddOns
private _hwElemsExcludingAddOns: Array<RICHWElem> = new Array<RICHWElem>();
private _connectedAddOns: Array<RICHWElem> = new Array<RICHWElem>();
// Calibration info
private _calibInfo: RICCalibInfo | null = null;
// WiFi connection info
private _ricWifiConnStatus: RICWifiConnStatus = new RICWifiConnStatus();
private _defaultWiFiHostname = "Marty";
private _maxSecsToWaitForWiFiConn = 20;
/**
* constructor
* @param ricMsgHandler
* @param addOnManager
*/
constructor(ricMsgHandler: RICMsgHandler, addOnManager: RICAddOnManager) {
this._ricMsgHandler = ricMsgHandler;
this._addOnManager = addOnManager;
}
/**
* getFriendlyName
*
* @returns friendly name
*/
getFriendlyName(): RICFriendlyName | null {
return this._ricFriendlyName;
}
/**
* invalidate
*/
invalidate() {
// Invalidate system info
this._systemInfo = null;
this._hwElemsExcludingAddOns = new Array<RICHWElem>();
this._connectedAddOns = new Array<RICHWElem>();
this._addOnManager.clear();
this._calibInfo = null;
this._ricFriendlyName = null;
RICLog.debug("RICSystem information invalidated");
}
/**
* getSystemInfo - get system info
* @returns Promise<RICSystemInfo>
*
*/
async retrieveInfo(): Promise<boolean> {
// Get HWElems (connected to RIC)
try {
await this.getHWElemList();
} catch (error) {
RICLog.warn("retrieveInfo - failed to get HWElems " + error);
return false;
}
// Get system info
RICLog.debug(`RICSystem retrieveInfo getting system info`);
try {
await this.getRICSystemInfo(true);
RICLog.debug(
`retrieveInfo - RIC Version ${this._systemInfo?.SystemVersion}`
);
} catch (error) {
RICLog.warn("retrieveInfo - frailed to get version " + error);
return false;
}
// Get RIC name
try {
await this.getRICName();
} catch (error) {
RICLog.warn("retrieveInfo - failed to get RIC name " + error);
return false;
}
// Get calibration info
try {
await this.getRICCalibInfo(true);
} catch (error) {
RICLog.warn("retrieveInfo - failed to get calib info " + error);
return false;
}
// Get WiFi connected info
try {
await this.getWiFiConnStatus();
} catch (error) {
RICLog.warn("retrieveInfo - failed to get WiFi Status " + error);
return false;
}
// Get HWElems (connected to RIC)
try {
await this.getHWElemList();
} catch (error) {
RICLog.warn("retrieveInfo - failed to get HWElems " + error);
return false;
}
return true;
}
/**
*
* getRICSystemInfo
* @returns Promise<RICSystemInfo>
*
*/
async getRICSystemInfo(forceGetFromRIC = false): Promise<RICSystemInfo> {
if (!forceGetFromRIC && this._systemInfo) {
return this._systemInfo;
}
try {
this._systemInfo = await this._ricMsgHandler.sendRICRESTURL<
RICSystemInfo
>("v");
RICLog.debug(
"getRICSystemInfo returned " + JSON.stringify(this._systemInfo)
);
this._systemInfo.validMs = Date.now();
return this._systemInfo;
} catch (error) {
RICLog.debug(`getRICSystemInfo Failed to get version ${error}`);
return new RICSystemInfo();
}
}
// Mark: Calibration -----------------------------------------------------------------------------------------
async calibrate(
cmd: string,
jointList: Array<string>,
jointNames: { [key: string]: string },
servoControllers: {[key: string]: string} = {
"LeftHip": "LeftHip",
"LeftTwist": "LeftHip",
"LeftKnee": "LeftHip",
"RightHip": "RightHip",
"RightTwist": "RightHip",
"RightKnee": "RightHip",
"LeftArm": "LeftArm",
"RightArm": "LeftArm",
"Eyes": "LeftArm"
}
) {
let overallResult = true;
if (cmd === "set") {
const controllerArray: Array<string> = [];
// Set calibration
for (const jnt of jointList) {
try {
// Set calibration on joint
const cmdUrl = "calibrate/set/" + jnt;
const rsl = await this._ricMsgHandler.sendRICRESTURL<RICOKFail>(
cmdUrl
);
// saving the calibration... (For the new servo boards it is necessary
// to send a "save" command after the calibration ones or any servo
// parameter changes in order to save any changes made into nonvolatile storage)
// log it now, and we'll do the saveparams commands once we've updated all the joints
// the saveparams function is not instant as the flash write takes a few ms
if (jnt in servoControllers && !controllerArray.includes(servoControllers[jnt]))
controllerArray.push(servoControllers[jnt])
if (rsl.rslt != "ok") overallResult = false;
} catch (error) {
console.log(`calibrate failed on joint ${jnt}`, error);
}
// Wait as writing to flash blocks servo access
// as of v0.0.113 of firmware, the pause is no longer required
//await new Promise(resolve => setTimeout(resolve, 3000));
}
// on newer (batch4+) robots with stm32 servo controllers it is necessary to send a saveparams command once per servo controller
for (const cID in controllerArray){
const saveCalibCmd = `elem/${controllerArray[cID]}/saveparams`;
const rslt = await this._ricMsgHandler.sendRICRESTURL<RICOKFail>(saveCalibCmd);
if (rslt.rslt != "ok") overallResult = false;
await new Promise(resolve => setTimeout(resolve, 200));
}
// ensure all joints are enabled
for (const jnt in jointNames) {
try {
// enable joint
const cmdUrl = "servo/" + jnt + "/enable/1";
const rsl = await this._ricMsgHandler.sendRICRESTURL<RICOKFail>(
cmdUrl
);
if (rsl.rslt != "ok") overallResult = false;
} catch (error) {
console.log(`enable failed on joint ${jnt}`, error);
}
}
// Result
console.log("Set calibration flag to true");
const rslt = new RICOKFail();
rslt.set(overallResult);
return rslt;
}
return false;
}
/**
*
* getRICCalibInfo
* @returns Promise<RICCalibInfo>
*
*/
async getRICCalibInfo(forceGetFromRIC = false): Promise<RICCalibInfo> {
if (!forceGetFromRIC && this._calibInfo) {
return this._calibInfo;
}
try {
this._calibInfo = await this._ricMsgHandler.sendRICRESTURL<RICCalibInfo>(
"calibrate"
);
RICLog.debug("getRICCalibInfo returned " + this._calibInfo);
this._calibInfo.validMs = Date.now();
return this._calibInfo;
} catch (error) {
RICLog.debug(`getRICCalibInfo Failed to get version ${error}`);
return new RICCalibInfo();
}
}
/**
*
* setRICName
* @param newName name to refer to RIC - used for BLE advertising
* @returns Promise<boolean> true if successful
*
*/
async setRICName(newName: string): Promise<boolean> {
try {
this._ricFriendlyName = await this._ricMsgHandler.sendRICRESTURL<
RICFriendlyName
>(`friendlyname/${newName}`);
if (this._ricFriendlyName) {
this._ricFriendlyName.friendlyNameIsSet = false;
this._ricFriendlyName.validMs = Date.now();
if (
this._ricFriendlyName &&
this._ricFriendlyName.rslt &&
this._ricFriendlyName.rslt.toLowerCase() === "ok"
) {
this._ricFriendlyName.friendlyNameIsSet = true;
}
RICLog.debug(
"setRICName returned " + JSON.stringify(this._ricFriendlyName)
);
return true;
}
return true;
} catch (error) {
this._ricFriendlyName = null;
return false;
}
}
/**
*
* getRICName
* @returns Promise<RICNameResponse> (object containing rslt)
*
*/
async getRICName(): Promise<RICFriendlyName> {
try {
this._ricFriendlyName = await this._ricMsgHandler.sendRICRESTURL<
RICFriendlyName
>("friendlyname");
if (
this._ricFriendlyName &&
this._ricFriendlyName.rslt &&
this._ricFriendlyName.rslt === "ok"
) {
this._ricFriendlyName.friendlyNameIsSet = this._ricFriendlyName
.friendlyNameIsSet
? true
: false;
} else {
this._ricFriendlyName.friendlyNameIsSet = false;
}
this._ricFriendlyName.validMs = Date.now();
RICLog.debug(
"Friendly name set is: " + JSON.stringify(this._ricFriendlyName)
);
return this._ricFriendlyName;
} catch (error) {
return new RICFriendlyName();
}
}
/**
*
* getHWElemList - get the list of hardware elements connected to the robot
* - the result (if successful) is processed as follows:
* = if no filter is applied then all non-add-ons found are stored in
* this._hwElemsExcludingAddOns and all addons are stored in this._connectedAddOns
* = if a filter is applied and this filter is RSAddOns then this._connectedAddOns is
* updated with the new list of addons
* = in all cases the discovered list is returned
*
* @returns Promise<RICHWElemList>
*
*/
async getHWElemList(filterByType?: string): Promise<Array<RICHWElem>> {
// Form a list of the requests to make
const reqList: Array<string> = [];
let addToNonAddOnsList = false;
if (!filterByType) {
reqList.push("SmartServo");
reqList.push("RSAddOn");
reqList.push("BusPixels");
reqList.push("!SmartServo,RSAddOn,BusPixels"); // not SmartServo or RSAddOn or BusPixels
this._hwElemsExcludingAddOns = new Array<RICHWElem>();
addToNonAddOnsList = true;
} else if (filterByType === "RSAddOn") {
// we treat BusPixels as an RSAddOn
// (batch 4 led eye add-ons have type BusPixels)
reqList.push("RSAddOn");
reqList.push("BusPixels");
this._connectedAddOns = new Array<RICHWElem>();
} else {
reqList.push(filterByType);
}
// Make the requests
const fullListOfElems = new Array<RICHWElem>();
this._connectedAddOns = [];
for (const reqType of reqList) {
try {
const hwElemList_Str = await this._ricMsgHandler.sendRICRESTURL<
RICHWElemList_Str
>(`hwstatus/strstat?filterByType=${reqType}`);
// if the results of hwElem indicates that we are on an older fw version
// send the old hwstatus command and don't expand()
// the logic behind deciding if we are on a fw version that
// supports strstat is: given that hwElemList_Str.hw === object[]
// if we get back string[], then we know we are on an older version
// if hw === empty array, then we don't have any hw elems in which
// case we can stop at that point
const hwElems = hwElemList_Str.hw;
let hwElemList;
if (hwElems.length) {
if (typeof hwElems[0] !== "object") {
// we are on an older version
hwElemList = await this._ricMsgHandler.sendRICRESTURL<
RICHWElemList
>(`hwstatus?filterByType=${reqType}`);
} else {
// we are on the fw version that supports strstat
hwElemList = RICHWElemList_Str.expand(hwElemList_Str);
}
}
if (hwElemList && hwElemList.rslt && hwElemList.rslt === "ok") {
fullListOfElems.push(...hwElemList.hw);
if (reqType === "RSAddOn") {
this._connectedAddOns = hwElemList.hw;
this._addOnManager.setHWElems(this._connectedAddOns);
// Debug
RICLog.debug(
`getHWElemList: found ${hwElemList.hw.length} addons/buspixels`
);
} else if (reqType === "BusPixels") {
// BusPixels are treated as an RSAddOn
this._connectedAddOns.push(...hwElemList.hw);
this._addOnManager.setHWElems(this._connectedAddOns);
// Debug
RICLog.debug(
`getHWElemList: found ${hwElemList.hw.length} addons/buspixels`
);
} else if (addToNonAddOnsList) {
this._hwElemsExcludingAddOns.push(...hwElemList.hw);
// Debug
RICLog.debug(
`getHWElemList: found ${hwElemList.hw.length} elems matching ${reqType}`
);
}
}
} catch (error) {
RICLog.debug(`getHWElemList failed to get ${reqType} ${error}`);
return new Array<RICHWElem>();
}
}
// Handle any callbacks
try {
const reports: Array<RICReportMsg> = [];
// add callback to subscribe to report messages
this._ricMsgHandler.reportMsgCallbacksSet("getHWElemCB", function (
report: RICReportMsg
) {
reports.push(report);
RICLog.debug(`getHWElemCB Report callback ${JSON.stringify(report)}`);
});
// run any required initialisation for the addons
const initCmds = this._addOnManager.getInitCmds();
// send init commands to the robot
const timeInitStart = Date.now();
for (let i = 0; i < initCmds.length; i++) {
this.runCommand(initCmds[i], {});
}
// wait a couple of seconds for any report messages to be received
await new Promise((resolve) => setTimeout(resolve, 2000));
// pass report messages to add on manager for processing
this._addOnManager.processReportMsg(reports, timeInitStart);
// clean up callback
this._ricMsgHandler.reportMsgCallbacksDelete("getHWElemCB");
} catch (error) {
RICLog.debug(`getHWElemList failed processing callback reports ${error}`);
return new Array<RICHWElem>();
}
// return the full list of elements
return fullListOfElems;
}
/**
*
* getAddOnConfigs - get list of add-ons configured on the robot
* @returns Promise<RICConfiguredAddOns>
*
*/
async getAddOnConfigs(): Promise<RICConfiguredAddOns> {
try {
const addOnList = await this._ricMsgHandler.sendRICRESTURL<
RICConfiguredAddOns
>("addon/list");
RICLog.debug("getAddOnConfigs returned " + addOnList);
return addOnList;
} catch (error) {
RICLog.debug(`getAddOnConfigs Failed to get list of add-ons ${error}`);
return new RICConfiguredAddOns();
}
}
/**
*
* getFileList - get list of files on file system
* @returns Promise<RICFileList>
*
*/
async getFileList(): Promise<RICFileList> {
try {
const ricFileList = await this._ricMsgHandler.sendRICRESTURL<RICFileList>(
"filelist"
);
RICLog.debug("getFileList returned " + ricFileList);
return ricFileList;
} catch (error) {
RICLog.debug(`getFileList Failed to get file list ${error}`);
return new RICFileList();
}
}
/**
*
* runCommand
* @param commandName command API string
* @param params parameters (simple name value pairs only) to parameterize trajectory
* @returns Promise<RICOKFail>
*
*/
async runCommand(commandName: string, params: object): Promise<RICOKFail> {
try {
// Format the paramList as query string
const paramEntries = Object.entries(params);
let paramQueryStr = "";
for (const param of paramEntries) {
if (paramQueryStr.length > 0) paramQueryStr += "&";
paramQueryStr += param[0] + "=" + param[1];
}
// Format the url to send
if (paramQueryStr.length > 0) commandName += "?" + paramQueryStr;
return await this._ricMsgHandler.sendRICRESTURL<RICOKFail>(commandName);
} catch (error) {
RICLog.debug(`runCommand failed ${error}`);
return new RICOKFail();
}
}
/**
*
* Get BLEMan sysmod info
*
* @returns RICSysModInfoBLEMan
*
*/
async getSysModInfoBLEMan(): Promise<RICSysModInfoBLEMan | null> {
try {
// Get SysMod Info
const bleInfo = await this._ricMsgHandler.sendRICRESTURL<
RICSysModInfoBLEMan
>("sysmodinfo/BLEMan");
// Debug
RICLog.debug(
`getSysModInfoBLEMan rslt ${bleInfo.rslt} isConn ${bleInfo.isConn} paused ${bleInfo.isAdv} txBPS ${bleInfo.txBPS} rxBPS ${bleInfo.rxBPS}`
);
// Check for test rate
if ("tBPS" in bleInfo) {
RICLog.debug(
`getSysModInfoBLEMan testMsgs ${bleInfo.tM} testBytes ${bleInfo.tB} testRateBytesPS ${bleInfo.tBPS}`
);
}
return bleInfo;
} catch (error) {
RICLog.debug(`getSysModInfoBLEMan sysmodinfo/BLEMan failed ${error}`);
}
return null;
}
/**
* Get hostname of connected WiFi
*
* @return string - hostname of connected WiFi
*
*/
_getHostnameFromFriendlyName(): string {
const friendlyName = this.getFriendlyName();
if (!friendlyName) {
return this._defaultWiFiHostname;
}
let hostname = friendlyName.friendlyName;
hostname = hostname?.replace(/ /g, "-");
hostname = hostname.replace(/\W+/g, "");
return hostname;
}
/**
* Get Wifi connection status
*
* @return boolean - true if connected
*
*/
async getWiFiConnStatus(): Promise<boolean | null> {
try {
// Get status
const ricSysModInfoWiFi = await this._ricMsgHandler.sendRICRESTURL<
RICSysModInfoWiFi
>("sysmodinfo/NetMan");
RICLog.debug(
`wifiConnStatus rslt ${ricSysModInfoWiFi.rslt} isConn ${ricSysModInfoWiFi.isConn} paused ${ricSysModInfoWiFi.isPaused}`
);
// Check status indicates WiFi connected
if (ricSysModInfoWiFi.rslt === "ok") {
this._ricWifiConnStatus.connState =
ricSysModInfoWiFi.isConn !== 0
? RICWifiConnState.WIFI_CONN_CONNECTED
: RICWifiConnState.WIFI_CONN_NONE;
this._ricWifiConnStatus.isPaused = ricSysModInfoWiFi.isPaused !== 0;
this._ricWifiConnStatus.ipAddress = ricSysModInfoWiFi.IP;
this._ricWifiConnStatus.hostname = ricSysModInfoWiFi.Hostname;
this._ricWifiConnStatus.ssid = ricSysModInfoWiFi.SSID;
this._ricWifiConnStatus.bssid = ricSysModInfoWiFi.WiFiMAC;
this._ricWifiConnStatus.validMs = Date.now();
return (
ricSysModInfoWiFi.isConn !== 0 || ricSysModInfoWiFi.isPaused !== 0
);
}
} catch (error) {
RICLog.debug(`[DEBUG]: wifiConnStatus sysmodinfo failed ${error}`);
this._ricWifiConnStatus.validMs = 0;
}
this._ricWifiConnStatus.connState = RICWifiConnState.WIFI_CONN_NONE;
this._ricWifiConnStatus.isPaused = false;
return null;
}
// Mark: WiFi Connection ------------------------------------------------------------------------------------
/**
* pause Wifi connection
*
* @param boolean - true to pause, false to resume
* @return boolean - true if successful
*
*/
async pauseWifiConnection(pause: boolean): Promise<boolean> {
try {
if (pause) {
await this._ricMsgHandler.sendRICRESTURL<RICOKFail>("wifipause/pause");
} else {
await this._ricMsgHandler.sendRICRESTURL<RICOKFail>("wifipause/resume");
}
} catch (error) {
RICLog.debug(`wifiConnect wifi pause ${error}`);
return true;
}
return false;
}
/**
* Connect to WiFi
*
* @param string - WiFi SSID
* @param string - WiFi password
* @return boolean - true if successful
*
*/
async wifiConnect(ssid: string, password: string): Promise<boolean> {
RICLog.debug(`Connect to WiFi ${ssid} password ${password}`);
// Issue the command to connect WiFi
try {
const RICRESTURL_wifiCredentials =
"w/" +
ssid +
"/" +
password +
"/" +
this._getHostnameFromFriendlyName();
RICLog.debug(
`wifiConnect attempting to connect to wifi ${RICRESTURL_wifiCredentials}`
);
await this._ricMsgHandler.sendRICRESTURL<RICOKFail>(
RICRESTURL_wifiCredentials
);
} catch (error) {
RICLog.debug(`wifiConnect failed ${error}`);
return false;
}
// Wait until connected, timed-out or failed
for (
let timeoutCount = 0;
timeoutCount < this._maxSecsToWaitForWiFiConn;
timeoutCount++
) {
// Wait a little before checking
await new Promise((resolve) => setTimeout(resolve, 1000));
// Get status info
const connStat = await this.getWiFiConnStatus();
RICLog.debug(`wifiConnect connStat ${connStat}`);
if (connStat) {
return true;
}
}
return false;
}
/**
* Disconnect WiFi
*
* @return boolean - true if successful
*
*/
async wifiDisconnect(): Promise<boolean> {
try {
RICLog.debug(`wifiDisconnect clearing wifi info`);
await this._ricMsgHandler.sendRICRESTURL<RICOKFail>("wc");
this.getWiFiConnStatus();
return true;
} catch (error) {
RICLog.debug(`wifiDisconnect clearing unsuccessful`);
}
return false;
}
// Mark: WiFi Scan ------------------------------------------------------------------------------------
/**
* WiFiScan start
*
* @return boolean - true if successful
*
*/
async wifiScanStart(): Promise<boolean> {
try {
RICLog.debug(`wifiScanStart`);
await this._ricMsgHandler.sendRICRESTURL<RICOKFail>("wifiscan/start");
return true;
} catch (error) {
RICLog.debug(`wifiScanStart unsuccessful`);
}
return false;
}
/**
* WiFiScan get results
*
* @return boolean - false if unsuccessful, otherwise the results of the promise
*
*/
async wifiScanResults(): Promise<boolean | RICOKFail | RICWifiScanResults> {
try {
RICLog.debug(`wifiScanResults`);
return this._ricMsgHandler.sendRICRESTURL<RICOKFail | RICWifiScanResults>(
"wifiscan/results"
);
} catch (error) {
RICLog.debug(`wifiScanResults unsuccessful`);
}
return false;
}
getCachedSystemInfo(): RICSystemInfo | null {
return this._systemInfo;
}
getCachedAddOnList(): Array<RICHWElem> {
return this._connectedAddOns;
}
getCachedAllHWElems(): Array<RICHWElem> {
const allHWElems = new Array<RICHWElem>();
allHWElems.push(...this._connectedAddOns);
allHWElems.push(...this._hwElemsExcludingAddOns);
return allHWElems;
}
getCachedCalibInfo(): RICCalibInfo | null {
return this._calibInfo;
}
getCachedRICName(): RICFriendlyName | null {
return this._ricFriendlyName;
}
getCachedWifiStatus(): RICWifiConnStatus {
return this._ricWifiConnStatus;
}
}