tassello 0.3.0 → 0.4.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.
@@ -7,6 +7,7 @@ import type { BehaviorTrees } from './behavior_trees.ts';
7
7
  import type { CommandRunner } from './commands.ts';
8
8
  import type { Diagnostics } from './diagnostics.ts';
9
9
  import type { EncodedFrame, FrameSink, StreamState } from './frame_stream.ts';
10
+ import type { Networking } from './networking.ts';
10
11
  import type { Poses } from './poses.ts';
11
12
  import type { Paths } from './paths.ts';
12
13
  import type { PluginHost } from './plugins.ts';
@@ -32,6 +33,7 @@ export interface SessionServices {
32
33
  rosout: Rosout;
33
34
  diagnostics: Diagnostics;
34
35
  terminals: Terminals;
36
+ networking: Networking;
35
37
  behaviorTrees: BehaviorTrees;
36
38
  behaviorTreeFiles: BehaviorTreeFiles;
37
39
  plugins: PluginHost;
@@ -47,8 +49,9 @@ export declare class Session implements FrameSink, PluginSession {
47
49
  private readonly playbacks;
48
50
  private readonly terminals;
49
51
  private readonly ownPage;
52
+ private readonly localAddress;
50
53
  private lastError;
51
- constructor(ws: WebSocket, services: SessionServices, ownPage: boolean);
54
+ constructor(ws: WebSocket, services: SessionServices, ownPage: boolean, localAddress: string | null);
52
55
  send(message: ServerMessage): void;
53
56
  sendStreamStatus(id: number, state: StreamState, message: string): void;
54
57
  sendFrame(id: number, frame: EncodedFrame): void;
@@ -4,6 +4,7 @@
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { WebSocket } from 'ws';
6
6
  import { FRAME_HEADER_BYTES, } from '../shared/protocol.js';
7
+ import { checkRequest, deviceOfAddress } from './networking_format.js';
7
8
  import { errorMessage, toJson } from './util.js';
8
9
  // The same error is reported to the page at most this often: a teleop widget
9
10
  // publishing on an invalid topic would otherwise report it 10 times a second
@@ -60,13 +61,16 @@ export class Session {
60
61
  // The terminals, for what is typed in them
61
62
  terminals = new Map();
62
63
  // The page is one of the dashboard's own (see ownPage): only those open
63
- // terminals
64
+ // terminals and change the networks
64
65
  ownPage;
66
+ // The server's address the page reached: the interface it comes through
67
+ localAddress;
65
68
  lastError = { message: '', at: 0 };
66
- constructor(ws, services, ownPage) {
69
+ constructor(ws, services, ownPage, localAddress) {
67
70
  this.ws = ws;
68
71
  this.services = services;
69
72
  this.ownPage = ownPage;
73
+ this.localAddress = localAddress;
70
74
  ws.on('message', (data, isBinary) => {
71
75
  if (!isBinary) {
72
76
  this.onMessage(data.toString());
@@ -302,6 +306,23 @@ export class Session {
302
306
  case 'terminal.restart':
303
307
  this.terminals.get(subscriptionId(message.id))?.restart();
304
308
  return;
309
+ case 'network.subscribe': {
310
+ const id = subscriptionId(message.id);
311
+ this.unsubscribe(id);
312
+ this.subscriptions.set(id, services.networking.watch((state) => this.send({
313
+ type: 'network',
314
+ id,
315
+ ...state,
316
+ via: deviceOfAddress(state.devices, this.localAddress),
317
+ canChange: this.ownPage,
318
+ })));
319
+ return;
320
+ }
321
+ case 'network.action':
322
+ this.answer(message.id, this.ownPage
323
+ ? Promise.resolve(message.request).then((request) => services.networking.act(checkRequest(request)))
324
+ : Promise.reject(new Error('The networks are changed from the dashboard’s own pages only')));
325
+ return;
305
326
  case 'arm.subscribe': {
306
327
  const id = subscriptionId(message.id);
307
328
  this.unsubscribe(id);
@@ -0,0 +1,178 @@
1
+ export type WifiSecurity = 'open' | 'owe' | 'wep' | 'wpa-psk' | 'sae' | 'wpa-eap';
2
+ export type ConnectionKind = 'wifi' | 'hotspot' | 'ethernet' | 'vpn' | 'other';
3
+ export type EditableKind = 'wifi' | 'hotspot' | 'ethernet';
4
+ export type Ipv4Method = 'auto' | 'manual' | 'shared' | 'disabled';
5
+ export declare const DEVICE_UNMANAGED = 10;
6
+ export declare const DEVICE_UNAVAILABLE = 20;
7
+ export declare const DEVICE_DISCONNECTED = 30;
8
+ export declare const DEVICE_ACTIVATED = 100;
9
+ export declare const DEVICE_DEACTIVATING = 110;
10
+ export declare const DEVICE_FAILED = 120;
11
+ export interface NetworkDevice {
12
+ name: string;
13
+ type: string;
14
+ state: number;
15
+ stateText: string;
16
+ reason: string;
17
+ connection: string | null;
18
+ connectionName: string;
19
+ external: boolean;
20
+ software: boolean;
21
+ hwaddr: string;
22
+ mtu: number | null;
23
+ carrier: boolean | null;
24
+ speed: string;
25
+ driver: string;
26
+ vendor: string;
27
+ product: string;
28
+ accessPoint: boolean;
29
+ band24: boolean;
30
+ band5: boolean;
31
+ ipv4: string[];
32
+ ipv6: string[];
33
+ gateway: string;
34
+ dns: string[];
35
+ }
36
+ export interface NetworkConnection {
37
+ uuid: string;
38
+ name: string;
39
+ type: string;
40
+ kind: ConnectionKind;
41
+ interface: string;
42
+ autoconnect: boolean;
43
+ priority: number;
44
+ lastUsed: number | null;
45
+ readOnly: boolean;
46
+ file: string;
47
+ device: string | null;
48
+ activeState: string | null;
49
+ external: boolean;
50
+ ssid: string;
51
+ security: WifiSecurity | null;
52
+ hidden: boolean;
53
+ band: '' | 'a' | 'bg';
54
+ channel: number;
55
+ identity: string;
56
+ vpnService: string;
57
+ ipv4Method: string;
58
+ addresses: string[];
59
+ gateway: string;
60
+ dns: string[];
61
+ }
62
+ export interface WifiNetwork {
63
+ ssid: string;
64
+ device: string;
65
+ bssid: string;
66
+ signal: number;
67
+ frequency: number;
68
+ channel: number;
69
+ rate: string;
70
+ securityText: string;
71
+ security: WifiSecurity;
72
+ mode: string;
73
+ active: boolean;
74
+ points: number;
75
+ }
76
+ export interface NetworkActivity {
77
+ id: number;
78
+ action: NetworkRequest['action'];
79
+ label: string;
80
+ uuid: string | null;
81
+ ssid: string | null;
82
+ device: string | null;
83
+ started: number;
84
+ finished: number | null;
85
+ error: string | null;
86
+ }
87
+ export interface NetworkPermissions {
88
+ control: boolean;
89
+ modify: boolean;
90
+ wifi: boolean;
91
+ scan: boolean;
92
+ share: boolean;
93
+ }
94
+ export interface NetworkState {
95
+ error: string | null;
96
+ version: string;
97
+ state: string;
98
+ connectivity: string;
99
+ wifi: boolean;
100
+ wifiHardware: boolean;
101
+ permissions: NetworkPermissions;
102
+ devices: NetworkDevice[];
103
+ connections: NetworkConnection[];
104
+ networks: WifiNetwork[];
105
+ scanning: boolean;
106
+ activity: NetworkActivity[];
107
+ }
108
+ export interface NetworkSettings {
109
+ kind: EditableKind;
110
+ name: string;
111
+ interface: string;
112
+ autoconnect: boolean;
113
+ priority: number;
114
+ ssid: string;
115
+ hidden: boolean;
116
+ security: WifiSecurity;
117
+ password: string | null;
118
+ identity: string;
119
+ band: '' | 'a' | 'bg';
120
+ channel: number;
121
+ ipv4: Ipv4Method;
122
+ addresses: string[];
123
+ gateway: string;
124
+ dns: string[];
125
+ }
126
+ export type NetworkRequest = {
127
+ action: 'connect';
128
+ uuid: string;
129
+ device: string | null;
130
+ password: string | null;
131
+ } | {
132
+ action: 'disconnect';
133
+ uuid: string;
134
+ } | {
135
+ action: 'create';
136
+ settings: NetworkSettings;
137
+ connect: boolean;
138
+ device: string | null;
139
+ } | {
140
+ action: 'modify';
141
+ uuid: string;
142
+ settings: NetworkSettings;
143
+ } | {
144
+ action: 'delete';
145
+ uuid: string;
146
+ } | {
147
+ action: 'wifi';
148
+ enabled: boolean;
149
+ } | {
150
+ action: 'rescan';
151
+ device: string | null;
152
+ } | {
153
+ action: 'secrets';
154
+ uuid: string;
155
+ };
156
+ export interface NetworkSecrets {
157
+ ssid: string;
158
+ security: WifiSecurity | null;
159
+ password: string;
160
+ hidden: boolean;
161
+ }
162
+ export interface NetworkResult {
163
+ uuid: string | null;
164
+ message: string | null;
165
+ }
166
+ export declare const WRONG_PASSWORD = "Wrong password, or none given";
167
+ export declare const SECURITY_LABELS: Record<WifiSecurity, string>;
168
+ export declare function needsPassword(security: WifiSecurity): boolean;
169
+ export declare function isInterfaceName(name: string): boolean;
170
+ export declare function isUuid(uuid: string): boolean;
171
+ export declare function isIpv4(address: string): boolean;
172
+ export declare function isIpv6(address: string): boolean;
173
+ export declare function cidrProblem(cidr: string): string | null;
174
+ export declare function splitList(text: string): string[];
175
+ export declare function passwordProblem(security: WifiSecurity, password: string, hotspot?: boolean): string | null;
176
+ export declare function settingsProblems(settings: NetworkSettings, creating: boolean): Partial<Record<keyof NetworkSettings, string>>;
177
+ export declare function settingsOf(connection: NetworkConnection): NetworkSettings | null;
178
+ export declare function defaultSettings(kind: EditableKind): NetworkSettings;
@@ -0,0 +1,248 @@
1
+ // The robot's networks, as NetworkManager has them: what the Networking
2
+ // widget shows and changes (see src/server/networking.ts), and the checks of
3
+ // the settings it sends, which the page makes as they are typed and the
4
+ // server again before it passes them to nmcli.
5
+ // NetworkManager's device states (NMDeviceState)
6
+ export const DEVICE_UNMANAGED = 10;
7
+ export const DEVICE_UNAVAILABLE = 20;
8
+ export const DEVICE_DISCONNECTED = 30;
9
+ export const DEVICE_ACTIVATED = 100;
10
+ export const DEVICE_DEACTIVATING = 110;
11
+ export const DEVICE_FAILED = 120;
12
+ // The reason a connection fails with when its password is wrong: the page
13
+ // asks for it again
14
+ export const WRONG_PASSWORD = 'Wrong password, or none given';
15
+ export const SECURITY_LABELS = {
16
+ open: 'None (open)',
17
+ owe: 'Enhanced Open (OWE)',
18
+ wep: 'WEP',
19
+ 'wpa-psk': 'WPA/WPA2 Personal',
20
+ sae: 'WPA3 Personal',
21
+ 'wpa-eap': 'WPA/WPA2 Enterprise',
22
+ };
23
+ export function needsPassword(security) {
24
+ return security !== 'open' && security !== 'owe';
25
+ }
26
+ // Linux's: at most 15 characters, no slashes, spaces or colons
27
+ const INTERFACE = /^[\w.@-]{1,15}$/;
28
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29
+ // Of the 2.4 GHz band, and of the 5 GHz one (20 MHz channels)
30
+ const CHANNELS_24 = Array.from({ length: 14 }, (_, i) => i + 1);
31
+ const CHANNELS_5 = [
32
+ 36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 149, 153, 157, 161, 165,
33
+ 169, 173, 177,
34
+ ];
35
+ export function isInterfaceName(name) {
36
+ return INTERFACE.test(name);
37
+ }
38
+ export function isUuid(uuid) {
39
+ return UUID.test(uuid);
40
+ }
41
+ export function isIpv4(address) {
42
+ const parts = address.split('.');
43
+ return parts.length === 4 && parts.every((part) => /^(0|[1-9]\d{0,2})$/.test(part) && Number(part) <= 255);
44
+ }
45
+ export function isIpv6(address) {
46
+ if (!/^[0-9a-f:.]+$/i.test(address) || !address.includes(':')) {
47
+ return false;
48
+ }
49
+ const halves = address.split('::');
50
+ if (halves.length > 2) {
51
+ return false;
52
+ }
53
+ const groups = halves.flatMap((half) => (half ? half.split(':') : []));
54
+ // An IPv4 tail counts as two groups
55
+ const last = groups.at(-1) ?? '';
56
+ const ipv4Tail = last.includes('.');
57
+ if (ipv4Tail && !isIpv4(last)) {
58
+ return false;
59
+ }
60
+ const hex = ipv4Tail ? groups.slice(0, -1) : groups;
61
+ if (!hex.every((group) => /^[0-9a-f]{1,4}$/i.test(group))) {
62
+ return false;
63
+ }
64
+ const count = hex.length + (ipv4Tail ? 2 : 0);
65
+ return halves.length === 2 ? count < 8 : count === 8;
66
+ }
67
+ // 192.168.1.20/24: an address of a network, not the network itself
68
+ export function cidrProblem(cidr) {
69
+ const [address, prefix, ...rest] = cidr.trim().split('/');
70
+ if (rest.length > 0 || !isIpv4(address)) {
71
+ return `“${cidr.trim()}” is not an IPv4 address`;
72
+ }
73
+ if (prefix === undefined) {
74
+ return `Give its network's size: ${address}/24`;
75
+ }
76
+ if (!/^\d{1,2}$/.test(prefix) || Number(prefix) < 1 || Number(prefix) > 32) {
77
+ return `/${prefix} is not a network's size: /1 to /32 (/24: 255.255.255.0)`;
78
+ }
79
+ return null;
80
+ }
81
+ // Separated by commas, spaces or new lines
82
+ export function splitList(text) {
83
+ return text
84
+ .split(/[\s,;]+/)
85
+ .map((item) => item.trim())
86
+ .filter(Boolean);
87
+ }
88
+ function utf8Length(text) {
89
+ return new TextEncoder().encode(text).length;
90
+ }
91
+ // Of a Wi-Fi network or an access point, by its security; null if fine
92
+ export function passwordProblem(security, password, hotspot = false) {
93
+ switch (security) {
94
+ case 'open':
95
+ case 'owe':
96
+ return null;
97
+ case 'wpa-psk':
98
+ if (/^[0-9a-f]{64}$/i.test(password)) {
99
+ return null;
100
+ }
101
+ if (!/^[\x20-\x7e]*$/.test(password)) {
102
+ return 'Letters, digits and symbols of the keyboard only: no accents';
103
+ }
104
+ if (password.length < 8 || password.length > 63) {
105
+ return hotspot ? '8 to 63 characters' : 'WPA passwords have 8 to 63 characters';
106
+ }
107
+ return null;
108
+ case 'sae':
109
+ return password.length === 0 ? 'The network’s password' : password.length > 128 ? 'At most 128 characters' : null;
110
+ case 'wep':
111
+ return /^([\x20-\x7e]{5}|[\x20-\x7e]{13}|[0-9a-f]{10}|[0-9a-f]{26})$/i.test(password)
112
+ ? null
113
+ : 'WEP keys have 5 or 13 characters, or 10 or 26 hexadecimal digits';
114
+ case 'wpa-eap':
115
+ return password.length === 0 ? 'Your password on the network' : null;
116
+ }
117
+ }
118
+ // What is wrong with the settings, by field: empty if nothing. creating: a
119
+ // new connection, whose password is required.
120
+ export function settingsProblems(settings, creating) {
121
+ const problems = {};
122
+ const { kind } = settings;
123
+ const name = settings.name.trim();
124
+ if (!name) {
125
+ problems.name = 'A name for the connection';
126
+ }
127
+ else if (name.length > 255 || /[\x00-\x1f]/.test(name)) {
128
+ problems.name = 'At most 255 characters, on one line';
129
+ }
130
+ if (settings.interface && !isInterfaceName(settings.interface)) {
131
+ problems.interface = `“${settings.interface}” is not an interface's name`;
132
+ }
133
+ else if (kind === 'hotspot' && !settings.interface) {
134
+ problems.interface = 'The Wi-Fi interface of the access point';
135
+ }
136
+ if (!Number.isInteger(settings.priority) || settings.priority < -999 || settings.priority > 999) {
137
+ problems.priority = 'A whole number, from -999 to 999';
138
+ }
139
+ if (kind !== 'ethernet') {
140
+ const bytes = utf8Length(settings.ssid);
141
+ if (bytes === 0) {
142
+ problems.ssid = kind === 'hotspot' ? 'The name the access point shows' : 'The network’s name';
143
+ }
144
+ else if (bytes > 32) {
145
+ problems.ssid = 'At most 32 bytes (fewer with accents and symbols)';
146
+ }
147
+ else if (/[\x00-\x1f]/.test(settings.ssid)) {
148
+ problems.ssid = 'No control characters';
149
+ }
150
+ if (kind === 'hotspot' && settings.security !== 'open' && settings.security !== 'wpa-psk') {
151
+ problems.security = 'An access point is open, or secured by a WPA2 password';
152
+ }
153
+ if (settings.security === 'wpa-eap') {
154
+ if (!settings.identity.trim()) {
155
+ problems.identity = 'Your user name on the network';
156
+ }
157
+ else if (/[\x00-\x1f]/.test(settings.identity)) {
158
+ problems.identity = 'No control characters';
159
+ }
160
+ }
161
+ if (settings.password !== null) {
162
+ const problem = passwordProblem(settings.security, settings.password, kind === 'hotspot');
163
+ if (problem) {
164
+ problems.password = problem;
165
+ }
166
+ }
167
+ else if (creating && needsPassword(settings.security)) {
168
+ problems.password = 'The network’s password';
169
+ }
170
+ if (kind === 'hotspot') {
171
+ const channels = settings.band === 'a' ? CHANNELS_5 : settings.band === 'bg' ? CHANNELS_24 : [];
172
+ if (settings.channel !== 0 && !channels.includes(settings.channel)) {
173
+ problems.channel = settings.band
174
+ ? `Not a channel of ${settings.band === 'a' ? '5' : '2.4'} GHz`
175
+ : 'Choose the band of the channel';
176
+ }
177
+ }
178
+ }
179
+ const methods = kind === 'hotspot' ? ['shared'] : ['auto', 'manual', 'shared', 'disabled'];
180
+ if (!methods.includes(settings.ipv4)) {
181
+ problems.ipv4 = kind === 'hotspot' ? 'An access point gives the addresses (shared)' : 'Not a way to get an address';
182
+ }
183
+ const addressProblem = settings.addresses.map(cidrProblem).find((problem) => problem !== null);
184
+ if (addressProblem) {
185
+ problems.addresses = addressProblem;
186
+ }
187
+ else if (settings.ipv4 === 'manual' && settings.addresses.length === 0) {
188
+ problems.addresses = 'The robot’s address on this network: 192.168.1.20/24';
189
+ }
190
+ else if (settings.ipv4 === 'shared' && settings.addresses.length > 1) {
191
+ problems.addresses = 'One address: the robot’s, on the network it gives';
192
+ }
193
+ if (settings.gateway && !isIpv4(settings.gateway)) {
194
+ problems.gateway = `“${settings.gateway}” is not an IPv4 address`;
195
+ }
196
+ const badDns = settings.dns.find((server) => !isIpv4(server) && !isIpv6(server));
197
+ if (badDns !== undefined) {
198
+ problems.dns = `“${badDns}” is not an IP address`;
199
+ }
200
+ return problems;
201
+ }
202
+ // The settings of a saved connection, to edit them (its password unchanged)
203
+ export function settingsOf(connection) {
204
+ const kind = connection.kind;
205
+ if (kind !== 'wifi' && kind !== 'hotspot' && kind !== 'ethernet') {
206
+ return null;
207
+ }
208
+ const methods = ['auto', 'manual', 'shared', 'disabled'];
209
+ return {
210
+ kind,
211
+ name: connection.name,
212
+ interface: connection.interface,
213
+ autoconnect: connection.autoconnect,
214
+ priority: connection.priority,
215
+ ssid: connection.ssid,
216
+ hidden: connection.hidden,
217
+ security: connection.security ?? 'open',
218
+ password: null,
219
+ identity: connection.identity,
220
+ band: connection.band,
221
+ channel: connection.channel,
222
+ ipv4: methods.includes(connection.ipv4Method) ? connection.ipv4Method : 'auto',
223
+ addresses: connection.addresses,
224
+ gateway: connection.gateway,
225
+ dns: connection.dns,
226
+ };
227
+ }
228
+ // A new connection's, of a kind
229
+ export function defaultSettings(kind) {
230
+ return {
231
+ kind,
232
+ name: '',
233
+ interface: '',
234
+ autoconnect: true,
235
+ priority: 0,
236
+ ssid: '',
237
+ hidden: false,
238
+ security: kind === 'ethernet' ? 'open' : 'wpa-psk',
239
+ password: kind === 'ethernet' ? null : '',
240
+ identity: '',
241
+ band: '',
242
+ channel: 0,
243
+ ipv4: kind === 'hotspot' ? 'shared' : 'auto',
244
+ addresses: [],
245
+ gateway: '',
246
+ dns: [],
247
+ };
248
+ }
@@ -1,4 +1,5 @@
1
1
  import type { BtNodeModel } from './bt_nodes.ts';
2
+ import type { NetworkRequest, NetworkState } from './networking.ts';
2
3
  export type ImageTransport = 'auto' | 'raw' | 'compressed';
3
4
  export type GridStyle = 'map' | 'costmap';
4
5
  export interface TwistCommand {
@@ -229,7 +230,16 @@ export interface TerminalRestart {
229
230
  type: 'terminal.restart';
230
231
  id: number;
231
232
  }
232
- export type ClientMessage = TwistCommand | ImageSubscribe | GridSubscribe | TfSubscribe | PathSubscribe | TopicSubscribe | ServicesSubscribe | ServiceAction | LogsSubscribe | Unsubscribe | FrameAck | PosePublish | CommandRun | CommandStop | ArmSubscribe | ArmJog | RosPublish | RosCall | PluginRequest | PluginSubscribe | BehaviorTreeSubscribe | BehaviorTreeRecord | BehaviorTreePlaybackSubscribe | BehaviorTreePlaybackControl | BehaviorTreeFileSubscribe | BehaviorTreeFileSave | RosoutSubscribe | DiagnosticsSubscribe | TerminalOpen | TerminalInput | TerminalResize | TerminalAck | TerminalRestart;
233
+ export interface NetworkSubscribe {
234
+ type: 'network.subscribe';
235
+ id: number;
236
+ }
237
+ export interface NetworkAction {
238
+ type: 'network.action';
239
+ id: number;
240
+ request: NetworkRequest;
241
+ }
242
+ export type ClientMessage = TwistCommand | ImageSubscribe | GridSubscribe | TfSubscribe | PathSubscribe | TopicSubscribe | ServicesSubscribe | ServiceAction | LogsSubscribe | Unsubscribe | FrameAck | PosePublish | CommandRun | CommandStop | ArmSubscribe | ArmJog | RosPublish | RosCall | PluginRequest | PluginSubscribe | BehaviorTreeSubscribe | BehaviorTreeRecord | BehaviorTreePlaybackSubscribe | BehaviorTreePlaybackControl | BehaviorTreeFileSubscribe | BehaviorTreeFileSave | RosoutSubscribe | DiagnosticsSubscribe | TerminalOpen | TerminalInput | TerminalResize | TerminalAck | TerminalRestart | NetworkSubscribe | NetworkAction;
233
243
  export interface Hello {
234
244
  type: 'hello';
235
245
  clientId: string;
@@ -510,7 +520,13 @@ export interface TerminalExit {
510
520
  started: number;
511
521
  exitCode: number | null;
512
522
  }
513
- export type ServerMessage = Hello | CommandStates | CommandUpdate | StreamStatus | TfUpdate | PathUpdate | TopicMessage | ServicesUpdate | ServiceResult | LogsUpdate | ArmUpdate | Result | PluginData | BehaviorTreeInfo | BehaviorTreeUpdate | BehaviorTreeRecording | BehaviorTreePlaybackState | BehaviorTreeFile | BehaviorTreeModels | RosoutUpdate | DiagnosticsUpdate | TerminalOutput | TerminalExit | PanelsChanged | ErrorMessage;
523
+ export interface NetworkUpdate extends NetworkState {
524
+ type: 'network';
525
+ id: number;
526
+ via: string | null;
527
+ canChange: boolean;
528
+ }
529
+ export type ServerMessage = Hello | CommandStates | CommandUpdate | StreamStatus | TfUpdate | PathUpdate | TopicMessage | ServicesUpdate | ServiceResult | LogsUpdate | ArmUpdate | Result | PluginData | BehaviorTreeInfo | BehaviorTreeUpdate | BehaviorTreeRecording | BehaviorTreePlaybackState | BehaviorTreeFile | BehaviorTreeModels | RosoutUpdate | DiagnosticsUpdate | TerminalOutput | TerminalExit | NetworkUpdate | PanelsChanged | ErrorMessage;
514
530
  export declare const FRAME_HEADER_BYTES = 11;
515
531
  export declare const IMAGE_FORMATS: readonly ['image/jpeg', 'image/png'];
516
532
  export interface GridInfo {
@@ -1,4 +1,5 @@
1
- import { type ArmSubscribe, type ArmUpdate, type BehaviorTreeFile, type BehaviorTreeFileSubscribe, type BehaviorTreeInfo, type BehaviorTreeModels, type BehaviorTreePlaybackControl, type BehaviorTreePlaybackState, type BehaviorTreePlaybackSubscribe, type BehaviorTreeRecording, type BehaviorTreeSaved, type BehaviorTreeSubscribe, type BehaviorTreeUpdate, type ClientMessage, type CommandState, type DiagnosticsSubscribe, type DiagnosticsUpdate, type GridSubscribe, type ImageSubscribe, type LogsSubscribe, type LogsUpdate, type PathSubscribe, type PathUpdate, type RosoutSubscribe, type RosoutUpdate, type ServerMessage, type ServiceScope, type ServicesSubscribe, type ServicesUpdate, type ServiceVerb, type StreamStatus, type TerminalExit, type TerminalOpen, type TerminalOutput, type TfSubscribe, type TfUpdate, type TopicSubscribe } from '../shared/protocol.ts';
1
+ import type { NetworkRequest, NetworkResult, NetworkSecrets } from '../shared/networking.ts';
2
+ import { type ArmSubscribe, type ArmUpdate, type BehaviorTreeFile, type BehaviorTreeFileSubscribe, type BehaviorTreeInfo, type BehaviorTreeModels, type BehaviorTreePlaybackControl, type BehaviorTreePlaybackState, type BehaviorTreePlaybackSubscribe, type BehaviorTreeRecording, type BehaviorTreeSaved, type BehaviorTreeSubscribe, type BehaviorTreeUpdate, type ClientMessage, type CommandState, type DiagnosticsSubscribe, type DiagnosticsUpdate, type GridSubscribe, type ImageSubscribe, type LogsSubscribe, type LogsUpdate, type NetworkUpdate, type PathSubscribe, type PathUpdate, type RosoutSubscribe, type RosoutUpdate, type ServerMessage, type ServiceScope, type ServicesSubscribe, type ServicesUpdate, type ServiceVerb, type StreamStatus, type TerminalExit, type TerminalOpen, type TerminalOutput, type TfSubscribe, type TfUpdate, type TopicSubscribe } from '../shared/protocol.ts';
2
3
  export type ConnectionStatus = 'connecting' | 'open' | 'closed';
3
4
  export type ImageOptions = Omit<ImageSubscribe, 'type' | 'id'>;
4
5
  export type GridOptions = Omit<GridSubscribe, 'type' | 'id'>;
@@ -102,6 +103,13 @@ export declare class Connection {
102
103
  resizeTerminal(id: number, cols: number, rows: number): boolean;
103
104
  ackTerminal(id: number, started: number, offset: number): boolean;
104
105
  restartTerminal(id: number): boolean;
106
+ subscribeNetwork(callback: (update: NetworkUpdate) => void): () => void;
107
+ networkAction(request: Exclude<NetworkRequest, {
108
+ action: 'secrets';
109
+ }>): Promise<NetworkResult>;
110
+ networkAction(request: Extract<NetworkRequest, {
111
+ action: 'secrets';
112
+ }>): Promise<NetworkSecrets>;
105
113
  serviceAction(unit: string, scope: ServiceScope, verb: ServiceVerb): Promise<string | null>;
106
114
  publish(topic: string, messageType: string, message: unknown): boolean;
107
115
  callService<T = unknown>(service: string, serviceType: string, request?: unknown, timeoutMs?: number): Promise<T>;
@@ -83,3 +83,9 @@ export declare const MinimizeIcon: (props: SVGProps<SVGSVGElement>) => import("r
83
83
  export declare const ClipboardIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
84
84
  export declare const TextSmallerIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
85
85
  export declare const TextLargerIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
86
+ export declare const NetworkIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
87
+ export declare const WifiIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
88
+ export declare const WifiOffIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
89
+ export declare const EthernetIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
90
+ export declare const HotspotIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
91
+ export declare const ShieldIcon: (props: SVGProps<SVGSVGElement>) => import("react").JSX.Element;
@@ -5,7 +5,8 @@ export { useDialogs, Modal, type Dialogs, type ConfirmOptions, type PromptOption
5
5
  export { notify } from '../notifications.tsx';
6
6
  export { Joystick, type JoystickProps } from '../widgets/Joystick.tsx';
7
7
  export * from '../icons.tsx';
8
- export type { BehaviorTreeFile, BehaviorTreeInfo, BehaviorTreeModels, BehaviorTreePlaybackState, BehaviorTreeRecording, BehaviorTreeSaved, BehaviorTreeUpdate, BtNode, BtNodeLibrary, BtNodeState, BtTransition, CommandState, ClientMessage, DiagnosticChange, DiagnosticEntry, DiagnosticsUpdate, LogEntry, RosoutUpdate, ServerMessage, StreamStatus, TerminalExit, TerminalOutput, TfUpdate, Transform2D, } from '../../shared/protocol.ts';
8
+ export type { BehaviorTreeFile, BehaviorTreeInfo, BehaviorTreeModels, BehaviorTreePlaybackState, BehaviorTreeRecording, BehaviorTreeSaved, BehaviorTreeUpdate, BtNode, BtNodeLibrary, BtNodeState, BtTransition, CommandState, ClientMessage, DiagnosticChange, DiagnosticEntry, DiagnosticsUpdate, LogEntry, NetworkUpdate, RosoutUpdate, ServerMessage, StreamStatus, TerminalExit, TerminalOutput, TfUpdate, Transform2D, } from '../../shared/protocol.ts';
9
9
  export type { BtNodeModel, BtPortModel } from '../../shared/bt_nodes.ts';
10
+ export type { ConnectionKind, NetworkActivity, NetworkConnection, NetworkDevice, NetworkPermissions, NetworkRequest, NetworkResult, NetworkSecrets, NetworkSettings, NetworkState, WifiNetwork, WifiSecurity, } from '../../shared/networking.ts';
10
11
  export type { FsAnalysis, FsConflict, FsDetails, FsDisk, FsEntry, FsListing, FsOperation, FsPlaces, FsResult, FsSaved, FsSpace, FsText, } from '../../shared/file_manager.ts';
11
12
  export type { Panel, WidgetInstance } from '../../shared/panel.ts';