tassello 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +66 -3
- package/dist/server/behavior_tree_file.d.ts +17 -0
- package/dist/server/behavior_tree_file.js +232 -0
- package/dist/server/behavior_tree_files.d.ts +18 -0
- package/dist/server/behavior_tree_files.js +405 -0
- package/dist/server/behavior_tree_log.d.ts +10 -0
- package/dist/server/behavior_tree_log.js +132 -0
- package/dist/server/behavior_tree_playback.d.ts +57 -0
- package/dist/server/behavior_tree_playback.js +188 -0
- package/dist/server/behavior_tree_state.d.ts +38 -0
- package/dist/server/behavior_tree_state.js +220 -0
- package/dist/server/behavior_trees.d.ts +29 -0
- package/dist/server/behavior_trees.js +362 -0
- package/dist/server/files.d.ts +5 -2
- package/dist/server/files.js +32 -13
- package/dist/server/http.js +7 -5
- package/dist/server/rosbag.d.ts +25 -0
- package/dist/server/rosbag.js +418 -0
- package/dist/server/rosout.d.ts +16 -0
- package/dist/server/rosout.js +203 -0
- package/dist/server/rosout_log.d.ts +20 -0
- package/dist/server/rosout_log.js +66 -0
- package/dist/server/server.js +20 -4
- package/dist/server/session.d.ts +7 -0
- package/dist/server/session.js +63 -0
- package/dist/server/systemd.js +1 -1
- package/dist/server/systemd_format.d.ts +1 -0
- package/dist/server/systemd_format.js +19 -3
- package/dist/shared/bt_nodes.d.ts +25 -0
- package/dist/shared/bt_nodes.js +180 -0
- package/dist/shared/protocol.d.ts +157 -2
- package/dist/shared/protocol.js +4 -0
- package/dist/shared/xml.d.ts +25 -0
- package/dist/shared/xml.js +164 -0
- package/dist/types/client/api.d.ts +4 -3
- package/dist/types/client/connection.d.ts +31 -1
- package/dist/types/client/dialogs.d.ts +2 -1
- package/dist/types/client/icons.d.ts +24 -0
- package/dist/types/client/sdk/index.d.ts +3 -2
- package/dist/types/client/widgets/types.d.ts +5 -1
- package/dist/types/shared/bt_nodes.d.ts +25 -0
- package/dist/types/shared/protocol.d.ts +157 -2
- package/dist/www/assets/Joystick.js +1 -1
- package/dist/www/assets/RobotModelView.js +2 -2
- package/dist/www/assets/icons.js +1 -1
- package/dist/www/assets/index.css +1 -1
- package/dist/www/assets/index.js +28 -1
- package/dist/www/assets/shared/tassello-sdk.js +1 -1
- package/docs/plugins.md +8 -0
- package/package.json +2 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// A rosbag of a behavior tree's log played back for one page, as the live log
|
|
2
|
+
// would be (behavior_trees.ts): its transitions applied, on the bag's clock,
|
|
3
|
+
// to a state of their own, sent as they change; at the page's speed, from
|
|
4
|
+
// where it seeks. Read whole when opened: logs are small, and seeking back
|
|
5
|
+
// replays them from the start.
|
|
6
|
+
import { BT_RATE } from '../shared/protocol.js';
|
|
7
|
+
import { readTree } from './behavior_tree_file.js';
|
|
8
|
+
import { decodeLog, logLayout } from './behavior_tree_log.js';
|
|
9
|
+
import { BehaviorTreeState } from './behavior_tree_state.js';
|
|
10
|
+
import { bagFiles, readBag } from './rosbag.js';
|
|
11
|
+
import { absoluteTopic, errorMessage } from './util.js';
|
|
12
|
+
const TICK_MS = 50;
|
|
13
|
+
// How often the page is told where the playback is, while playing
|
|
14
|
+
const STATE_MS = 250;
|
|
15
|
+
const MIN_SPEED = 0.05;
|
|
16
|
+
const MAX_SPEED = 64;
|
|
17
|
+
// Beyond them, the bag is too long to play whole
|
|
18
|
+
const MAX_TRANSITIONS = 2_000_000;
|
|
19
|
+
// The bag's log: of the topic if it has it, else its first
|
|
20
|
+
export async function readLog(bag, topic) {
|
|
21
|
+
const logs = await readBag(await bagFiles(bag), (t) => /\/BehaviorTreeLog$/.test(t.type));
|
|
22
|
+
const found = (topic && logs.get(absoluteTopic(topic))) || [...logs.values()][0];
|
|
23
|
+
if (!found) {
|
|
24
|
+
throw new Error(`No behavior tree log in ${bag}: no nav2_msgs/msg/BehaviorTreeLog topic`);
|
|
25
|
+
}
|
|
26
|
+
const uids = logLayout(found.messages.map((message) => message.data));
|
|
27
|
+
if (uids === null) {
|
|
28
|
+
throw new Error(`No transition in ${bag}, of ${found.topic.name}`);
|
|
29
|
+
}
|
|
30
|
+
const messages = [];
|
|
31
|
+
let count = 0;
|
|
32
|
+
for (const { time, data } of found.messages) {
|
|
33
|
+
let transitions;
|
|
34
|
+
try {
|
|
35
|
+
transitions = decodeLog(data, uids);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
count += transitions.length;
|
|
41
|
+
if (count > MAX_TRANSITIONS) {
|
|
42
|
+
throw new Error(`${bag} is too long to play: more than ${MAX_TRANSITIONS} transitions`);
|
|
43
|
+
}
|
|
44
|
+
messages.push({ time, transitions });
|
|
45
|
+
}
|
|
46
|
+
return { topic: found.topic.name, messages };
|
|
47
|
+
}
|
|
48
|
+
export class BehaviorTreePlayback {
|
|
49
|
+
watcher;
|
|
50
|
+
nodes = [];
|
|
51
|
+
messages = [];
|
|
52
|
+
state = new BehaviorTreeState([]);
|
|
53
|
+
// The next message to apply
|
|
54
|
+
next = 0;
|
|
55
|
+
topic = '';
|
|
56
|
+
start = 0;
|
|
57
|
+
end = 0;
|
|
58
|
+
time = 0;
|
|
59
|
+
playing = false;
|
|
60
|
+
speed = 1;
|
|
61
|
+
error = null;
|
|
62
|
+
timer = null;
|
|
63
|
+
lastTick = 0;
|
|
64
|
+
lastFlush = 0;
|
|
65
|
+
lastState = 0;
|
|
66
|
+
closed = false;
|
|
67
|
+
constructor(options, watcher) {
|
|
68
|
+
this.watcher = watcher;
|
|
69
|
+
void this.open(options);
|
|
70
|
+
}
|
|
71
|
+
// What the page asks: any of them
|
|
72
|
+
control({ playing, speed, time }) {
|
|
73
|
+
if (this.messages.length === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (speed !== undefined && Number.isFinite(speed)) {
|
|
77
|
+
this.speed = Math.min(MAX_SPEED, Math.max(MIN_SPEED, speed));
|
|
78
|
+
}
|
|
79
|
+
if (time !== undefined && Number.isFinite(time)) {
|
|
80
|
+
this.seek(time);
|
|
81
|
+
}
|
|
82
|
+
if (playing !== undefined) {
|
|
83
|
+
// From the start again, once at the end
|
|
84
|
+
if (playing && this.time >= this.end) {
|
|
85
|
+
this.seek(this.start);
|
|
86
|
+
}
|
|
87
|
+
this.playing = playing;
|
|
88
|
+
if (playing && !this.timer) {
|
|
89
|
+
this.lastTick = Date.now();
|
|
90
|
+
this.timer = setInterval(() => this.tick(), TICK_MS);
|
|
91
|
+
}
|
|
92
|
+
else if (!playing) {
|
|
93
|
+
this.stopTimer();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.sendState();
|
|
97
|
+
}
|
|
98
|
+
close() {
|
|
99
|
+
this.closed = true;
|
|
100
|
+
this.stopTimer();
|
|
101
|
+
}
|
|
102
|
+
async open({ bag, topic, tree: source, uploadsDir, logger }) {
|
|
103
|
+
this.watcher.onStatus('waiting', `Reading ${bag}…`);
|
|
104
|
+
const [{ tree }, log] = await Promise.all([
|
|
105
|
+
readTree(source, uploadsDir, logger),
|
|
106
|
+
readLog(bag, topic).catch((e) => new Error(errorMessage(e))),
|
|
107
|
+
]);
|
|
108
|
+
if (this.closed) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.nodes = tree.nodes;
|
|
112
|
+
this.state = new BehaviorTreeState(this.nodes);
|
|
113
|
+
this.watcher.onTree(tree);
|
|
114
|
+
if (log instanceof Error) {
|
|
115
|
+
this.error = log.message;
|
|
116
|
+
this.watcher.onStatus('error', log.message);
|
|
117
|
+
this.watcher.onUpdate({ reset: true, ...this.state.snapshot() });
|
|
118
|
+
this.sendState();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.topic = log.topic;
|
|
122
|
+
this.messages = log.messages;
|
|
123
|
+
this.start = this.messages[0]?.time ?? 0;
|
|
124
|
+
this.end = this.messages.at(-1)?.time ?? 0;
|
|
125
|
+
this.time = this.start;
|
|
126
|
+
this.watcher.onStatus('streaming', `Playing ${bag}`);
|
|
127
|
+
this.seek(this.start);
|
|
128
|
+
this.control({ playing: true });
|
|
129
|
+
}
|
|
130
|
+
tick() {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
this.time = Math.min(this.end, this.time + (now - this.lastTick) * this.speed);
|
|
133
|
+
this.lastTick = now;
|
|
134
|
+
this.advance();
|
|
135
|
+
if (this.time >= this.end) {
|
|
136
|
+
this.playing = false;
|
|
137
|
+
this.stopTimer();
|
|
138
|
+
this.flush();
|
|
139
|
+
this.sendState();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (now - this.lastFlush >= 1000 / BT_RATE) {
|
|
143
|
+
this.flush();
|
|
144
|
+
}
|
|
145
|
+
if (now - this.lastState >= STATE_MS) {
|
|
146
|
+
this.sendState();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// The messages recorded until the playback's time
|
|
150
|
+
advance() {
|
|
151
|
+
while (this.next < this.messages.length && this.messages[this.next].time <= this.time) {
|
|
152
|
+
const { time, transitions } = this.messages[this.next++];
|
|
153
|
+
for (const transition of transitions) {
|
|
154
|
+
this.state.apply(transition, time);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Back: the state again from the start. The page gets it whole.
|
|
159
|
+
seek(time) {
|
|
160
|
+
const target = Math.min(this.end, Math.max(this.start, time));
|
|
161
|
+
if (target < this.time) {
|
|
162
|
+
this.state = new BehaviorTreeState(this.nodes);
|
|
163
|
+
this.next = 0;
|
|
164
|
+
}
|
|
165
|
+
this.time = target;
|
|
166
|
+
this.advance();
|
|
167
|
+
this.watcher.onUpdate({ reset: true, ...this.state.snapshot() });
|
|
168
|
+
this.lastFlush = Date.now();
|
|
169
|
+
}
|
|
170
|
+
flush() {
|
|
171
|
+
this.lastFlush = Date.now();
|
|
172
|
+
const changes = this.state.takeChanges();
|
|
173
|
+
if (changes) {
|
|
174
|
+
this.watcher.onUpdate({ reset: false, ...changes });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
sendState() {
|
|
178
|
+
this.lastState = Date.now();
|
|
179
|
+
const { topic, start, end, time, playing, speed, error } = this;
|
|
180
|
+
this.watcher.onPlayback({ topic, start, end, time, playing, speed, error });
|
|
181
|
+
}
|
|
182
|
+
stopTimer() {
|
|
183
|
+
if (this.timer) {
|
|
184
|
+
clearInterval(this.timer);
|
|
185
|
+
this.timer = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BtNode, BtNodeState, BtTransition } from '../shared/protocol.ts';
|
|
2
|
+
import type { LogTransition } from './behavior_tree_log.ts';
|
|
3
|
+
export interface Changes {
|
|
4
|
+
added: BtNode[];
|
|
5
|
+
states: BtNodeState[];
|
|
6
|
+
transitions: BtTransition[];
|
|
7
|
+
mismatch: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare class BehaviorTreeState {
|
|
10
|
+
readonly nodes: BtNode[];
|
|
11
|
+
private readonly treeSize;
|
|
12
|
+
private readonly states;
|
|
13
|
+
private readonly depths;
|
|
14
|
+
private readonly byName;
|
|
15
|
+
private readonly added;
|
|
16
|
+
private uidOffset;
|
|
17
|
+
private uidOffsets;
|
|
18
|
+
private uidSamples;
|
|
19
|
+
private uidMisses;
|
|
20
|
+
private uidUnfit;
|
|
21
|
+
private seq;
|
|
22
|
+
private readonly recent;
|
|
23
|
+
private readonly dirty;
|
|
24
|
+
private pending;
|
|
25
|
+
private addedSince;
|
|
26
|
+
private mismatchSent;
|
|
27
|
+
constructor(tree: BtNode[]);
|
|
28
|
+
apply(transition: LogTransition, now: number): void;
|
|
29
|
+
takeChanges(): Changes | null;
|
|
30
|
+
snapshot(): Changes;
|
|
31
|
+
private blank;
|
|
32
|
+
private stateOf;
|
|
33
|
+
private resolve;
|
|
34
|
+
private byUid;
|
|
35
|
+
private byStatus;
|
|
36
|
+
private add;
|
|
37
|
+
private mismatch;
|
|
38
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// What a behavior tree's nodes are doing, from the transitions of its log:
|
|
2
|
+
// which node each transition is of, then the node's status, last result and
|
|
3
|
+
// counts, and the latest transitions.
|
|
4
|
+
//
|
|
5
|
+
// The log names the nodes, and since Nav2 Jazzy gives their UIDs too, which
|
|
6
|
+
// BT.CPP numbers in the order of the tree's nodes (see behavior_tree_file.ts):
|
|
7
|
+
// the offset between the two is found from the first transitions, after which
|
|
8
|
+
// each UID is its node. Without UIDs, or when they do not fit the tree, nodes
|
|
9
|
+
// are found by name; between nodes of the same name (a RecoveryNode named
|
|
10
|
+
// after its action, the many Sequences), by what the transition says of the
|
|
11
|
+
// node's status and what is known of its parent's: BT.CPP only ticks, halts
|
|
12
|
+
// and resets the children of a running node.
|
|
13
|
+
// Kept for the pages to come, and between two updates
|
|
14
|
+
const MAX_TRANSITIONS = 300;
|
|
15
|
+
// Of the log not in the tree: beyond them, the log is not worth listing
|
|
16
|
+
const MAX_ADDED = 1000;
|
|
17
|
+
// UIDs that do not fit in a row: the robot runs another tree, look again
|
|
18
|
+
const UID_MISSES = 3;
|
|
19
|
+
export class BehaviorTreeState {
|
|
20
|
+
nodes;
|
|
21
|
+
treeSize;
|
|
22
|
+
states = [];
|
|
23
|
+
depths = [];
|
|
24
|
+
byName = new Map();
|
|
25
|
+
// The nodes added, by UID and name, or name
|
|
26
|
+
added = new Map();
|
|
27
|
+
uidOffset = null;
|
|
28
|
+
// The offsets the transitions so far allow, until one is left, and how
|
|
29
|
+
// many transitions agreed on them
|
|
30
|
+
uidOffsets = null;
|
|
31
|
+
uidSamples = 0;
|
|
32
|
+
uidMisses = 0;
|
|
33
|
+
uidUnfit = false;
|
|
34
|
+
seq = 0;
|
|
35
|
+
recent = [];
|
|
36
|
+
// Since the last takeChanges
|
|
37
|
+
dirty = new Set();
|
|
38
|
+
pending = [];
|
|
39
|
+
addedSince;
|
|
40
|
+
mismatchSent = null;
|
|
41
|
+
constructor(tree) {
|
|
42
|
+
this.nodes = [...tree];
|
|
43
|
+
this.treeSize = tree.length;
|
|
44
|
+
this.addedSince = tree.length;
|
|
45
|
+
tree.forEach((node, index) => {
|
|
46
|
+
this.states.push(this.blank(index));
|
|
47
|
+
this.depths.push(node.parent < 0 ? 0 : this.depths[node.parent] + 1);
|
|
48
|
+
const same = this.byName.get(node.name);
|
|
49
|
+
if (same) {
|
|
50
|
+
same.push(index);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.byName.set(node.name, [index]);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
apply(transition, now) {
|
|
58
|
+
const index = this.resolve(transition);
|
|
59
|
+
if (index === null) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const { from, to } = transition;
|
|
63
|
+
const state = this.states[index];
|
|
64
|
+
state.status = to;
|
|
65
|
+
state.since = now;
|
|
66
|
+
state.changes++;
|
|
67
|
+
state.seq = ++this.seq;
|
|
68
|
+
if (to === 'SUCCESS' || to === 'FAILURE') {
|
|
69
|
+
state.result = to;
|
|
70
|
+
state[to === 'SUCCESS' ? 'successes' : 'failures']++;
|
|
71
|
+
}
|
|
72
|
+
this.dirty.add(index);
|
|
73
|
+
// Ticked, or running: so are its ancestors, whose start came before
|
|
74
|
+
if ((to !== 'IDLE' && to !== 'SKIPPED') || from === 'RUNNING') {
|
|
75
|
+
for (let p = this.nodes[index].parent; p >= 0 && this.states[p].status === null; p = this.nodes[p].parent) {
|
|
76
|
+
this.states[p].status = 'RUNNING';
|
|
77
|
+
this.dirty.add(p);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const entry = { time: transition.time, node: index, from, to };
|
|
81
|
+
this.recent.push(entry);
|
|
82
|
+
if (this.recent.length > MAX_TRANSITIONS) {
|
|
83
|
+
this.recent.shift();
|
|
84
|
+
}
|
|
85
|
+
this.pending.push(entry);
|
|
86
|
+
if (this.pending.length > MAX_TRANSITIONS) {
|
|
87
|
+
this.pending.shift();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// What changed since the last call, null if nothing
|
|
91
|
+
takeChanges() {
|
|
92
|
+
const mismatch = this.mismatch();
|
|
93
|
+
if (this.dirty.size === 0 && this.pending.length === 0 && mismatch === this.mismatchSent) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const changes = {
|
|
97
|
+
added: this.nodes.slice(this.addedSince),
|
|
98
|
+
states: [...this.dirty].map((index) => this.stateOf(index)),
|
|
99
|
+
transitions: this.pending,
|
|
100
|
+
mismatch,
|
|
101
|
+
};
|
|
102
|
+
this.dirty.clear();
|
|
103
|
+
this.pending = [];
|
|
104
|
+
this.addedSince = this.nodes.length;
|
|
105
|
+
this.mismatchSent = mismatch;
|
|
106
|
+
return changes;
|
|
107
|
+
}
|
|
108
|
+
// Everything known, for a page that comes; what changed so far is sent
|
|
109
|
+
// with it, not again
|
|
110
|
+
snapshot() {
|
|
111
|
+
this.takeChanges();
|
|
112
|
+
return {
|
|
113
|
+
added: this.nodes.slice(this.treeSize),
|
|
114
|
+
states: this.states.flatMap((state, index) => (state.status === null ? [] : [this.stateOf(index)])),
|
|
115
|
+
transitions: [...this.recent],
|
|
116
|
+
mismatch: this.mismatchSent,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
blank(node) {
|
|
120
|
+
return { node, status: null, result: null, since: 0, changes: 0, successes: 0, failures: 0, seq: 0 };
|
|
121
|
+
}
|
|
122
|
+
stateOf(index) {
|
|
123
|
+
const { seq: _, ...state } = this.states[index];
|
|
124
|
+
return state;
|
|
125
|
+
}
|
|
126
|
+
resolve(transition) {
|
|
127
|
+
if (this.treeSize > 0 && this.byName.has(transition.name)) {
|
|
128
|
+
return (transition.uid === null ? null : this.byUid(transition.uid, transition.name)) ?? this.byStatus(transition);
|
|
129
|
+
}
|
|
130
|
+
return this.add(transition);
|
|
131
|
+
}
|
|
132
|
+
byUid(uid, name) {
|
|
133
|
+
if (this.uidOffset !== null) {
|
|
134
|
+
const index = uid - this.uidOffset;
|
|
135
|
+
if (index >= 0 && index < this.treeSize && this.nodes[index].name === name) {
|
|
136
|
+
this.uidMisses = 0;
|
|
137
|
+
return index;
|
|
138
|
+
}
|
|
139
|
+
if (++this.uidMisses >= UID_MISSES) {
|
|
140
|
+
this.uidOffset = null;
|
|
141
|
+
this.uidMisses = 0;
|
|
142
|
+
this.uidUnfit = true;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const offsets = new Set(this.byName.get(name).map((index) => uid - index));
|
|
147
|
+
this.uidOffsets = this.uidOffsets ? new Set([...this.uidOffsets].filter((o) => offsets.has(o))) : offsets;
|
|
148
|
+
this.uidSamples++;
|
|
149
|
+
if (this.uidOffsets.size === 1 && this.uidSamples >= 2) {
|
|
150
|
+
this.uidOffset = [...this.uidOffsets][0];
|
|
151
|
+
this.uidOffsets = null;
|
|
152
|
+
this.uidSamples = 0;
|
|
153
|
+
this.uidUnfit = false;
|
|
154
|
+
return uid - this.uidOffset;
|
|
155
|
+
}
|
|
156
|
+
if (this.uidOffsets.size === 0) {
|
|
157
|
+
// Not the file's order: by name, and again from the next transitions
|
|
158
|
+
this.uidOffsets = null;
|
|
159
|
+
this.uidSamples = 0;
|
|
160
|
+
this.uidUnfit = true;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
// The node of that name the transition fits best: in the status it leaves,
|
|
165
|
+
// under a running parent (or one not known yet, which may be). Ties go to
|
|
166
|
+
// the node whose parent changed last; then parents start before their
|
|
167
|
+
// children, which finish and are reset first.
|
|
168
|
+
byStatus({ name, from, to }) {
|
|
169
|
+
const candidates = this.byName.get(name);
|
|
170
|
+
if (candidates.length === 1) {
|
|
171
|
+
return candidates[0];
|
|
172
|
+
}
|
|
173
|
+
const starting = from === 'IDLE';
|
|
174
|
+
let best = candidates[0];
|
|
175
|
+
let bestKey = [];
|
|
176
|
+
for (const index of candidates) {
|
|
177
|
+
const status = this.states[index].status;
|
|
178
|
+
const parent = this.nodes[index].parent;
|
|
179
|
+
const parentStatus = parent < 0 ? 'RUNNING' : this.states[parent].status;
|
|
180
|
+
const key = [
|
|
181
|
+
(status === from ? 4 : status === null ? 2 : 0) + (parentStatus === 'RUNNING' || parentStatus === null ? 2 : 0),
|
|
182
|
+
parent < 0 ? 0 : this.states[parent].seq,
|
|
183
|
+
starting ? -this.depths[index] : this.depths[index],
|
|
184
|
+
];
|
|
185
|
+
const better = key.findIndex((value, i) => value !== bestKey[i]);
|
|
186
|
+
if (bestKey.length === 0 || (better !== -1 && key[better] > bestKey[better])) {
|
|
187
|
+
best = index;
|
|
188
|
+
bestKey = key;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return best;
|
|
192
|
+
}
|
|
193
|
+
// A node not in the tree: listed after the others
|
|
194
|
+
add({ uid, name }) {
|
|
195
|
+
const key = uid === null ? name : `${uid} ${name}`;
|
|
196
|
+
let index = this.added.get(key);
|
|
197
|
+
if (index === undefined) {
|
|
198
|
+
if (this.added.size >= MAX_ADDED) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
index = this.nodes.length;
|
|
202
|
+
this.nodes.push({ name, type: name, kind: null, parent: -1, ports: {} });
|
|
203
|
+
this.states.push(this.blank(index));
|
|
204
|
+
this.depths.push(0);
|
|
205
|
+
this.added.set(key, index);
|
|
206
|
+
}
|
|
207
|
+
return index;
|
|
208
|
+
}
|
|
209
|
+
mismatch() {
|
|
210
|
+
const missing = this.nodes.length - this.treeSize;
|
|
211
|
+
if (this.treeSize === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
if (missing > 0) {
|
|
215
|
+
const names = this.nodes.slice(this.treeSize, this.treeSize + 3).map((node) => node.name).join(', ');
|
|
216
|
+
return `${missing === 1 ? 'A node' : `${missing} nodes`} of the log not in the tree file (${names}${missing > 3 ? ', …' : ''}): is it the tree the robot runs?`;
|
|
217
|
+
}
|
|
218
|
+
return this.uidUnfit ? 'UIDs of the log that do not fit the tree file: its nodes are found by name' : null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type rclnodejs from 'rclnodejs';
|
|
2
|
+
import { type BehaviorTreeRecording, type BehaviorTreeUpdate } from '../shared/protocol.ts';
|
|
3
|
+
import { type TreeInfo } from './behavior_tree_file.ts';
|
|
4
|
+
import { BehaviorTreePlayback, type PlaybackWatcher } from './behavior_tree_playback.ts';
|
|
5
|
+
import type { StreamState } from './frame_stream.ts';
|
|
6
|
+
import { type Logger } from './util.ts';
|
|
7
|
+
export type RecordingInfo = BehaviorTreeRecording['recording'];
|
|
8
|
+
export interface BehaviorTreeWatcher {
|
|
9
|
+
onStatus: (state: StreamState, message: string) => void;
|
|
10
|
+
onTree: (tree: TreeInfo) => void;
|
|
11
|
+
onUpdate: (update: Omit<BehaviorTreeUpdate, 'type' | 'id'>) => void;
|
|
12
|
+
onRecording: (recording: RecordingInfo) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare class BehaviorTrees {
|
|
15
|
+
private readonly node;
|
|
16
|
+
private readonly logger;
|
|
17
|
+
private readonly uploadsDir;
|
|
18
|
+
private readonly topics;
|
|
19
|
+
private readonly monitors;
|
|
20
|
+
private readonly recordings;
|
|
21
|
+
constructor(node: rclnodejs.Node, logger: Logger, uploadsDir: string);
|
|
22
|
+
watch(topic: string, tree: string, watcher: BehaviorTreeWatcher): () => void;
|
|
23
|
+
record(topic: string, path: string | null): Promise<string | null>;
|
|
24
|
+
play(bag: string, topic: string, tree: string, watcher: PlaybackWatcher): BehaviorTreePlayback;
|
|
25
|
+
stopAll(): Promise<void>;
|
|
26
|
+
private recordingOf;
|
|
27
|
+
private announce;
|
|
28
|
+
private forget;
|
|
29
|
+
}
|