tassello 0.6.0 → 0.7.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 CHANGED
@@ -5,6 +5,22 @@ change the plugin API (`tassello/sdk`, `tassello/server`, `tassello/vite`), as n
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ - Rosbags widget: the robot's rosbags, as `ros2 bag` makes and reads them. A folder's bags, the
9
+ newest first, with their time span, size, storage and compression, and each topic's type,
10
+ count and rate, from their `metadata.yaml` (Humble's and Jazzy's), else from their files (a
11
+ recording killed: MCAP's summary, SQLite's tables). Downloaded (folders as `.tar.gz`), deleted
12
+ (bags only, from the dashboard's own pages), or played on the robot's topics with
13
+ `ros2 bag play`: paused, sought, sped up, looped, from an offset, some topics only, `/clock`
14
+ published. New ones recorded with `ros2 bag record`: topics chosen from the graph, all of them,
15
+ or matching a regular expression, but those matching another; with its advanced options, the
16
+ storage (MCAP, SQLite) and its preset, a new file by size or duration, the compression of each
17
+ file or message and its format, the cache, hidden topics, sim time. The recordings and the
18
+ playback are the robot's: every page sees them, and stops them.
19
+ - Behavior Tree Inspector: its recordings are made by the same rosbags service, with the same
20
+ advanced options, and listed (and stopped) by the Rosbags widgets. Bags compressed by rosbag2
21
+ (zstd, each file or each message) are played back too.
22
+ - Plugins: `connection.subscribeBags`, `bagsAction`, `recordBehaviorTree`'s options, and the
23
+ types of `src/shared/rosbag.ts`.
8
24
  - Parameters widget: nodes' parameters, as rqt_reconfigure and `ros2 param` show them. A node's
9
25
  parameters grouped by their prefixes, each edited in place (a switch, a slider within its range,
10
26
  a field), with what the node refuses and why; changes made elsewhere followed live on
package/README.md CHANGED
@@ -12,7 +12,8 @@ and plugins add more.
12
12
  - **Built in**: teleop pads, a Nav2 map with goals, the robot's URDF in 3D, an arm jogger, cameras,
13
13
  images, command buttons, a systemd application manager, a behavior tree inspector and
14
14
  editor, the ROS log of every node, the robot's diagnostics, its topics, services and actions
15
- as the `ros2` command line shows them, its nodes' parameters, a file manager, a terminal, and its networks: Wi-Fi
15
+ as the `ros2` command line shows them, its nodes' parameters, its rosbags (recorded, listed,
16
+ downloaded and played back), a file manager, a terminal, and its networks: Wi-Fi
16
17
  joined, connections switched, the robot as an access point.
17
18
  - **Plugins**: widgets of your own, with server-side code if they need it, as npm or ROS packages.
18
19
  - **One process**: a ROS 2 node in Node.js. Nothing to install on the robot but Node.js.
@@ -132,6 +133,7 @@ after the others on the devices already arranged.
132
133
  | **Diagnostics** | The robot's diagnostics (`/diagnostics`, or the aggregator's `/diagnostics_agg`), as rqt_robot_monitor shows them: each status's level, the groups with the worst under them, those with problems unfolded. As a tree, as a list (the worst first) or as the latest changes of level, filtered by level and by text; a widget can be for some statuses only. A status's details: its message, hardware ID, values and latest changes. Pause, download, and forget the statuses no longer published |
133
134
  | **ROS Monitor** | The ROS graph, as the `ros2` command line shows it, on three sheets: its topics, services and actions, filtered by name or type, the hidden ones on demand. A topic's messages, their rate, bandwidth and delay, its publishers and subscriptions with their QoS and those that cannot talk, a message to publish once or at a rate (`ros2 topic echo`, `hz`, `bw`, `delay`, `info -v`, `pub`); a service to call, once or at a rate (`ros2 service call`, `info`, `echo`); every goal of an action, whoever sent it, with its feedback, and a goal to send or cancel (`ros2 action send_goal`, `info`); their types' definitions (`ros2 interface show`) |
134
135
  | **Parameters** | Nodes' parameters, as rqt_reconfigure and `ros2 param` show them: the nodes of the graph, and the parameters of the one chosen grouped by their prefixes, each edited in place (a switch, a slider within its range, a field), live as they change wherever they are set. Their descriptions, ranges and constraints (`ros2 param describe`); new ones set and undeclared ones deleted (`set`, `delete`); all of them dumped as YAML, downloaded or saved on the robot (`dump`), and loaded from a YAML file of the robot or of the page's device (`load`) |
136
+ | **Rosbags** | The robot's rosbags, as `ros2 bag` makes and reads them: those of a folder, the newest first, with when, how long and how big, their storage and compression, each topic's type, count and rate (`ros2 bag info`); downloaded, deleted, or played on the robot's topics (`ros2 bag play`: paused, sought, sped up, looped, some topics only). New ones recorded (`ros2 bag record`): topics chosen from the graph, all of them, or matching an expression, with rosbag2's options: the storage (MCAP, SQLite) and its preset, a new file by size or duration, compression of each file or message. Every page sees the recordings and the playback, the behavior tree inspectors' too |
135
137
  | **File Manager** | The robot's files: folders unfolded in place, places (home, the ROS workspaces, bookmarks) and the disks' free space, an entry's details and permissions, what takes a folder's space. Make, rename, cut, copy, paste, drag onto folders, delete, download (folders as `.tar.gz`) and upload (drop files and folders onto it). Text files edited in tabs as gedit does: highlighting, search and replace, undo; images and PGM maps shown |
136
138
  | **Terminal** | A shell on the robot, or a command (`htop`, `tmux attach`), in a full terminal: colours, full screen programs, the mouse. It outlives the page: reloaded, reconnected or shown again, it is as it was. On touch screens, the keys phones lack, as Termux has them: Esc, Tab, Ctrl, Alt, Home, End, the arrows, Page Up and Down, and while typing the terminal fills the screen above the on-screen keyboard |
137
139
  | **Networking** | The robot's networks, through NetworkManager, as a phone's settings show them: the Wi-Fi networks in range, joined with their password; the connections saved, switched between, edited (their addresses by DHCP or static), forgotten; the robot as an access point, its network shared by a QR code; the interfaces, their state and addresses. Asks before a change that would cut the page off from the robot |
@@ -198,12 +200,14 @@ Notes on some of them:
198
200
  shows how it last ended.
199
201
 
200
202
  Its record button records the log on the robot with `ros2 bag record`, into a new folder
201
- (`<recordings folder>/behavior_tree_<date>_<time>` by default), until pressed again: every
202
- page showing the topic shows it recording. Its playback button opens a rosbag of the robot
203
+ (`<recordings folder>/behavior_tree_<date>_<time>` by default), with the Rosbags widget's
204
+ advanced options, until pressed again: every page showing the topic shows it recording, and
205
+ the Rosbags widgets list it (and may stop it). Its playback button opens a rosbag of the robot
203
206
  (a bag's folder, or its `.mcap` or `.db3` file, e.g. of a whole run recorded with
204
207
  `ros2 bag record -a`) and plays its log in the widget, on the tree of its file, without
205
208
  publishing anything: pause, seek and choose the speed (0.1× to 16×). Bags of Humble and of
206
- Jazzy are read either way; SQLite bags need Node.js 22.13, zstd compressed MCAP 22.15.
209
+ Jazzy are read either way, compressed by rosbag2 (zstd, each file or each message) or not;
210
+ SQLite bags need Node.js 22.13, zstd compressed ones 22.15.
207
211
  - **Behavior Tree Editor** edits one BehaviorTree of the file at a time (its toolbar switches
208
212
  between them, adds new ones, and double clicking a subtree opens its tree, even in an included
209
213
  file). Add a node from the palette by dragging it where it goes (into a node, or beside one), or
@@ -320,6 +324,29 @@ Notes on some of them:
320
324
  node's section, and the `/**` one) and sets its parameters one by one, saying which the node
321
325
  refused and why. Parameter files are read and written by Python's yaml, as `ros2 param` does. A
322
326
  widget can be *view only*.
327
+ - **Rosbags** lists the bags of its *folder* (`~/bags` by default; the toolbar browses for
328
+ another): its folders holding a bag, and its lone `.mcap` and `.db3` files, the newest first,
329
+ read again every 2 s while shown. What a bag holds is read from its `metadata.yaml` (Humble's
330
+ and Jazzy's); a bag without it (its recording killed) from its files: MCAP's summary, SQLite's
331
+ tables (up to 64 MB), and it says so (`ros2 bag reindex` makes the metadata). A bag's details
332
+ give its path, its time span, its files, and its topics with their types, counts and rates.
333
+ *Download* sends a folder as a `.tar.gz`, a file as it is; *Delete* deletes bags only, a folder
334
+ holding anything else than a bag's files being refused (the File Manager deletes those).
335
+ *Record* runs `ros2 bag record` into a new folder (`<folder>/rosbag2_<date>_<time>`): the
336
+ topics chosen from the graph (the widget's *topics to record* chosen first, others typed in),
337
+ or all of them, and those matching a regular expression, but those matching another. Its
338
+ *Advanced* options are `ros2 bag record`'s: the storage (the distribution's default, MCAP or
339
+ SQLite, as `ros2 bag list storage` lists them) and its preset (MCAP's `fastwrite`, `zstd_fast`,
340
+ `zstd_small`, SQLite's `resilient`), a new file at a size or a duration (`-b`, `-d`), the
341
+ compression of each file or of each message and its format (`ros2 bag list compressor`: zstd;
342
+ MCAP compresses its chunks with its presets instead, each message is refused), the cache, the
343
+ hidden topics and sim time. The page remembers them for the next recording, the behavior tree
344
+ inspector's too. Recordings show above the bags with their time and size, stopped as Ctrl+C
345
+ stops them, which completes the bag; the server stops them too when it stops. *Play* runs
346
+ `ros2 bag play`, one bag at a time: its messages published again on the robot's topics, at a
347
+ rate, looped, from an offset, some topics only, `/clock` published or not, started paused.
348
+ Pause, seek and the rate go through the player's services (`/rosbag2_player/…`); where it is
349
+ is the server's reckoning from them. A widget can be *view only*.
323
350
  - **File Manager** opens its *folder* (`~` by default; `/…`, `~/…`, `package://…`) and lists it
324
351
  with its entries' sizes and times, sorted by name (naturally: `run_2` before `run_10`), size or
325
352
  time, folders first; the chevrons unfold folders in place. Its places are home, the root, the
@@ -467,16 +494,16 @@ and ship example panels. See **[docs/plugins.md](docs/plugins.md)**.
467
494
 
468
495
  Tassello has no login. **Anyone who can open the page can drive the robot, publish on any topic,
469
496
  call any service and send or cancel any goal (the ROS Monitor), change any node's parameters
470
- (Parameters), run any command as
497
+ (Parameters), record, play and delete rosbags (Rosbags), run any command as
471
498
  the server's user (the Terminal, a shell), read, write, upload, move and delete any of the
472
499
  files that user can (the File Manager), and change the robot's networks and read their passwords
473
500
  (Networking)**: keep it on a network you trust, or behind a reverse proxy
474
501
  with authentication (it serves everything under `/`, its WebSocket at `/ws`). Changes to the files
475
502
  are accepted from the dashboard's own pages only (they send a header that another site's page
476
503
  cannot), and the root, the home folder and the system's top folders (`/usr`, `/etc`…) are never
477
- deleted nor moved. Terminals open, and networks change, from the dashboard's own pages only too:
478
- those whose origin is the host they reached (the `Host` header, or the `X-Forwarded-Host` a
479
- reverse proxy sets), which another site's page cannot pretend.
504
+ deleted nor moved. Terminals open, networks change and rosbags are deleted from the dashboard's
505
+ own pages only too: those whose origin is the host they reached (the `Host` header, or the
506
+ `X-Forwarded-Host` a reverse proxy sets), which another site's page cannot pretend.
480
507
 
481
508
  ## How it works
482
509
 
@@ -489,6 +516,7 @@ reverse proxy sets), which another site's page cannot pretend.
489
516
  ├─ bash: the buttons' commands
490
517
  ├─ python3: the terminals' pseudo-terminals
491
518
  ├─ nmcli: the networks (NetworkManager)
519
+ ├─ ros2 bag: rosbags recorded and played
492
520
  └─ the plugins' server sides
493
521
  ```
494
522
 
@@ -522,6 +550,10 @@ Safety and bandwidth are the server's concern:
522
550
  and every 3 s for the signals of the Wi-Fi networks in range, which are scanned for when a page
523
551
  opens the widget (at most every 30 s) or asks. Passwords are on `nmcli`'s command line only for
524
552
  the moment it takes to save them, activating a connection being another command.
553
+ - Rosbags are recorded and played by `ros2 bag`, the robot's own, as children of the server:
554
+ one recording per folder, for every page (the behavior tree inspectors' among them), one
555
+ playback. A folder of bags is read once for every page showing it, every 2 s, a bag's metadata
556
+ again only when its files change; the recordings' sizes every second.
525
557
  - The File Manager's folders are read again every 3 s, and answered "unchanged" while they are;
526
558
  the largest are listed in part (5,000 entries), and only the rows in sight are drawn. Uploads
527
559
  and downloads are streamed, folders archived by `tar` as they are sent; saves replace files at
@@ -7,7 +7,7 @@ import { BT_RATE } from '../shared/protocol.js';
7
7
  import { readTree } from './behavior_tree_file.js';
8
8
  import { decodeLog, logLayout } from './behavior_tree_log.js';
9
9
  import { BehaviorTreeState } from './behavior_tree_state.js';
10
- import { bagFiles, readBag } from './rosbag.js';
10
+ import { openBag, readBag } from './rosbag.js';
11
11
  import { absoluteTopic, errorMessage } from './util.js';
12
12
  const TICK_MS = 50;
13
13
  // How often the page is told where the playback is, while playing
@@ -18,7 +18,7 @@ const MAX_SPEED = 64;
18
18
  const MAX_TRANSITIONS = 2_000_000;
19
19
  // The bag's log: of the topic if it has it, else its first
20
20
  export async function readLog(bag, topic) {
21
- const logs = await readBag(await bagFiles(bag), (t) => /\/BehaviorTreeLog$/.test(t.type));
21
+ const logs = await readBag(await openBag(bag), (t) => /\/BehaviorTreeLog$/.test(t.type));
22
22
  const found = (topic && logs.get(absoluteTopic(topic))) || [...logs.values()][0];
23
23
  if (!found) {
24
24
  throw new Error(`No behavior tree log in ${bag}: no nav2_msgs/msg/BehaviorTreeLog topic`);
@@ -1,8 +1,10 @@
1
1
  import type rclnodejs from 'rclnodejs';
2
2
  import { type BehaviorTreeRecording, type BehaviorTreeUpdate } from '../shared/protocol.ts';
3
+ import { type RecordOptions } from '../shared/rosbag.ts';
3
4
  import { type TreeInfo } from './behavior_tree_file.ts';
4
5
  import { BehaviorTreePlayback, type PlaybackWatcher } from './behavior_tree_playback.ts';
5
6
  import type { StreamState } from './frame_stream.ts';
7
+ import type { Rosbags } from './rosbags.ts';
6
8
  import { type Logger } from './util.ts';
7
9
  export type RecordingInfo = BehaviorTreeRecording['recording'];
8
10
  export interface BehaviorTreeWatcher {
@@ -17,12 +19,12 @@ export declare class BehaviorTrees {
17
19
  private readonly uploadsDir;
18
20
  private readonly topics;
19
21
  private readonly monitors;
22
+ private readonly rosbags;
20
23
  private readonly recordings;
21
- constructor(node: rclnodejs.Node, logger: Logger, uploadsDir: string);
24
+ constructor(node: rclnodejs.Node, logger: Logger, uploadsDir: string, rosbags: Rosbags);
22
25
  watch(topic: string, tree: string, watcher: BehaviorTreeWatcher): () => void;
23
- record(topic: string, path: string | null): Promise<string | null>;
26
+ record(topic: string, path: string | null, options?: RecordOptions): Promise<string | null>;
24
27
  play(bag: string, topic: string, tree: string, watcher: PlaybackWatcher): BehaviorTreePlayback;
25
- stopAll(): Promise<void>;
26
28
  private recordingOf;
27
29
  private announce;
28
30
  private forget;
@@ -9,17 +9,19 @@
9
9
  // outlives its last page for LINGER_MS: a page reloaded, or a panel shown
10
10
  // again, finds the nodes as they are, which the log only says when they change.
11
11
  //
12
- // A topic is recorded into a rosbag (ros2 bag record) when a page asks, until
13
- // one asks to stop: every page following it is told. A bag is played back for
14
- // the page that opens it (behavior_tree_playback.ts).
12
+ // A topic is recorded into a rosbag when a page asks, until one asks to stop
13
+ // (or a Rosbags widget stops it): every page following it is told. The
14
+ // recording is the server's rosbags' (rosbags.ts), as the Rosbags widgets'
15
+ // are. A bag is played back for the page that opens it
16
+ // (behavior_tree_playback.ts).
15
17
  import { promises as fs } from 'node:fs';
16
18
  import { BT_RATE } from '../shared/protocol.js';
19
+ import { DEFAULT_RECORD_OPTIONS } from '../shared/rosbag.js';
17
20
  import { readTree } from './behavior_tree_file.js';
18
21
  import { logTransitions } from './behavior_tree_log.js';
19
22
  import { BehaviorTreePlayback } from './behavior_tree_playback.js';
20
23
  import { BehaviorTreeState } from './behavior_tree_state.js';
21
24
  import { publishersQos } from './qos.js';
22
- import { BagRecorder, newBagFolder } from './rosbag.js';
23
25
  import { absoluteTopic, errorMessage } from './util.js';
24
26
  // How often the graph is checked for the topic's publishers, until it has one
25
27
  const RETRY_PERIOD_MS = 1000;
@@ -252,12 +254,23 @@ export class BehaviorTrees {
252
254
  topics = new Map();
253
255
  // By topic, then tree source
254
256
  monitors = new Map();
255
- // By topic
257
+ rosbags;
258
+ // By topic: the folder it is recorded into, '' while it starts
256
259
  recordings = new Map();
257
- constructor(node, logger, uploadsDir) {
260
+ constructor(node, logger, uploadsDir, rosbags) {
258
261
  this.node = node;
259
262
  this.logger = logger;
260
263
  this.uploadsDir = uploadsDir;
264
+ this.rosbags = rosbags;
265
+ // A recording stopped elsewhere, or by itself
266
+ rosbags.onChange(() => {
267
+ for (const [topic, folder] of this.recordings) {
268
+ if (folder && !rosbags.recording(folder)) {
269
+ this.recordings.delete(topic);
270
+ this.announce(topic);
271
+ }
272
+ }
273
+ });
261
274
  }
262
275
  // Until the returned function is called
263
276
  watch(topic, tree, watcher) {
@@ -290,57 +303,42 @@ export class BehaviorTrees {
290
303
  }
291
304
  // Records the topic into a new folder, or stops (path null): resolves with
292
305
  // the bag's folder, null if it was not recording
293
- async record(topic, path) {
306
+ async record(topic, path, options = DEFAULT_RECORD_OPTIONS) {
294
307
  const name = absoluteTopic(topic);
295
308
  const current = this.recordings.get(name);
296
309
  if (path === null) {
297
- if (!current?.recorder) {
310
+ if (!current) {
298
311
  return null;
299
312
  }
300
313
  this.recordings.delete(name);
301
314
  this.announce(name);
302
- await current.recorder.stop();
303
- this.logger.info(`Recorded ${name} into ${current.path}`);
304
- return current.path;
315
+ return this.rosbags.stop(current);
305
316
  }
306
- if (current) {
307
- throw new Error(`${name} is being recorded already, into ${current.path}`);
317
+ if (current !== undefined) {
318
+ throw new Error(`${name} is being recorded already${current ? `, into ${current}` : ''}`);
308
319
  }
309
- const recording = { path: '', since: 0, recorder: null };
310
- this.recordings.set(name, recording);
320
+ this.recordings.set(name, '');
321
+ let folder;
311
322
  try {
312
- recording.path = await newBagFolder(path);
313
- recording.recorder = await BagRecorder.start(recording.path, [name]);
323
+ const request = { path, topics: [name], all: false, regex: '', exclude: '', options };
324
+ folder = (await this.rosbags.record(request, 'behavior-tree')).path;
314
325
  }
315
326
  catch (e) {
316
327
  this.recordings.delete(name);
317
328
  throw new Error(`Cannot record ${name}: ${errorMessage(e)}`);
318
329
  }
319
- recording.since = Date.now();
320
- recording.recorder.onExit = (reason) => {
321
- if (this.recordings.get(name) === recording) {
322
- this.recordings.delete(name);
323
- this.announce(name);
324
- this.logger.warn(`Recording ${name} stopped: ${reason}`);
325
- }
326
- };
327
- this.logger.info(`Recording ${name} into ${recording.path}`);
330
+ this.recordings.set(name, folder);
328
331
  this.announce(name);
329
- return recording.path;
332
+ return folder;
330
333
  }
331
334
  // A bag played back for a page, until closed
332
335
  play(bag, topic, tree, watcher) {
333
336
  return new BehaviorTreePlayback({ bag, topic, tree: tree.trim(), uploadsDir: this.uploadsDir, logger: this.logger }, watcher);
334
337
  }
335
- // The recordings completed, as the server stops
336
- async stopAll() {
337
- const recorders = [...this.recordings.values()].flatMap((recording) => recording.recorder ?? []);
338
- this.recordings.clear();
339
- await Promise.all(recorders.map((recorder) => recorder.stop()));
340
- }
341
338
  recordingOf(topic) {
342
- const recording = this.recordings.get(topic);
343
- return recording?.recorder ? { path: recording.path, since: recording.since } : null;
339
+ const folder = this.recordings.get(topic);
340
+ const recording = folder ? this.rosbags.recording(folder) : null;
341
+ return recording ? { path: recording.path, since: recording.since } : null;
344
342
  }
345
343
  announce(topic) {
346
344
  for (const monitor of this.monitors.get(topic)?.values() ?? []) {
@@ -1,3 +1,4 @@
1
+ import type { BagInfo } from '../shared/rosbag.ts';
1
2
  export interface BagTopic {
2
3
  name: string;
3
4
  type: string;
@@ -10,16 +11,19 @@ export type BagTopics = Map<string, {
10
11
  topic: BagTopic;
11
12
  messages: BagMessage[];
12
13
  }>;
14
+ export interface BagSource {
15
+ files: string[];
16
+ messageCompression: string;
17
+ }
18
+ export declare function bagPath(input: string, home?: string): string;
19
+ export declare function openBag(input: string, home?: string): Promise<BagSource>;
13
20
  export declare function bagFiles(input: string, home?: string): Promise<string[]>;
14
- export declare function readBag(files: string[], accept: (topic: BagTopic) => boolean): Promise<BagTopics>;
21
+ export declare function readBag(bag: BagSource | string[], accept: (topic: BagTopic) => boolean): Promise<BagTopics>;
22
+ export declare function bagInfo(input: string, home?: string): Promise<BagInfo>;
23
+ export type BagCache = Map<string, {
24
+ version: string;
25
+ info: BagInfo;
26
+ }>;
27
+ export declare function listBags(folder: string, cache?: BagCache, home?: string): Promise<BagInfo[]>;
15
28
  export declare function newBagFolder(input: string, home?: string): Promise<string>;
16
- export declare class BagRecorder {
17
- readonly folder: string;
18
- onExit: ((reason: string) => void) | null;
19
- private readonly child;
20
- private output;
21
- private stopping;
22
- private constructor();
23
- static start(folder: string, topics: string[]): Promise<BagRecorder>;
24
- stop(): Promise<void>;
25
- }
29
+ export declare function folderSize(folder: string): Promise<number>;