// used to append interfaces to omnicore sdk so that created app and AppStudio can use
import API from './ecosystem-base.js';
import {Logger} from './../function/log-helper.js';
import {InfoType, WarningType} from '../information/informationCode.js';
import {UAS, Success} from '../store/const.js';
import Store from '../store/store.js';
import {ErrorCode} from '../exception/exceptionDesc.js';
const factoryApiUasManagement = function (es) {
let logModule = 'ecosystem-uas';
/**
* The API.UAS namespace provides a set of interfaces for User Administration Service (UAS) management.
* @alias API.UAS
* @namespace
*/
es.UAS = new (function () {
/**
* Enum for all user grants
* @readonly
* @enum {string}
* @memberof API.UAS
*/
this.USERGRANTLIST = {
UAS_CFG_WRITE: 'UAS_CFG_WRITE',
UAS_BACKUP: 'UAS_BACKUP',
UAS_CALIBRATE: 'UAS_CALIBRATE',
UAS_CONTROLLER_PROPERTIES_WRITE: 'UAS_CONTROLLER_PROPERTIES_WRITE',
UAS_EVENTLOG_CLEAR: 'UAS_EVENTLOG_CLEAR',
UAS_FILE_ACCESS_READ: 'UAS_FILE_ACCESS_READ',
UAS_FILE_ACCESS_READ_WRITE: 'UAS_FILE_ACCESS_READ_WRITE',
UAS_IO_WRITE: 'UAS_IO_WRITE',
UAS_REMOTE_WARMSTART: 'UAS_REMOTE_WARMSTART',
UAS_RESTORE: 'UAS_RESTORE',
UAS_RAPID_EDIT: 'UAS_RAPID_EDIT',
UAS_RAPID_LOADPROGRAM: 'UAS_RAPID_LOADPROGRAM',
UAS_RAPID_MODPOS: 'UAS_RAPID_MODPOS',
UAS_RAPID_EXECUTE: 'UAS_RAPID_EXECUTE',
UAS_RAPID_DEBUG: 'UAS_RAPID_DEBUG',
UAS_SYSTEM_ADMINISTRATION: 'UAS_SYSTEM_ADMINISTRATION',
UAS_SPEED_DECREASE: 'UAS_SPEED_DECREASE',
UAS_RAPID_CURRVALUE: 'UAS_RAPID_CURRVALUE',
UAS_REVOLUTION_COUNTER_UPDATE: 'UAS_REVOLUTION_COUNTER_UPDATE',
UAS_SYSUPDATE: 'UAS_SYSUPDATE',
UAS_REMOTE_LOGIN: 'UAS_REMOTE_LOGIN',
UAS_NETWORK_SECURITY: 'UAS_NETWORK_SECURITY',
UAS_REMOTE_MOUNT_FILE_ACCESS_READ_WRITE: 'UAS_REMOTE_MOUNT_FILE_ACCESS_READ_WRITE',
UAS_REMOTE_START_STOP_IN_AUTO: 'UAS_REMOTE_START_STOP_IN_AUTO',
UAS_FTP_READ: 'UAS_FTP_READ',
UAS_FTP_WRITE: 'UAS_FTP_WRITE',
UAS_LOCK_SAFETY_CONFIG: 'UAS_LOCK_SAFETY_CONFIG',
UAS_SAFETY_SERVICES: 'UAS_SAFETY_SERVICES',
UAS_SAFETY_SYNCHRONIZATION: 'UAS_SAFETY_SYNCHRONIZATION',
UAS_KL_MODE_SELECTOR: 'UAS_KL_MODE_SELECTOR',
UAS_SAFETY_COMMISSIONING_MODE: 'UAS_SAFETY_COMMISSIONING_MODE',
UAS_REMOTE_MOUNT_FILE_ACCESS_READ: 'UAS_REMOTE_MOUNT_FILE_ACCESS_READ',
UAS_APPL_KEY_FP_AUTO_LOGOFF: 'UAS_APPL_KEY_FP_AUTO_LOGOFF',
UAS_UAS_ADMINISTRATION: 'UAS_UAS_ADMINISTRATION',
UAS_DETACH_FLEXPENDANT: 'UAS_DETACH_FLEXPENDANT',
};
/**
* Functions that have grant requirements.
* @alias GRANTTYPES
* @memberof API.UAS
* @readonly
* @enum {number}
*/
this.GRANTTYPES = {
loadModule: 1,
deploy: 2,
updatePosition: 3,
ioWrite: 4,
editRAPID: 5,
executeProgram: 6,
};
/**
* Functions that grant state.
* @alias GRANTSTATES
* @memberof API.UAS
* @readonly
* @enum {number}
*/
this.GRANTSTATES = {
NOT_GRANTED: 'NOT_GRANTED',
GRANTED: 'GRANTED',
};
/**
* Stable subscription object reference
* @private
*/
let uasSubscription = null;
/**
* Whether UAS subscription is active
* @private
*/
let uasSubscribed = false;
/**
* Prevent concurrent subscribe
* @private
*/
let uasSubscribePromise = null;
/**
* Prevent concurrent unsubscribe
* @private
*/
let uasUnsubscribePromise = null;
/**
* Whether Store has been initialized once
* @private
*/
let uasStoreInitialized = false;
/**
* One-time initialization lock
* @private
*/
let uasInitPromise = null;
/**
* Refresh lock for event-triggered updates
* @private
*/
let uasRefreshPromise = null;
/**
* Watchers: Map<grantKey, Set<callback>>
* @private
*/
const uasGrantWatchers = new Map();
/**
* Ensure UAS namespace exists in Store.
*
* @private
* @returns {Object} Store namespace
*/
const ensureUasNamespace = () => {
let namespace = Store.getNamespace(UAS);
if (!namespace) {
Store.registerNamespace(UAS);
namespace = Store.getNamespace(UAS);
}
return namespace;
};
/**
* Read one grant state from Store.
*
* @private
* @param {string} key
* @returns {boolean|undefined}
*/
const readGrantStateFromStore = (key) => {
const mappingKey = Store.generateMappingKey(UAS, [key]);
const mapping = ensureUasNamespace().getMapping(mappingKey);
return mapping ? mapping.value : undefined;
};
/**
* Write one grant state into Store.
*
* @private
* @param {string} key
* @param {boolean} value
*/
const writeGrantStateToStore = (key, value) => {
const namespace = ensureUasNamespace();
const mappingKey = Store.generateMappingKey(UAS, [key]);
namespace.setMapping(mappingKey, {state: Success, value: Boolean(value)});
};
/**
* Build complete grant state map from current grants array.
*
* @private
* @param {string[]} grants
* @returns {Object<string, boolean>}
*/
const buildGrantStateMap = (grants) => {
const grantSet = new Set(Array.isArray(grants) ? grants : []);
const stateMap = {};
for (const grantKey of Object.values(this.USERGRANTLIST)) {
stateMap[grantKey] = grantSet.has(grantKey);
}
return stateMap;
};
/**
* Write the full state map into Store WITHOUT notifying watchers.
* Used only for first-time initialization.
*
* @private
* @param {Object<string, boolean>} stateMap
*/
const initializeGrantStoreSilently = (stateMap) => {
for (const grantKey of Object.values(this.USERGRANTLIST)) {
writeGrantStateToStore(grantKey, Boolean(stateMap[grantKey]));
}
uasStoreInitialized = true;
};
/**
* Notify watchers of one specific grant.
*
* @private
* @param {string} key
* @param {boolean} value
*/
const notifyGrantWatchers = (key, value) => {
const watchers = uasGrantWatchers.get(key);
if (!watchers || watchers.size === 0) return;
for (const callback of watchers) {
try {
callback(value);
} catch (err) {
Logger.w(
WarningType.RobotOperation,
`monitorUserGrant callback failed for ${key}: ${err && err.message ? err.message : err}`,
);
}
}
};
/**
* Refresh Store from controller after a UAS event,
* then notify watchers only for grants whose boolean changed.
*
* @private
* @returns {Promise<void>}
*/
const refreshUasGrantStoreFromEvent = async () => {
if (uasRefreshPromise) {
return uasRefreshPromise;
}
uasRefreshPromise = (async () => {
try {
const grants = await this.getUserGrants();
const nextStateMap = buildGrantStateMap(grants);
for (const grantKey of Object.values(this.USERGRANTLIST)) {
const prevValue = readGrantStateFromStore(grantKey);
const nextValue = Boolean(nextStateMap[grantKey]);
writeGrantStateToStore(grantKey, nextValue);
if (prevValue !== nextValue) {
notifyGrantWatchers(grantKey, nextValue);
}
}
Logger.i(InfoType.RobotOperation, 'UAS grant store refreshed from event.');
} catch (err) {
Logger.w(
WarningType.RobotOperation,
`Failed to refresh UAS grant store from event: ${err && err.message ? err.message : err}`,
);
throw err;
} finally {
uasRefreshPromise = null;
}
})();
return uasRefreshPromise;
};
/**
* Ensure the UAS subscription object exists.
*
* @private
*/
const ensureUasSubscriptionObject = () => {
if (uasSubscription) return;
uasSubscription = {
getResourceString() {
return '/rw/system/uas';
},
getTitle() {
return 'UAS Subscription';
},
onchanged: async (newValue) => {
// Logger.i(InfoType.RobotOperation, `UAS event received: ${JSON.stringify(newValue)}`);
try {
await refreshUasGrantStoreFromEvent();
} catch (err) {
Logger.w(
WarningType.RobotOperation,
`Failed to process UAS event: ${err && err.message ? err.message : err}`,
);
}
},
};
};
/**
* Ensure UAS is fully initialized:
* 1) start subscription
* 2) fetch grants once
* 3) write Store silently
*
* IMPORTANT:
* - No watcher notification during initialization
* - Concurrent calls share the same promise
*
* @private
* @returns {Promise<void>}
*/
const ensureUasInitialized = () => {
if (uasStoreInitialized) {
return Promise.resolve();
}
if (uasInitPromise) {
return uasInitPromise;
}
uasInitPromise = (async () => {
await this.subscribeRes();
const grants = await this.getUserGrants();
const stateMap = buildGrantStateMap(grants);
initializeGrantStoreSilently(stateMap);
Logger.i(InfoType.RobotOperation, 'UAS grant store initialized.');
})().finally(() => {
uasInitPromise = null;
});
return uasInitPromise;
};
// =========================
// public api
// =========================
/**
* Gets the user grants.
* @alias getUserGrants
* @memberof API.UAS
* @returns {Promise<string[]>}
* @example
* await API.UAS.getUserGrants()
*/
this.getUserGrants = async function () {
// get logged user name
let loginInfo = await RWS.UAS.getUser();
let userName = loginInfo['name'];
let userGrants = await API.RWS.UAS.getGrantsOfUser(userName);
return userGrants;
};
/**
* Checks whether the logged in user has function-required grants.
* @alias hasSpecificGrants
* @memberof API.UAS
* @param {API.UAS.GRANTTYPES} [type] Funciton types.
* @returns {Promise<boolean>}
* @example
* await API.UAS.hasSpecificGrants(API.UAS.GRANTTYPES.loadModule)
*/
this.hasSpecificGrants = async function (type) {
this.userGrants = await this.getUserGrants();
// Logger.d(InfoType.RobotOperation, `The current user owned the grants: ${this.userGrants.toString()}`);
let requiredGrants = [];
switch (type) {
case this.GRANTTYPES.loadModule:
requiredGrants = [this.USERGRANTLIST.UAS_FILE_ACCESS_READ_WRITE, this.USERGRANTLIST.UAS_RAPID_LOADPROGRAM];
break;
case this.GRANTTYPES.deploy:
requiredGrants = [this.USERGRANTLIST.UAS_FILE_ACCESS_READ_WRITE];
break;
case this.GRANTTYPES.updatePosition:
requiredGrants = [this.USERGRANTLIST.UAS_RAPID_CURRVALUE];
break;
case this.GRANTTYPES.ioWrite:
requiredGrants = [this.USERGRANTLIST.UAS_IO_WRITE];
break;
case this.GRANTTYPES.editRAPID:
requiredGrants = [this.USERGRANTLIST.UAS_RAPID_EDIT];
break;
case this.GRANTTYPES.executeProgram:
requiredGrants = [this.USERGRANTLIST.UAS_RAPID_EXECUTE];
break;
default:
break;
}
return requiredGrants.every((grant) => this.userGrants.includes(grant));
};
this.isLoggedInAsLocalClient = async function () {
let loginInfo = await RWS.UAS.getUser();
return (loginInfo['name'] && loginInfo['locale'] && loginInfo['locale'] == 'local') || false;
};
/**
* Start UAS subscription only.
* Does not initialize Store by itself.
*
* @returns {Promise<void>}
*/
this.subscribeRes = () => {
if (uasSubscribed) {
return Promise.resolve();
}
if (uasSubscribePromise) {
return uasSubscribePromise;
}
ensureUasSubscriptionObject();
uasSubscribePromise = RWS.Subscriptions.subscribe([uasSubscription])
.then(() => {
uasSubscribed = true;
Logger.i(InfoType.RobotOperation, 'UAS subscription successfully registered.');
})
.catch((err) => {
uasSubscribed = false;
Logger.w(WarningType.RobotOperation, `UAS subscription failed: ${err && err.message ? err.message : err}`);
return API.rejectWithStatus(`Subscription to ${uasSubscription.getTitle()} failed.`, err, {
errorCode: ErrorCode.FailedToSubscribeResource,
});
})
.finally(() => {
uasSubscribePromise = null;
});
return uasSubscribePromise;
};
/**
* Stop UAS subscription.
*
* @returns {Promise<void>}
*/
this.unsubscribeRes = () => {
if (uasUnsubscribePromise) {
return uasUnsubscribePromise;
}
if (!uasSubscribed || !uasSubscription) {
return Promise.resolve();
}
uasUnsubscribePromise = RWS.Subscriptions.unsubscribe([uasSubscription])
.then(() => {
uasSubscribed = false;
Logger.i(InfoType.RobotOperation, 'UAS subscription successfully unsubscribed.');
})
.catch((err) => {
Logger.w(WarningType.RobotOperation, `Failed to unsubscribe UAS: ${err && err.message ? err.message : err}`);
return API.rejectWithStatus(`Unsubscription from ${uasSubscription.getTitle()} failed.`, err, {
errorCode: ErrorCode.FailedToUnsubscribeResource,
});
})
.finally(() => {
uasUnsubscribePromise = null;
});
return uasUnsubscribePromise;
};
/**
* Monitor one specific grant.
*
* Behavior:
* 1) Ensure Store is initialized silently
* 2) Register watcher
* 3) Invoke ONLY this callback once with current cached value
*
* It does NOT trigger other callbacks.
*
* @param {string} key Grant name
* @param {Function} callback Receives boolean
* @returns {Promise<Function>} disposer
*/
this.monitorUserGrant = async (key, callback) => {
if (!Object.values(this.USERGRANTLIST).includes(key)) {
throw new Error(`Unknown UAS grant key: ${String(key)}`);
}
if (typeof callback !== 'function') {
throw new Error('monitorUserGrant requires callback to be a function.');
}
// Ensure subscription + store init once, silently
await ensureUasInitialized();
// Register watcher AFTER initialization
if (!uasGrantWatchers.has(key)) {
uasGrantWatchers.set(key, new Set());
}
uasGrantWatchers.get(key).add(callback);
// Call only this callback once with current value
const currentValue = Boolean(readGrantStateFromStore(key));
callback(currentValue);
// Return disposer for this callback
return () => {
this.unmonitorUserGrant(key, callback);
};
};
/**
* Remove one watcher callback for a grant.
*
* @param {string} key
* @param {Function} [callback]
*/
this.unmonitorUserGrant = (key, callback) => {
const watchers = uasGrantWatchers.get(key);
if (!watchers) return;
if (typeof callback === 'function') {
watchers.delete(callback);
} else {
watchers.clear();
}
if (watchers.size === 0) {
uasGrantWatchers.delete(key);
}
};
/**
* Optional full cleanup helper.
*
* @returns {Promise<void>}
*/
this.disposeUasMonitoring = async () => {
try {
await this.unsubscribeRes();
} finally {
uasGrantWatchers.clear();
uasSubscription = null;
uasSubscribed = false;
uasSubscribePromise = null;
uasUnsubscribePromise = null;
uasStoreInitialized = false;
uasInitPromise = null;
uasRefreshPromise = null;
}
};
})();
es.constructUasManagement = true;
};
if (typeof API.constructUasManagement === 'undefined') {
factoryApiUasManagement(API);
}
export default API;
export {factoryApiUasManagement};