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.
- package/CHANGELOG.md +15 -0
- package/README.md +70 -7
- package/dist/server/networking.d.ts +41 -0
- package/dist/server/networking.js +508 -0
- package/dist/server/networking_format.d.ts +38 -0
- package/dist/server/networking_format.js +651 -0
- package/dist/server/server.js +5 -1
- package/dist/server/session.d.ts +4 -1
- package/dist/server/session.js +23 -2
- package/dist/shared/networking.d.ts +178 -0
- package/dist/shared/networking.js +248 -0
- package/dist/shared/protocol.d.ts +18 -2
- package/dist/types/client/connection.d.ts +9 -1
- package/dist/types/client/icons.d.ts +6 -0
- package/dist/types/client/sdk/index.d.ts +2 -1
- package/dist/types/shared/networking.d.ts +178 -0
- package/dist/types/shared/protocol.d.ts +18 -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/icons.js +1 -1
- package/dist/www/assets/index.css +1 -1
- package/dist/www/assets/index.js +42 -42
- package/dist/www/assets/shared/tassello-sdk.js +1 -1
- package/docs/plugins.md +2 -0
- package/package.json +1 -1
package/dist/server/session.d.ts
CHANGED
|
@@ -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;
|
package/dist/server/session.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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';
|