tassello 0.4.0 → 0.6.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 +38 -0
- package/README.md +92 -4
- package/dist/server/actions.d.ts +24 -0
- package/dist/server/actions.js +331 -0
- package/dist/server/files.d.ts +1 -1
- package/dist/server/files.js +4 -1
- package/dist/server/interfaces.d.ts +16 -0
- package/dist/server/interfaces.js +145 -0
- package/dist/server/message_code.d.ts +12 -0
- package/dist/server/message_code.js +165 -0
- package/dist/server/message_format.d.ts +24 -0
- package/dist/server/message_format.js +345 -0
- package/dist/server/overlay_encoding.d.ts +26 -0
- package/dist/server/overlay_encoding.js +40 -0
- package/dist/server/overlays.d.ts +21 -0
- package/dist/server/overlays.js +172 -0
- package/dist/server/param_format.d.ts +33 -0
- package/dist/server/param_format.js +155 -0
- package/dist/server/param_yaml.d.ts +14 -0
- package/dist/server/param_yaml.js +168 -0
- package/dist/server/parameters.d.ts +38 -0
- package/dist/server/parameters.js +299 -0
- package/dist/server/ros.d.ts +4 -2
- package/dist/server/ros.js +13 -5
- package/dist/server/ros_graph.d.ts +25 -0
- package/dist/server/ros_graph.js +148 -0
- package/dist/server/ros_graph_format.d.ts +51 -0
- package/dist/server/ros_graph_format.js +139 -0
- package/dist/server/server.js +26 -1
- package/dist/server/session.d.ts +14 -0
- package/dist/server/session.js +116 -1
- package/dist/server/tf.d.ts +6 -0
- package/dist/server/tf.js +19 -1
- package/dist/server/topic_monitor.d.ts +25 -0
- package/dist/server/topic_monitor.js +229 -0
- package/dist/server/topic_publishing.d.ts +20 -0
- package/dist/server/topic_publishing.js +153 -0
- package/dist/server/topic_stats.d.ts +13 -0
- package/dist/server/topic_stats.js +106 -0
- package/dist/server/topics.js +2 -1
- package/dist/server/transforms.d.ts +1 -0
- package/dist/server/transforms.js +5 -0
- package/dist/shared/monitor.d.ts +144 -0
- package/dist/shared/monitor.js +12 -0
- package/dist/shared/parameters.d.ts +29 -0
- package/dist/shared/parameters.js +16 -0
- package/dist/shared/protocol.d.ts +134 -2
- package/dist/shared/protocol.js +2 -0
- package/dist/types/client/api.d.ts +1 -1
- package/dist/types/client/connection.d.ts +44 -2
- package/dist/types/client/icons.d.ts +6 -0
- package/dist/types/client/sdk/index.d.ts +4 -2
- package/dist/types/client/widgets/types.d.ts +8 -0
- package/dist/types/shared/monitor.d.ts +144 -0
- package/dist/types/shared/parameters.d.ts +29 -0
- package/dist/types/shared/protocol.d.ts +134 -2
- package/dist/www/assets/Joystick.js +1 -1
- package/dist/www/assets/RobotModelView.js +2 -2
- package/dist/www/assets/TerminalView.js +2 -2
- package/dist/www/assets/dialogs.js +1 -1
- package/dist/www/assets/icons.js +1 -1
- package/dist/www/assets/index.css +1 -1
- package/dist/www/assets/index.js +47 -45
- package/dist/www/assets/shared/tassello-sdk.js +1 -1
- package/docs/plugins.md +26 -1
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The ROS interfaces (messages, services, actions) for the monitor widgets:
|
|
2
|
+
// their definitions as ros2 interface show prints them, from the files of
|
|
3
|
+
// their packages; their schemas and default values, from rclnodejs's code of
|
|
4
|
+
// them; and the JSON of the pages made into their messages.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import rclnodejs from 'rclnodejs';
|
|
8
|
+
import { defaultMessage, expandDefinition, HEADER_TYPE, parseType, partTypes, toRosMessage, } from './message_format.js';
|
|
9
|
+
import { missingCodeHint } from './message_code.js';
|
|
10
|
+
import { errorMessage, packageShareDirectory } from './util.js';
|
|
11
|
+
function load(type) {
|
|
12
|
+
let typeClass;
|
|
13
|
+
try {
|
|
14
|
+
typeClass = rclnodejs.require(type);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error(missingCodeHint(errorMessage(e)));
|
|
18
|
+
}
|
|
19
|
+
if (!typeClass?.ROSMessageDef) {
|
|
20
|
+
throw new Error(`${type} is not a message type`);
|
|
21
|
+
}
|
|
22
|
+
return typeClass;
|
|
23
|
+
}
|
|
24
|
+
// A field's message type, pkg/<msg|srv|action>/Name: rclnodejs tells only
|
|
25
|
+
// its package and name, which the parts of services and actions share
|
|
26
|
+
function nestedType(pkg, name) {
|
|
27
|
+
const kinds = /_(Request|Response|Event)$/.test(name)
|
|
28
|
+
? ['srv', 'msg', 'action']
|
|
29
|
+
: /_(Goal|Result|Feedback|FeedbackMessage|SendGoal_Request|SendGoal_Response|GetResult_Request|GetResult_Response)$/.test(name)
|
|
30
|
+
? ['action', 'msg', 'srv']
|
|
31
|
+
: ['msg', 'action', 'srv'];
|
|
32
|
+
for (const kind of kinds) {
|
|
33
|
+
try {
|
|
34
|
+
if (rclnodejs.require(`${pkg}/${kind}/${name}`)?.ROSMessageDef) {
|
|
35
|
+
return `${pkg}/${kind}/${name}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Not of this kind
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return `${pkg}/msg/${name}`;
|
|
43
|
+
}
|
|
44
|
+
function plain(value) {
|
|
45
|
+
if (ArrayBuffer.isView(value)) {
|
|
46
|
+
return Array.from(value, (v) => (typeof v === 'bigint' ? Number(v) : v));
|
|
47
|
+
}
|
|
48
|
+
return typeof value === 'bigint' ? Number(value) : value;
|
|
49
|
+
}
|
|
50
|
+
export class Interfaces {
|
|
51
|
+
node;
|
|
52
|
+
schemas = new Map();
|
|
53
|
+
infos = new Map();
|
|
54
|
+
constructor(node) {
|
|
55
|
+
this.node = node;
|
|
56
|
+
}
|
|
57
|
+
// The schema of a message type, and of those it nests
|
|
58
|
+
schemasOf(type, into = {}) {
|
|
59
|
+
if (into[type]) {
|
|
60
|
+
return into;
|
|
61
|
+
}
|
|
62
|
+
const schema = this.schema(type);
|
|
63
|
+
into[type] = schema;
|
|
64
|
+
for (const field of schema.fields) {
|
|
65
|
+
if (!field.primitive) {
|
|
66
|
+
this.schemasOf(field.type, into);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return into;
|
|
70
|
+
}
|
|
71
|
+
// ros2 interface show, and the schemas and default values of its parts
|
|
72
|
+
info(type) {
|
|
73
|
+
const known = this.infos.get(type);
|
|
74
|
+
if (known) {
|
|
75
|
+
return known;
|
|
76
|
+
}
|
|
77
|
+
const { pkg, kind, name } = parseType(type);
|
|
78
|
+
const schemas = {};
|
|
79
|
+
const parts = partTypes(type).map((part) => {
|
|
80
|
+
this.schemasOf(part.type, schemas);
|
|
81
|
+
return { ...part, default: defaultMessage(part.type, schemas) };
|
|
82
|
+
});
|
|
83
|
+
const info = { type, kind, text: this.definition(pkg, kind, name), parts, schemas };
|
|
84
|
+
this.infos.set(type, info);
|
|
85
|
+
return info;
|
|
86
|
+
}
|
|
87
|
+
// The message of type the page's JSON describes (see toRosMessage)
|
|
88
|
+
message(type, value) {
|
|
89
|
+
return toRosMessage(value, type, this.schemasOf(type), () => this.now());
|
|
90
|
+
}
|
|
91
|
+
// Its messages begin with a std_msgs/msg/Header, whose stamp ros2 topic
|
|
92
|
+
// delay compares with their arrival
|
|
93
|
+
hasHeader(type) {
|
|
94
|
+
try {
|
|
95
|
+
const first = this.schema(type).fields[0];
|
|
96
|
+
return first?.name === 'header' && first.type === HEADER_TYPE;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// On the ROS clock: the simulation's with use_sim_time
|
|
103
|
+
now() {
|
|
104
|
+
const { seconds, nanoseconds } = this.node.getClock().now().secondsAndNanoseconds;
|
|
105
|
+
return { sec: Number(seconds), nanosec: Number(nanoseconds) };
|
|
106
|
+
}
|
|
107
|
+
schema(type) {
|
|
108
|
+
const known = this.schemas.get(type);
|
|
109
|
+
if (known) {
|
|
110
|
+
return known;
|
|
111
|
+
}
|
|
112
|
+
const { ROSMessageDef: def } = load(type);
|
|
113
|
+
const schema = {
|
|
114
|
+
fields: def.fields.map(({ name, type: t, default_value }) => ({
|
|
115
|
+
name,
|
|
116
|
+
type: t.isPrimitiveType ? t.type : nestedType(t.pkgName ?? '', t.type),
|
|
117
|
+
primitive: t.isPrimitiveType,
|
|
118
|
+
array: !t.isArray ? 'none' : t.isFixedSizeArray ? 'fixed' : t.isUpperBound ? 'bounded' : 'dynamic',
|
|
119
|
+
size: t.isArray && (t.isFixedSizeArray || t.isUpperBound) ? t.arraySize : null,
|
|
120
|
+
stringBound: t.stringUpperBound ?? null,
|
|
121
|
+
default: plain(default_value) ?? null,
|
|
122
|
+
})),
|
|
123
|
+
constants: def.constants.map(({ name, type: t, value }) => ({ name, type: t, value: plain(value) })),
|
|
124
|
+
};
|
|
125
|
+
this.schemas.set(type, schema);
|
|
126
|
+
return schema;
|
|
127
|
+
}
|
|
128
|
+
// Its file's text, expanded; null if not found
|
|
129
|
+
definition(pkg, kind, name) {
|
|
130
|
+
const text = readDefinition(pkg, kind, name);
|
|
131
|
+
return text === null ? null : expandDefinition(text, pkg, (p, n) => readDefinition(p, 'msg', n));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function readDefinition(pkg, kind, name) {
|
|
135
|
+
const share = packageShareDirectory(pkg);
|
|
136
|
+
if (!share) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return readFileSync(path.join(share, kind, `${name}.${kind}`), 'utf8');
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Logger } from './util.ts';
|
|
2
|
+
export interface MissingPackage {
|
|
3
|
+
name: string;
|
|
4
|
+
prefix: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function expectedFiles(pkg: string, index: string, blocked?: (name: string) => boolean): string[];
|
|
7
|
+
export declare function missingPackages(prefixes: string[], generatedRoot: string, blocklist?: {
|
|
8
|
+
pkgName?: string;
|
|
9
|
+
interfaceName?: string;
|
|
10
|
+
}[]): Promise<MissingPackage[]>;
|
|
11
|
+
export declare function missingCodeHint(message: string): string;
|
|
12
|
+
export declare function makeMessageCode(logger: Logger): Promise<void>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// rclnodejs's JavaScript code of the ROS interfaces, without which it cannot
|
|
2
|
+
// use them: rclnodejs makes it when installed, for the packages sourced then.
|
|
3
|
+
// Those sourced since (a robot's own installation, a workspace), or rebuilt
|
|
4
|
+
// with new interfaces, are made here at start, before anything uses them: by
|
|
5
|
+
// rclnodejs's own generator, as it makes them when installed, run on a prefix
|
|
6
|
+
// of their own (their ament index entries, their share folders linked). Its
|
|
7
|
+
// own fallback misses the interfaces of services and actions, and makes a
|
|
8
|
+
// whole prefix's while the server waits.
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { errorMessage, tasselloRoot } from './util.js';
|
|
15
|
+
// Where the ament index lists each package's interfaces
|
|
16
|
+
const INDEX = path.join('share', 'ament_index', 'resource_index', 'rosidl_interfaces');
|
|
17
|
+
const GENERATION_TIMEOUT_MS = 5 * 60_000;
|
|
18
|
+
// The files rclnodejs makes of a package's interfaces, from its entry in the
|
|
19
|
+
// ament index: msg/Pose.msg, msg/Pose.idl give <pkg>__msg__Pose.js. The
|
|
20
|
+
// services' and actions' parts are made with them.
|
|
21
|
+
export function expectedFiles(pkg, index, blocked = () => false) {
|
|
22
|
+
const files = new Set();
|
|
23
|
+
for (const line of index.split('\n')) {
|
|
24
|
+
const match = /^([\w-]+)\/(\w+)\.(msg|srv|action|idl)$/.exec(line.trim());
|
|
25
|
+
if (match && !blocked(match[2])) {
|
|
26
|
+
// msg-common: msg, as rclnodejs names them
|
|
27
|
+
files.add(`${pkg}__${match[1].split('-')[0]}__${match[2]}.js`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [...files];
|
|
31
|
+
}
|
|
32
|
+
// The packages of the prefixes whose code is not all made: the first of each
|
|
33
|
+
// name, as the prefixes overlay those after them
|
|
34
|
+
export async function missingPackages(prefixes, generatedRoot, blocklist = []) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const missing = [];
|
|
37
|
+
const filters = blocklist.map((f) => ({
|
|
38
|
+
pkg: f.pkgName ? new RegExp(f.pkgName) : null,
|
|
39
|
+
name: f.interfaceName ? new RegExp(f.interfaceName) : null,
|
|
40
|
+
}));
|
|
41
|
+
for (const prefix of prefixes) {
|
|
42
|
+
let packages;
|
|
43
|
+
try {
|
|
44
|
+
packages = await fs.readdir(path.join(prefix, INDEX));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const name of packages.sort()) {
|
|
50
|
+
if (seen.has(name)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
seen.add(name);
|
|
54
|
+
const applying = filters.filter((f) => !f.pkg || f.pkg.test(name));
|
|
55
|
+
if (applying.some((f) => !f.name)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
let index;
|
|
59
|
+
try {
|
|
60
|
+
index = await fs.readFile(path.join(prefix, INDEX, name), 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const expected = expectedFiles(name, index, (interfaceName) => applying.some((f) => f.name?.test(interfaceName)));
|
|
66
|
+
const made = new Set(await fs.readdir(path.join(generatedRoot, name)).catch(() => []));
|
|
67
|
+
if (expected.some((file) => !made.has(file))) {
|
|
68
|
+
missing.push({ name, prefix });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return missing;
|
|
73
|
+
}
|
|
74
|
+
// What to do when rclnodejs has no code of an interface, after its error
|
|
75
|
+
export function missingCodeHint(message) {
|
|
76
|
+
return /message required does not exist/i.test(message)
|
|
77
|
+
? `${message}. Tassello makes the code of the interfaces sourced when it starts: restart it with their workspace sourced, or run npx generate-ros-messages in ${tasselloRoot()}`
|
|
78
|
+
: message;
|
|
79
|
+
}
|
|
80
|
+
function rclnodejsRoot() {
|
|
81
|
+
return path.dirname(createRequire(import.meta.url).resolve('rclnodejs/package.json'));
|
|
82
|
+
}
|
|
83
|
+
// Runs rclnodejs's generator on the prefix: rejects with its error output if
|
|
84
|
+
// it fails
|
|
85
|
+
function generate(worker, prefix) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const child = spawn(process.execPath, [worker], {
|
|
88
|
+
env: { ...process.env, WORKER_TARGET_PATH: prefix },
|
|
89
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
90
|
+
});
|
|
91
|
+
let stderr = '';
|
|
92
|
+
child.stderr.on('data', (data) => {
|
|
93
|
+
stderr = (stderr + data.toString()).slice(-2000);
|
|
94
|
+
});
|
|
95
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), GENERATION_TIMEOUT_MS);
|
|
96
|
+
child.on('error', (e) => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
reject(e);
|
|
99
|
+
});
|
|
100
|
+
child.on('close', (code, signal) => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
if (code === 0) {
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
reject(new Error(stderr.trim() || (signal ? `killed by ${signal}` : `exit code ${code}`)));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Makes the code of the packages sourced whose code is missing; says what it
|
|
112
|
+
// did, or why it could not, and goes on: those packages stay unusable then
|
|
113
|
+
export async function makeMessageCode(logger) {
|
|
114
|
+
const prefixes = (process.env.AMENT_PREFIX_PATH ?? '').split(path.delimiter).filter(Boolean);
|
|
115
|
+
let root;
|
|
116
|
+
try {
|
|
117
|
+
root = rclnodejsRoot();
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
logger.warn(`Cannot find rclnodejs to check its code of the ROS interfaces: ${errorMessage(e)}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const generatedRoot = path.join(root, 'generated');
|
|
124
|
+
let blocklist = [];
|
|
125
|
+
try {
|
|
126
|
+
blocklist = JSON.parse(await fs.readFile(path.join(root, 'rosidl_gen', 'blocklist.json'), 'utf8'));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// None
|
|
130
|
+
}
|
|
131
|
+
const missing = await missingPackages(prefixes, generatedRoot, blocklist);
|
|
132
|
+
if (missing.length === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const names = missing.map((p) => p.name).join(', ');
|
|
136
|
+
const worker = path.join(root, 'rosidl_gen', 'generate_worker.cjs');
|
|
137
|
+
if (!existsSync(worker)) {
|
|
138
|
+
logger.warn(`rclnodejs has no code of the interfaces of ${names}: run npx generate-ros-messages in ${tasselloRoot()} with them sourced`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
logger.info(`Making rclnodejs's code of the ROS interfaces it has not, of ${missing.length} package(s): ${names}`);
|
|
142
|
+
const started = Date.now();
|
|
143
|
+
const prefix = await fs.mkdtemp(path.join(os.tmpdir(), 'tassello-interfaces-'));
|
|
144
|
+
try {
|
|
145
|
+
await fs.mkdir(path.join(prefix, INDEX), { recursive: true });
|
|
146
|
+
for (const pkg of missing) {
|
|
147
|
+
await fs.copyFile(path.join(pkg.prefix, INDEX, pkg.name), path.join(prefix, INDEX, pkg.name));
|
|
148
|
+
await fs.symlink(path.join(pkg.prefix, 'share', pkg.name), path.join(prefix, 'share', pkg.name));
|
|
149
|
+
}
|
|
150
|
+
await generate(worker, prefix);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
logger.warn(`Could not make rclnodejs's code of the interfaces of ${names} (${errorMessage(e)}): run npx generate-ros-messages in ${tasselloRoot()} with them sourced, as a user who may write in ${generatedRoot}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await fs.rm(prefix, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
const left = await missingPackages(missing.map((p) => p.prefix).filter((p, i, all) => all.indexOf(p) === i), generatedRoot, blocklist);
|
|
160
|
+
const still = left.filter((p) => missing.some((m) => m.name === p.name));
|
|
161
|
+
if (still.length > 0) {
|
|
162
|
+
logger.warn(`rclnodejs's code of some interfaces of ${still.map((p) => p.name).join(', ')} could not be made`);
|
|
163
|
+
}
|
|
164
|
+
logger.info(`Made in ${((Date.now() - started) / 1000).toFixed(1)} s`);
|
|
165
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { InterfaceKind, MessageSchema } from '../shared/monitor.ts';
|
|
2
|
+
export declare const TIME_TYPE = "builtin_interfaces/msg/Time";
|
|
3
|
+
export declare const HEADER_TYPE = "std_msgs/msg/Header";
|
|
4
|
+
export declare function isPrimitive(type: string): boolean;
|
|
5
|
+
export declare function parseType(type: string): {
|
|
6
|
+
pkg: string;
|
|
7
|
+
kind: InterfaceKind;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function partTypes(type: string): {
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
}[];
|
|
14
|
+
export type DefinitionReader = (pkg: string, name: string) => string | null;
|
|
15
|
+
export declare function nestedFieldType(line: string, pkg: string): [string, string] | null;
|
|
16
|
+
export declare function expandDefinition(text: string, pkg: string, read: DefinitionReader, depth?: number): string;
|
|
17
|
+
export declare function defaultMessage(type: string, schemas: Record<string, MessageSchema>): Record<string, unknown>;
|
|
18
|
+
export interface RosTime {
|
|
19
|
+
sec: number;
|
|
20
|
+
nanosec: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function toRosMessage(value: unknown, type: string, schemas: Record<string, MessageSchema>, now: () => RosTime, path?: string): Record<string, unknown>;
|
|
23
|
+
export declare function fieldAt(message: unknown, path: string): unknown;
|
|
24
|
+
export declare function echoJson(value: unknown, limit: number): unknown;
|