tassello 0.7.0 → 0.9.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,30 @@ change the plugin API (`tassello/sdk`, `tassello/server`, `tassello/vite`), as n
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ - Plot widget: numbers of any topic over time, as rqt_plot and PlotJuggler plot them
9
+ (`/odom.twist.twist.linear.x`, a battery's voltage, joints' efforts), up to 8 series on a
10
+ chart, from every message. Live over the latest *time shown*; paused, zoomed (drag across it,
11
+ the wheel, a pinch) and scrolled through time (Shift+wheel, a swipe, the arrow keys); the value
12
+ axis fitted to what is shown or fixed; the values at the pointer; series hidden from the
13
+ legend. What is shown exported as CSV, a row a message. The series are picked in the widget,
14
+ from the topics of the graph and the numeric fields of their latest message (a `JointState`'s
15
+ named by joint), and saved with the panel, or listed in its properties.
16
+ - The topic monitor (`topic.monitor`) samples: the numbers of every message at some fields, sent
17
+ ten times a second (`TopicSamples`). Plugins: `subscribeTopicMonitor`'s `samples`; its `stats`
18
+ and `echo` handlers are optional.
19
+ - Plugins: widgets get `updateConfig`, which changes their properties and saves the panel, as the
20
+ side panel does.
21
+ - System Monitor widget: the robot's computer, every second. The CPU's load, of each core (those
22
+ offline too), their frequencies and the load average; the memory, its cache and the swap; the
23
+ temperatures of the thermal zones and hwmon chips, by chip with their hottest, against their
24
+ own limits; the fans; a Raspberry Pi's throttling (under-voltage, frequency capped, throttled,
25
+ soft temperature limit, now and since boot); the GPUs: a Jetson's (devfreq), NVIDIA's
26
+ (`nvidia-smi`), AMD's (amdgpu); the power rails: a Jetson's INA3221 (L4T 32's too), hwmon's
27
+ power sensors, Intel's RAPL when readable; the disks' traffic and busy time, and the file
28
+ systems' space; the networks' traffic, of the device's own interfaces or those chosen. Charts
29
+ of the last 5 minutes, kept by the server, with the values at the pointer (or the arrow keys).
30
+ The board, its OS, uptime, and a Jetson's L4T release and power mode.
31
+ - Plugins: `connection.subscribeSystem`, and the types of `src/shared/system.ts`.
8
32
  - Rosbags widget: the robot's rosbags, as `ros2 bag` makes and reads them. A folder's bags, the
9
33
  newest first, with their time span, size, storage and compression, and each topic's type,
10
34
  count and rate, from their `metadata.yaml` (Humble's and Jazzy's), else from their files (a
package/README.md CHANGED
@@ -12,9 +12,11 @@ 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, its rosbags (recorded, listed,
16
- downloaded and played back), a file manager, a terminal, and its networks: Wi-Fi
17
- joined, connections switched, the robot as an access point.
15
+ as the `ros2` command line shows them, plots of their numbers over time, its nodes'
16
+ parameters, its rosbags (recorded, listed,
17
+ downloaded and played back), a file manager, a terminal, its networks (Wi-Fi
18
+ joined, connections switched, the robot as an access point), and its computer: CPU, memory,
19
+ temperatures, a Raspberry Pi's throttling, a Jetson's GPU and power, disks and networks.
18
20
  - **Plugins**: widgets of your own, with server-side code if they need it, as npm or ROS packages.
19
21
  - **One process**: a ROS 2 node in Node.js. Nothing to install on the robot but Node.js.
20
22
 
@@ -132,10 +134,12 @@ after the others on the devices already arranged.
132
134
  | **Logs** | The ROS log (`/rosout`), as rqt_console and swri_console show it: every node's messages coloured by level, the nodes running and those that logged with their counts, filtered by node, by level and by text (terms or a regular expression, to show or to hide). Pause, follow the newest, clear, download, and a message's details: where it was logged |
133
135
  | **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 |
134
136
  | **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`) |
137
+ | **Plot** | Numbers of any topic over time, as rqt_plot and PlotJuggler plot them: `/odom.twist.twist.linear.x`, a battery's voltage, joints' efforts (named by their joints), up to 8 series on a chart, from every message. Paused, zoomed and scrolled through time, each series shown or hidden; what is shown exported as CSV. Its series are picked in the widget, from the topics of the graph and the fields of their messages, or listed in its properties |
135
138
  | **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
139
  | **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 |
137
140
  | **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 |
138
141
  | **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 |
142
+ | **System Monitor** | The robot's computer, as top, sensors and a Jetson's jtop show it: the CPU's load, of each core, and their frequencies; the memory and swap; the temperatures by chip, and the fans; a Raspberry Pi's throttling (under-voltage, capped frequency, now and since boot); the GPUs (a Jetson's, NVIDIA's, AMD's) and the power rails (a Jetson's); the disks' traffic and space; the networks' traffic. With charts of the last 5 minutes |
139
143
  | **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 |
140
144
 
141
145
  Their properties (topics, frames, speeds, colours, ...) are edited in the side panel of edit mode.
@@ -306,6 +310,26 @@ Notes on some of them:
306
310
 
307
311
  Messages, requests and goals are made from rclnodejs's code of their types, which Tassello
308
312
  makes when it starts for the workspaces sourced (see Install).
313
+ - **Plot** draws up to 8 series on one value axis: a topic, a dot and its field
314
+ (`/odom.twist.twist.linear.x`, `/joint_states.effort[2]`, list items by their index), then a
315
+ label if any, one a line in its *series* property. *Series…* picks them in the widget, in view
316
+ mode, and saves them with the panel: a topic of the graph, then fields among those of its
317
+ latest message that are numbers, with their values (a `JointState`'s by joint name), or a field
318
+ typed. Numbers, booleans (0 or 1) and times (in seconds, a header's stamp) are plotted; NaN and
319
+ infinities break the line, as does a topic that stopped. The server reads every message of the
320
+ topics plotted, not 10 a second, and sends the numbers of the fields asked for ten times a
321
+ second; each sample is on the server's clock, when its message came. The page keeps the
322
+ latest 10 minutes of each series (200,000 samples at most, 200 s of a 1 kHz topic), from when
323
+ the widget opened.
324
+
325
+ Live, it shows the latest *time shown* (30 s), the value axis fitted to what is shown or
326
+ *fixed*. Drag across it to zoom in on that span, which pauses it; the wheel zooms around the
327
+ pointer (live, a shorter or longer span to now), Shift+wheel or a sideways swipe scrolls
328
+ through time, two fingers pinch; *Live* or a double click goes on. Space pauses, the arrow keys
329
+ scroll, `+` and `-` zoom, Escape goes back live. The pointer shows each series' value at its
330
+ time, the legend the values at the right edge; clicking a series in the legend hides it. The
331
+ download button exports what is shown as CSV: the time (s since the epoch), then a column a
332
+ series, a row a message (those of a topic's series on the same rows).
309
333
  - **Parameters** lists the nodes of the graph (but the hidden ones, as `ros2 node list` does), or
310
334
  shows one node only, its *node* (a panel to tune `/controller_server`). A node's parameters are
311
335
  read through its parameter services (`rcl_interfaces`), grouped by the prefixes of their names
@@ -466,6 +490,21 @@ Notes on some of them:
466
490
  Without it, the widget says the networks cannot be changed. *Share…* reads the passwords with
467
491
  `nmcli --show-secrets`, which needs the same right; those a desktop's keyring keeps cannot be
468
492
  read.
493
+ - **System Monitor** reads `/proc` and `/sys` every second, for every page at once, while one
494
+ shows it, and keeps the last 5 minutes for the charts (a page opened later has them at once).
495
+ The temperatures are the kernel's thermal zones and its hwmon chips' sensors (the chips that
496
+ are a thermal zone again, once), grouped by chip with the hottest of each; a sensor is hot from
497
+ its own limits (else 80 °C), and critical at its own critical (else 95 °C). A **Raspberry Pi**'s
498
+ throttling is its firmware's (`get_throttled` in `/sys`, else `vcgencmd get_throttled`): under-
499
+ voltage, frequency capped, throttled, soft temperature limit, now and since boot. A **Jetson**'s
500
+ GPU is devfreq's (its load and clock), its power the INA3221's rails (`VDD_IN` the whole
501
+ board's), its L4T release and power mode (`nvpmodel -q`) are shown with the board. NVIDIA's
502
+ GPUs are read with `nvidia-smi` every 2 s, AMD's from amdgpu's files; an Intel CPU's package
503
+ power from RAPL, if its counters are readable (root's only, since Linux 5.10). Cores taken
504
+ offline (a Jetson's power modes) show as such. The disks are the whole ones (not partitions,
505
+ loop devices, device mapper or RAID), their space the File Manager's; the networks are the
506
+ device's own interfaces (not docker's bridges, VPN clients' tunnels: their traffic is the
507
+ devices' too), or those of its *interfaces* (`wlan0, tun*`). Each section can be hidden.
469
508
 
470
509
  ## Plugins
471
510
 
@@ -517,6 +556,7 @@ own pages only too: those whose origin is the host they reached (the `Host` head
517
556
  ├─ python3: the terminals' pseudo-terminals
518
557
  ├─ nmcli: the networks (NetworkManager)
519
558
  ├─ ros2 bag: rosbags recorded and played
559
+ ├─ /proc, /sys, nvidia-smi: the system monitor
520
560
  └─ the plugins' server sides
521
561
  ```
522
562
 
@@ -554,6 +594,13 @@ Safety and bandwidth are the server's concern:
554
594
  one recording per folder, for every page (the behavior tree inspectors' among them), one
555
595
  playback. A folder of bags is read once for every page showing it, every 2 s, a bag's metadata
556
596
  again only when its files change; the recordings' sizes every second.
597
+ - A topic plotted is followed once for every page (the ROS Monitor's subscription, with its
598
+ publishers' QoS): its messages are deserialized only while a page plots it, and each page gets
599
+ the numbers of its own fields, ten batches a second: for a 200 Hz topic, about 3 KB a second
600
+ for when its messages came, and 4 KB for each field.
601
+ - The system monitor's sensors are read once for every page, every second while one watches and
602
+ for 30 s after the last leaves; they are looked for again every 30 s (USB ones come and go).
603
+ Each page gets a sample a second, about 4 KB.
557
604
  - The File Manager's folders are read again every 3 s, and answered "unchanged" while they are;
558
605
  the largest are listed in part (5,000 entries), and only the rows in sight are drawn. Uploads
559
606
  and downloads are streamed, folders archived by `tar` as they are sent; saves replace files at
@@ -21,4 +21,5 @@ export interface RosTime {
21
21
  }
22
22
  export declare function toRosMessage(value: unknown, type: string, schemas: Record<string, MessageSchema>, now: () => RosTime, path?: string): Record<string, unknown>;
23
23
  export declare function fieldAt(message: unknown, path: string): unknown;
24
+ export declare function sampleValue(value: unknown): number | null;
24
25
  export declare function echoJson(value: unknown, limit: number): unknown;
@@ -307,6 +307,27 @@ export function fieldAt(message, path) {
307
307
  }
308
308
  return value;
309
309
  }
310
+ // A field's value as a plot takes it: numbers, 64 bit integers, booleans (0
311
+ // or 1), times and durations (in seconds); null for anything else, and for
312
+ // NaN and infinities
313
+ export function sampleValue(value) {
314
+ if (typeof value === 'number') {
315
+ return Number.isFinite(value) ? value : null;
316
+ }
317
+ if (typeof value === 'bigint') {
318
+ return Number(value);
319
+ }
320
+ if (typeof value === 'boolean') {
321
+ return value ? 1 : 0;
322
+ }
323
+ if (value !== null && typeof value === 'object' && 'sec' in value && 'nanosec' in value) {
324
+ const { sec, nanosec } = value;
325
+ if (typeof sec === 'number' && typeof nanosec === 'number') {
326
+ return sec + nanosec / 1e9;
327
+ }
328
+ }
329
+ return null;
330
+ }
310
331
  function number(value) {
311
332
  if (Number.isFinite(value)) {
312
333
  return value;
@@ -33,6 +33,7 @@ import { Rosbags } from './rosbags.js';
33
33
  import { Rosout } from './rosout.js';
34
34
  import { Session } from './session.js';
35
35
  import { FrameStreams } from './streams.js';
36
+ import { SystemMonitor } from './system.js';
36
37
  import { SystemdServices } from './systemd.js';
37
38
  import { Teleop } from './teleop.js';
38
39
  import { Terminals } from './terminals.js';
@@ -112,6 +113,7 @@ export async function startServer(options) {
112
113
  const diagnostics = new Diagnostics(node, logger);
113
114
  const terminals = new Terminals(logger);
114
115
  const networking = new Networking(logger);
116
+ const system = new SystemMonitor(logger);
115
117
  // Next to the panels
116
118
  const uploadsDir = path.join(path.dirname(panelsDir), 'images');
117
119
  const rosbags = new Rosbags(ros, logger);
@@ -171,6 +173,7 @@ export async function startServer(options) {
171
173
  actions,
172
174
  parameters,
173
175
  rosbags,
176
+ system,
174
177
  plugins: host,
175
178
  hostname: os.hostname(),
176
179
  }, ownPage(req.headers), req.socket.localAddress ?? null);
@@ -207,6 +210,7 @@ export async function startServer(options) {
207
210
  systemd.stopAll();
208
211
  terminals.stopAll();
209
212
  networking.stopAll();
213
+ system.stop();
210
214
  // Stops the robot if a page was driving it, while the node still runs
211
215
  for (const session of sessions) {
212
216
  session.close();
@@ -20,6 +20,7 @@ import type { Rosbags } from './rosbags.ts';
20
20
  import type { RosGraph } from './ros_graph.ts';
21
21
  import type { Rosout } from './rosout.ts';
22
22
  import type { FrameStreams } from './streams.ts';
23
+ import type { SystemMonitor } from './system.ts';
23
24
  import type { SystemdServices } from './systemd.ts';
24
25
  import type { Teleop } from './teleop.ts';
25
26
  import type { Terminals } from './terminals.ts';
@@ -52,6 +53,7 @@ export interface SessionServices {
52
53
  actions: Actions;
53
54
  parameters: Parameters;
54
55
  rosbags: Rosbags;
56
+ system: SystemMonitor;
55
57
  plugins: PluginHost;
56
58
  hostname: string;
57
59
  }
@@ -404,9 +404,11 @@ export class Session {
404
404
  echo: echo && typeof echo === 'object'
405
405
  ? { field: optionalString(echo.field), truncate: number(echo.truncate, 1, MAX_TRUNCATE, 128) }
406
406
  : null,
407
+ samples: message.samples === undefined ? null : strings(message.samples, 'samples'),
407
408
  onStatus: (state, text) => this.sendStreamStatus(id, state, text),
408
409
  onStats: (stats) => this.send({ type: 'topic.stats', id, stats }),
409
410
  onEcho: (update) => this.send({ type: 'topic.echo', id, ...update }),
411
+ onSamples: (samples) => this.send({ type: 'topic.samples', id, ...samples }),
410
412
  }));
411
413
  return;
412
414
  }
@@ -468,6 +470,12 @@ export class Session {
468
470
  case 'bags.action':
469
471
  this.answer(message.id, Promise.resolve().then(() => services.rosbags.act(checkBagsRequest(message.request), this.ownPage)));
470
472
  return;
473
+ case 'system.subscribe': {
474
+ const id = subscriptionId(message.id);
475
+ this.unsubscribe(id);
476
+ this.subscriptions.set(id, services.system.watch((update) => this.send({ type: 'system', id, ...update })));
477
+ return;
478
+ }
471
479
  case 'action.subscribe': {
472
480
  const id = subscriptionId(message.id);
473
481
  this.unsubscribe(id);
@@ -0,0 +1,40 @@
1
+ import type { SystemUpdate } from '../shared/protocol.ts';
2
+ import { type Logger } from './util.ts';
3
+ export type SystemUpdateBody = Omit<SystemUpdate, 'type' | 'id'>;
4
+ type Listener = (update: SystemUpdateBody) => void;
5
+ export declare class SystemMonitor {
6
+ private readonly logger;
7
+ private readonly listeners;
8
+ private readonly history;
9
+ private info;
10
+ private infoJson;
11
+ private infoAt;
12
+ private space;
13
+ private spaceJson;
14
+ private spaceAt;
15
+ private sensors;
16
+ private sensorsAt;
17
+ private previous;
18
+ private latest;
19
+ private nvidia;
20
+ private vcgencmd;
21
+ private underVoltageSeen;
22
+ private timer;
23
+ private linger;
24
+ private running;
25
+ private ticking;
26
+ constructor(logger: Logger);
27
+ watch(listener: Listener): () => void;
28
+ stop(): void;
29
+ private send;
30
+ private tick;
31
+ private refresh;
32
+ private sample;
33
+ private temperatures;
34
+ private fans;
35
+ private frequencies;
36
+ private power;
37
+ private gpus;
38
+ private throttled;
39
+ }
40
+ export {};