rtos-views
Version:
RTOS views for microcontrollers
760 lines (671 loc) • 31.3 kB
text/typescript
/* eslint-disable @typescript-eslint/naming-convention */
import * as vscode from 'vscode';
import { DebugProtocol } from '@vscode/debugprotocol';
import * as RTOSCommon from './rtos-common';
// We will have two rows of headers for uC/OS-II and the table below describes
// the columns headers for the two rows and the width of each column as a fraction
// of the overall space.
enum DisplayFields {
ID,
Address,
TaskName,
Status,
Priority,
StackPercent,
StackPeakPercent
}
const RTOSUCOS2Items: { [key: string]: RTOSCommon.DisplayColumnItem } = {};
RTOSUCOS2Items[DisplayFields[DisplayFields.ID]] = {
width: 1,
headerRow1: '',
headerRow2: 'ID',
colType: RTOSCommon.ColTypeEnum.colTypeNumeric
};
RTOSUCOS2Items[DisplayFields[DisplayFields.Address]] = {
width: 2,
headerRow1: '',
headerRow2: 'Address',
colGapBefore: 1
};
RTOSUCOS2Items[DisplayFields[DisplayFields.TaskName]] = {
width: 4,
headerRow1: '',
headerRow2: 'Name',
colGapBefore: 1
};
RTOSUCOS2Items[DisplayFields[DisplayFields.Status]] = {
width: 4,
headerRow1: 'Thread',
headerRow2: 'Status',
colType: RTOSCommon.ColTypeEnum.colTypeCollapse
};
RTOSUCOS2Items[DisplayFields[DisplayFields.Priority]] = {
width: 1,
headerRow1: 'Prio',
headerRow2: 'rity',
colType: RTOSCommon.ColTypeEnum.colTypeNumeric,
colGapAfter: 1
}; // 3 are enough but 4 aligns better with header text
RTOSUCOS2Items[DisplayFields[DisplayFields.StackPercent]] = {
width: 4,
headerRow1: 'Stack Usage',
headerRow2: '% (Used B / Size B)',
colType: RTOSCommon.ColTypeEnum.colTypePercentage
};
RTOSUCOS2Items[DisplayFields[DisplayFields.StackPeakPercent]] = {
width: 4,
headerRow1: 'Stack Peak Usage',
headerRow2: '% (Peak B / Size B)',
colType: RTOSCommon.ColTypeEnum.colTypePercentage
};
const DisplayFieldNames: string[] = Object.keys(RTOSUCOS2Items);
export class RTOSUCOS2 extends RTOSCommon.RTOSBase {
// We keep a bunch of variable references (essentially pointers) that we can use to query for values
// Since all of them are global variable, we only need to create them once per session. These are
// similar to Watch/Hover variables
private OSRunning: RTOSCommon.RTOSVarHelperMaybe;
private OSRunningVal: number = 0;
private stackEntrySize: number = 0;
private OSTaskCtr: RTOSCommon.RTOSVarHelperMaybe;
private OSTaskCtrVal: number = 0;
private OSTCBList: RTOSCommon.RTOSVarHelperMaybe;
private OSTCBCur: RTOSCommon.RTOSVarHelperMaybe;
private OSTCBCurVal: number = 0;
private OSFlagTbl: RTOSCommon.RTOSVarHelperMaybe;
private stale: boolean = true;
private foundThreads: RTOSCommon.RTOSThreadInfo[] = [];
private finalThreads: RTOSCommon.RTOSThreadInfo[] = [];
private timeInfo: string = '';
private readonly maxThreads = 1024;
private stackPattern = 0x00;
private stackIncrements = -1; // negative numbers => OS_STK_GROWTH = OS_STK_GROWTH_HI_TO_LO (1)
private helpHtml: string | undefined;
constructor(public session: vscode.DebugSession) {
super(session, 'uC/OS-II');
if (session.configuration.rtosViewConfig) {
if (session.configuration.rtosViewConfig.stackPattern) {
this.stackPattern = parseInt(session.configuration.rtosViewConfig.stackPattern);
}
if (session.configuration.rtosViewConfig.stackGrowth) {
this.stackIncrements = parseInt(session.configuration.rtosViewConfig.stackGrowth);
}
}
}
public async tryDetect(useFrameId: number): Promise<RTOSCommon.RTOSBase> {
this.progStatus = 'stopped';
try {
if (this.status === 'none') {
// We only get references to all the interesting variables. Note that any one of the following can fail
// and the caller may try again until we know that it definitely passed or failed. Note that while we
// re-try everything, we do remember what already had succeeded and don't waste time trying again. That
// is how this.getVarIfEmpty() works
this.OSRunning = await this.getVarIfEmpty(this.OSRunning, useFrameId, 'OSRunning', false);
this.OSTaskCtr = await this.getVarIfEmpty(this.OSTaskCtr, useFrameId, 'OSTaskCtr', false);
this.OSTCBList = await this.getVarIfEmpty(this.OSTCBList, useFrameId, 'OSTCBList', false);
this.OSTCBCur = await this.getVarIfEmpty(this.OSTCBCur, useFrameId, 'OSTCBCur', false);
this.OSFlagTbl = await this.getVarIfEmpty(this.OSFlagTbl, useFrameId, 'OSFlagTbl', true);
this.status = 'initialized';
}
return this;
} catch (e) {
if (e instanceof RTOSCommon.ShouldRetry) {
console.error(e.message);
} else {
this.status = 'failed';
this.failedWhy = e;
}
return this;
}
}
protected createHmlHelp(th: RTOSCommon.RTOSThreadInfo, thInfo: RTOSCommon.RTOSStrToValueMap) {
function strong(text: string) {
return `<strong>${text}</strong>`;
}
if (this.helpHtml === undefined) {
this.helpHtml = '';
try {
let ret: string = '';
if (!thInfo['OSTCBTaskName']?.val) {
ret += `Thread name missing: Enable macro ${strong('OS_TASK_NAME_EN')} and use
${strong('OSTaskNameSet')} in FW<br><br>`;
}
if (!thInfo['OSTCBId']?.val || !th.stackInfo.stackSize) {
ret +=
`Thread ID & Stack Size & Peak missing: Enable macro ${strong('OS_TASK_CREATE_EXT_EN')} and` +
`use ${strong('OSTaskCreateExt')} in FW<br><br>`;
}
if (ret) {
ret +=
'Note: Make sure you consider the performance/resources impact for any changes to your FW.<br>\n';
this.helpHtml =
'<button class="help-button">Hints to get more out of the uC/OS-II RTOS View</button>\n' +
`<div class="help"><p>\n${ret}\n</p></div>\n`;
}
} catch (e) {
console.log(e);
}
}
}
public refresh(frameId: number): Promise<void> {
return new Promise<void>((resolve) => {
if (this.progStatus !== 'stopped') {
resolve();
return;
}
const timer = new RTOSCommon.HrTimer();
this.stale = true;
this.timeInfo = new Date().toISOString();
// OSRunning & OSTaskCtr can go invalid anytime. Like when a reset/restart happens
this.OSTaskCtrVal = Number.MAX_SAFE_INTEGER;
this.OSRunningVal = Number.MAX_SAFE_INTEGER;
this.foundThreads = [];
this.OSRunning?.getValue(frameId).then(
async (str) => {
try {
this.OSRunningVal = str ? parseInt(str) : 0;
if (0 !== this.OSRunningVal) {
const count = await this.OSTaskCtr?.getValue(frameId);
this.OSTaskCtrVal = count ? parseInt(count) : Number.MAX_SAFE_INTEGER;
if (this.OSTaskCtrVal > 0 && this.OSTaskCtrVal <= this.maxThreads) {
const OSTCBListVal = await this.OSTCBList?.getValue(frameId);
if (OSTCBListVal && 0 !== parseInt(OSTCBListVal)) {
if (this.stackEntrySize === 0) {
/* Only get stack entry size once per session */
const stackEntrySizeRef = await this.getExprVal('sizeof(OS_STK)', frameId);
this.stackEntrySize = parseInt(stackEntrySizeRef || '');
}
const osFlagTblVal = this.OSFlagTbl
? await this.OSFlagTbl.getVarChildren(frameId)
: [];
const flagPendMap = await this.getPendingFlagGroupsForTasks(osFlagTblVal, frameId);
const tmpOSTCBCurVal = await this.OSTCBCur?.getValue(frameId);
this.OSTCBCurVal = tmpOSTCBCurVal
? parseInt(tmpOSTCBCurVal)
: Number.MAX_SAFE_INTEGER;
await this.getThreadInfo(this.OSTCBList, flagPendMap, frameId);
if (this.foundThreads[0].display['ID'].text !== '???') {
this.foundThreads.sort(
(a, b) => parseInt(a.display['ID'].text) - parseInt(b.display['ID'].text)
);
} else {
this.foundThreads.sort(
(a, b) =>
parseInt(a.display['Address'].text) -
parseInt(b.display['Address'].text)
);
}
}
this.finalThreads = [...this.foundThreads];
} else {
this.finalThreads = [];
}
} else {
this.finalThreads = [];
}
this.stale = false;
this.timeInfo += ' in ' + timer.deltaMs() + ' ms';
resolve();
} catch (e) {
resolve();
console.error('RTOSUCOS2.refresh() failed: ', e);
}
},
(reason) => {
resolve();
console.error('RTOSUCOS2.refresh() failed: ', reason);
}
);
});
}
private getThreadInfo(
tcbListEntry: RTOSCommon.RTOSVarHelperMaybe,
flagPendMap: Map<number, FlagGroup[]>,
frameId: number
): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!tcbListEntry || !tcbListEntry.varReference || this.foundThreads.length >= this.OSTaskCtrVal) {
resolve();
return;
}
if (this.progStatus !== 'stopped') {
reject(new Error('Busy'));
return;
}
tcbListEntry.getVarChildrenObj(frameId).then(
async (obj: RTOSCommon.RTOSStrToValueMap) => {
try {
let curTaskObj = obj;
let thAddress = parseInt(tcbListEntry?.value || '');
let threadCount = 1;
do {
const threadId = curTaskObj['OSTCBId']?.val;
let thName = '???';
if (curTaskObj['OSTCBTaskName']) {
const tmpThName = await this.getExprVal('(char *)' + curTaskObj['OSTCBTaskName']?.exp, frameId) || '';
const matchName = tmpThName.match(/"([^*]*)"$/);
thName = matchName ? matchName[1] : tmpThName;
}
const threadRunning = thAddress === this.OSTCBCurVal;
const thStateObject = await this.analyzeTaskState(
thAddress,
curTaskObj,
flagPendMap,
frameId
);
const thState = thStateObject.describe();
const stackInfo = await this.getStackInfo(curTaskObj, this.stackPattern, frameId);
const display: { [key: string]: RTOSCommon.DisplayRowItem } = {};
const mySetter = (x: DisplayFields, text: string, value?: any) => {
display[DisplayFieldNames[x]] = { text, value };
};
mySetter(DisplayFields.ID, threadId ? parseInt(threadId).toString() : '???');
mySetter(DisplayFields.Address, RTOSCommon.hexFormat(thAddress));
mySetter(DisplayFields.TaskName, thName);
mySetter(
DisplayFields.Status,
threadRunning ? 'RUNNING' : thState,
thStateObject.fullData()
);
mySetter(DisplayFields.Priority, parseInt(curTaskObj['OSTCBPrio']?.val).toString());
if (stackInfo.stackUsed !== undefined && stackInfo.stackSize !== undefined) {
const stackPercentVal = Math.round((stackInfo.stackUsed / stackInfo.stackSize) * 100);
const stackPercentText = `${stackPercentVal} % (${stackInfo.stackUsed} / ${stackInfo.stackSize})`;
mySetter(DisplayFields.StackPercent, stackPercentText, stackPercentVal);
} else {
mySetter(DisplayFields.StackPercent, '?? %');
}
if (stackInfo.stackPeak !== undefined && stackInfo.stackSize !== undefined) {
const stackPeakPercentVal =
Math.round((stackInfo.stackPeak / stackInfo.stackSize) * 100);
const stackPeakPercentText = `${stackPeakPercentVal.toString().padStart(3)} %` +
` (${stackInfo.stackPeak} / ${stackInfo.stackSize})`;
mySetter(DisplayFields.StackPeakPercent, stackPeakPercentText, stackPeakPercentVal);
} else {
mySetter(DisplayFields.StackPeakPercent, '?? %');
}
const thread: RTOSCommon.RTOSThreadInfo = {
display: display,
stackInfo: stackInfo,
running: threadRunning
};
this.foundThreads.push(thread);
this.createHmlHelp(thread, curTaskObj);
thAddress = parseInt(curTaskObj['OSTCBNext']?.val);
if (0 !== thAddress) {
const nextThreadObj = await this.getVarChildrenObj(
curTaskObj['OSTCBNext']?.ref,
'OSTCBNext'
);
curTaskObj = nextThreadObj || {};
threadCount++;
}
if (threadCount > this.OSTaskCtrVal) {
console.log(
'RTOSUCOS2.getThreadInfo() detected more threads in OSTCBCur linked list that OSTaskCtr states'
);
break;
}
} while (0 !== thAddress);
resolve();
} catch (e) {
console.log('RTOSUCOS2.getThreadInfo() error', e);
}
},
(e) => {
reject(e);
}
);
});
}
protected async getEventInfo(
address: number,
eventObject: RTOSCommon.RTOSStrToValueMap,
_frameId: number
): Promise<EventInfo> {
const eventInfo: EventInfo = { address, eventType: parseInt(eventObject['OSEventType']?.val) };
if (eventObject['OSEventName']?.val) {
const value = eventObject['OSEventName']?.val;
const matchName = value.match(/"(.*)"$/);
eventInfo.name = matchName ? matchName[1] : value;
}
return eventInfo;
}
protected async readEventArray(baseAddress: number, frameId: number): Promise<EventInfo[]> {
const result = [];
for (let eventIndex = 0; ; eventIndex++) {
const eventAddress = parseInt(
(await this.getExprVal(`((OS_EVENT**)(${baseAddress}))[${eventIndex}]`, frameId)) || ''
);
if (eventAddress === 0) {
break;
} else {
const eventObject = await this.getExprValChildrenObj(`(OS_EVENT*)(${eventAddress})`, frameId);
result.push(await this.getEventInfo(eventAddress, eventObject, frameId));
}
}
return result;
}
protected async analyzeTaskState(
threadAddr: number,
curTaskObj: any,
flagPendMap: Map<number, FlagGroup[]>,
frameId: number
): Promise<TaskState> {
const state = parseInt(curTaskObj['OSTCBStat']?.val);
switch (state) {
case OsTaskState.READY:
return new TaskReady();
case OsTaskState.SUSPENDED:
return new TaskSuspended();
default: {
const resultState = new TaskPending();
PendingTaskStates.forEach((candidateState) => {
if ((state & candidateState) === candidateState) {
resultState.addEventType(getEventTypeForTaskState(candidateState));
}
});
if (curTaskObj['OSTCBEventPtr']?.val) {
const eventAddress = parseInt(curTaskObj['OSTCBEventPtr']?.val);
if (eventAddress !== 0) {
const event = await this.getVarChildrenObj(curTaskObj['OSTCBEventPtr']?.ref, 'OSTCBEventPtr');
if (event) {
const eventInfo = await this.getEventInfo(eventAddress, event, frameId);
resultState.addEvent(eventInfo);
}
}
}
if (curTaskObj['OSTCBEventMultiPtr']?.val) {
const eventMultiBaseAddress = parseInt(curTaskObj['OSTCBEventMultiPtr']?.val);
if (eventMultiBaseAddress !== 0) {
(await this.readEventArray(eventMultiBaseAddress, frameId)).forEach((eventInfo) =>
resultState.addEvent(eventInfo)
);
}
}
if (flagPendMap.has(threadAddr)) {
flagPendMap.get(threadAddr)?.forEach((flagGroup) =>
resultState.addEvent({
name: flagGroup.name,
eventType: OsEventType.Flag,
address: flagGroup.address,
})
);
}
return resultState;
}
}
}
protected async getPendingFlagGroupsForTasks(
osFlagTable: DebugProtocol.Variable[],
frameId: number
): Promise<Map<number, FlagGroup[]>> {
// Builds a map from task IDs to flag groups that the tasks are pending on
const result: Map<number, FlagGroup[]> = new Map();
for (const flagGroupPtr of osFlagTable) {
if (flagGroupPtr.variablesReference > 0 && flagGroupPtr.evaluateName) {
const osFlagGrp = await this.getVarChildrenObj(flagGroupPtr.variablesReference, flagGroupPtr.name);
// Check if we are looking at an initialized flag group
if (osFlagGrp && parseInt(osFlagGrp['OSFlagType']?.val) === OsEventType.Flag) {
const groupAddr = parseInt(
(await this.getExprVal(`&(${flagGroupPtr.evaluateName})`, frameId)) || ''
);
const flagGroup: FlagGroup = { address: groupAddr };
const reprValue = osFlagGrp['OSFlagName']?.val;
if (reprValue) {
const matchName = reprValue.match(/"(.*)"$/);
flagGroup.name = matchName ? matchName[1] : reprValue;
}
// Follow the linked list of flag group nodes. The cast is safe here because we checked OSFlagType before
let flagNode = await this.getExprValChildrenObj(
`(OS_FLAG_NODE *)(${osFlagGrp['OSFlagWaitList']?.exp})`,
frameId
);
while (flagNode) {
const waitingTcbAddr = parseInt(flagNode['OSFlagNodeTCB']?.val || '');
if (!waitingTcbAddr || waitingTcbAddr === 0) {
break;
}
if (!result.has(waitingTcbAddr)) {
result.set(waitingTcbAddr, []);
}
result.get(waitingTcbAddr)?.push(flagGroup);
const nextFlagNodeAddr = parseInt(flagNode['OSFlagNodeNext']?.val);
if (nextFlagNodeAddr === 0) {
break;
} else {
// Need to cast here since the next pointer is declared as void *
flagNode = await this.getExprValChildrenObj(
`(OS_FLAG_NODE *) ${nextFlagNodeAddr}`,
frameId
);
}
}
}
}
}
return result;
}
protected async getStackInfo(thInfo: RTOSCommon.RTOSStrToValueMap, stackPattern: number, _frameId: number) {
const TopOfStack = thInfo['OSTCBStkPtr']?.val;
/* only available with OS_TASK_CREATE_EXT_EN (optional) */
const EndOfStack = parseInt(thInfo['OSTCBStkBottom']?.val) || 0;
const StackSize = parseInt(thInfo['OSTCBStkSize']?.val) || 0;
let Stack = 0;
if (EndOfStack !== 0 && StackSize !== 0) {
if (this.stackIncrements < 0) {
Stack = EndOfStack + StackSize * this.stackEntrySize;
}
else {
Stack = EndOfStack - StackSize * this.stackEntrySize;
}
}
else {
/* As stackStart is mandatory, we need to set it to some reasonable value */
Stack = parseInt(TopOfStack);
}
const stackInfo: RTOSCommon.RTOSStackInfo = {
stackStart: Stack,
stackTop: parseInt(TopOfStack)
};
if (EndOfStack !== 0 && StackSize !== 0) {
stackInfo.stackEnd = EndOfStack;
stackInfo.stackSize = StackSize * this.stackEntrySize;
if (this.stackIncrements < 0) {
const stackDelta = stackInfo.stackStart - stackInfo.stackTop;
stackInfo.stackFree = stackInfo.stackSize - stackDelta;
stackInfo.stackUsed = stackDelta;
} else {
const stackDelta = stackInfo.stackTop - stackInfo.stackStart;
stackInfo.stackFree = stackDelta;
stackInfo.stackUsed = stackInfo.stackSize - stackDelta;
}
/* check stack peak */
const memArg: DebugProtocol.ReadMemoryArguments = {
memoryReference: RTOSCommon.hexFormat(Math.min(stackInfo.stackTop, stackInfo.stackEnd)),
count: stackInfo.stackFree
};
try {
const stackData = await this.session.customRequest('readMemory', memArg);
const buf = Buffer.from(stackData.data, 'base64');
stackInfo.bytes = new Uint8Array(buf);
let start = this.stackIncrements < 0 ? 0 : stackInfo.bytes.length - 1;
const end = this.stackIncrements < 0 ? stackInfo.bytes.length : -1;
let peak = 0;
while (start !== end) {
if (stackInfo.bytes[start] !== stackPattern) {
break;
}
start -= this.stackIncrements;
peak++;
}
stackInfo.stackPeak = stackInfo.stackSize - peak;
} catch (e) {
console.log(e);
}
}
return stackInfo;
}
public lastValidHtmlContent: RTOSCommon.HtmlInfo = { html: '', css: '' };
public getHTML(): RTOSCommon.HtmlInfo {
const htmlContent: RTOSCommon.HtmlInfo = { html: '', css: '' };
// WARNING: This stuff is super fragile. Once we know how this works, then we should refactor this
let msg = '';
if (this.status === 'none') {
htmlContent.html = '<p>RTOS not yet fully initialized. Will occur next time program pauses</p>\n';
return htmlContent;
} else if (this.stale) {
const lastHtmlInfo = this.lastValidHtmlContent;
if (this.OSTaskCtrVal === Number.MAX_SAFE_INTEGER) {
msg = ' Could not read "OSTaskCtr". Perhaps program is busy or did not stop long enough';
lastHtmlInfo.html = '';
lastHtmlInfo.css = '';
} else if (this.OSTaskCtrVal > this.maxThreads) {
msg = ` uC/OS-II variable OSTaskCtr = ${this.OSTaskCtrVal} seems invalid`;
lastHtmlInfo.html = '';
lastHtmlInfo.css = '';
} else if (lastHtmlInfo.html) {
// TODO check if this condition is ok
msg = ' Following info from last query may be stale.';
}
htmlContent.html = `<p>Unable to collect full RTOS information.${msg}</p>\n` + lastHtmlInfo.html;
htmlContent.css = lastHtmlInfo.css;
return htmlContent;
} else if (this.OSTaskCtrVal !== Number.MAX_SAFE_INTEGER && this.finalThreads.length !== this.OSTaskCtrVal) {
msg += `<p>Expecting ${this.OSTaskCtrVal} threads, found ${this.finalThreads.length}. Thread data may be unreliable<p>\n`;
} else if (this.finalThreads.length === 0) {
htmlContent.html = `<p>No ${this.name} threads detected, perhaps RTOS not yet initialized or tasks yet to be created!</p>\n`;
return htmlContent;
}
const ret = this.getHTMLCommon(DisplayFieldNames, RTOSUCOS2Items, this.finalThreads, this.timeInfo);
htmlContent.html = msg + ret.html + (this.helpHtml || '');
htmlContent.css = ret.css;
this.lastValidHtmlContent = htmlContent;
// console.log(this.lastValidHtmlContent.html);
return this.lastValidHtmlContent;
}
}
enum OsTaskState {
READY = 0x00,
SUSPENDED = 0x08,
PEND_SEMAPHORE = 0x01,
PEND_MAILBOX = 0x02,
PEND_QUEUE = 0x04,
PEND_MUTEX = 0x10,
PEND_FLAGGROUP = 0x20
}
const PendingTaskStates = [
OsTaskState.PEND_SEMAPHORE,
OsTaskState.PEND_MAILBOX,
OsTaskState.PEND_QUEUE,
OsTaskState.PEND_MUTEX,
OsTaskState.PEND_FLAGGROUP
] as const;
enum OsEventType {
Mailbox = 1,
Queue = 2,
Semaphore = 3,
Mutex = 4,
Flag = 5
}
function getEventTypeForTaskState(state: OsTaskState): OsEventType {
switch (state) {
case OsTaskState.PEND_SEMAPHORE:
return OsEventType.Semaphore;
case OsTaskState.PEND_MAILBOX:
return OsEventType.Mailbox;
case OsTaskState.PEND_QUEUE:
return OsEventType.Queue;
case OsTaskState.PEND_MUTEX:
return OsEventType.Mutex;
case OsTaskState.PEND_FLAGGROUP:
return OsEventType.Flag;
default:
return OsEventType.Flag; // Should not happen, but we need something for lint
}
}
abstract class TaskState {
public abstract describe(): string;
public abstract fullData(): any;
}
class TaskReady extends TaskState {
public describe(): string {
return 'READY';
}
public fullData(): any {
return null;
}
}
class TaskSuspended extends TaskState {
public describe(): string {
return 'SUSPENDED';
}
public fullData(): any {
return null;
}
}
class TaskPending extends TaskState {
private pendingInfo: Map<OsEventType, EventInfo[]>;
constructor() {
super();
this.pendingInfo = new Map();
}
public addEvent(event: EventInfo) {
this.addEventType(event.eventType);
this.pendingInfo.get(event.eventType)?.push(event);
}
public addEventType(eventType: OsEventType) {
if (!this.pendingInfo.has(eventType)) {
this.pendingInfo.set(eventType, []);
}
}
public describe(): string {
// Converting to an array here is inefficient, but JS has no builtin iterator map/reduce feature
const eventCount = [...this.pendingInfo.values()].reduce((acc, events) => acc + events.length, 0);
if (eventCount <= 1) {
let event: EventInfo | undefined;
for (const events of this.pendingInfo.values()) {
if (events.length > 0) {
event = events[0];
}
}
if (event) {
const eventTypeStr = OsEventType[event.eventType] ? OsEventType[event.eventType] : 'Unknown';
return `PEND ${eventTypeStr}: ${describeEvent(event)}`;
} else {
// This should not happen, but we still keep it as a fallback
return 'PEND Unknown';
}
} else {
return 'PEND MULTI';
}
}
public fullData() {
// Build an object containing mapping event types to event descriptions
const result: any = {};
const eventTypes = [...this.pendingInfo.keys()];
eventTypes.sort();
for (const eventType of eventTypes) {
result[OsEventType[eventType]] = [];
for (const event of this.pendingInfo.get(eventType) || []) {
result[OsEventType[eventType]].push(describeEvent(event));
}
}
return result;
}
}
interface EventInfo {
name?: string;
address: number;
eventType: OsEventType;
}
function describeEvent(event: EventInfo): string {
if (event.name && event.name !== '?') {
return event.name;
} else {
return `0x${event.address.toString(16)}`;
}
}
interface FlagGroup {
name?: string;
address: number;
}