brilliantsole 0.0.26 → 0.0.28
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/README.md +16 -10
- package/assets/3d/anchor.glb +0 -0
- package/assets/3d/coin.glb +0 -0
- package/assets/3d/glasses.glb +0 -0
- package/assets/3d/rightHand.glb +0 -0
- package/assets/audio/bounceMedium.wav +0 -0
- package/assets/audio/bounceStrong.wav +0 -0
- package/assets/audio/bounceWeak.wav +0 -0
- package/assets/audio/coin.wav +0 -0
- package/assets/audio/getUp.wav +0 -0
- package/assets/audio/grab.wav +0 -0
- package/assets/audio/kick.wav +0 -0
- package/assets/audio/platterFadeIn old.wav +0 -0
- package/assets/audio/platterFadeIn.wav +0 -0
- package/assets/audio/platterFadeOut.wav +0 -0
- package/assets/audio/punch.wav +0 -0
- package/assets/audio/punchSqueak.wav +0 -0
- package/assets/audio/purr.wav +0 -0
- package/assets/audio/purrFadeOut.wav +0 -0
- package/assets/audio/release.wav +0 -0
- package/assets/audio/splat.wav +0 -0
- package/assets/audio/stomp.wav +0 -0
- package/assets/images/ukaton-pressure-0.svg +9 -0
- package/assets/images/ukaton-pressure-1.svg +9 -0
- package/assets/images/ukaton-pressure-10.svg +9 -0
- package/assets/images/ukaton-pressure-11.svg +9 -0
- package/assets/images/ukaton-pressure-12.svg +9 -0
- package/assets/images/ukaton-pressure-13.svg +9 -0
- package/assets/images/ukaton-pressure-14.svg +9 -0
- package/assets/images/ukaton-pressure-15.svg +9 -0
- package/assets/images/ukaton-pressure-2.svg +9 -0
- package/assets/images/ukaton-pressure-3.svg +9 -0
- package/assets/images/ukaton-pressure-4.svg +9 -0
- package/assets/images/ukaton-pressure-5.svg +9 -0
- package/assets/images/ukaton-pressure-6.svg +9 -0
- package/assets/images/ukaton-pressure-7.svg +9 -0
- package/assets/images/ukaton-pressure-8.svg +9 -0
- package/assets/images/ukaton-pressure-9.svg +9 -0
- package/assets/images/ukaton-right-insole.svg +798 -0
- package/build/brilliantsole.cjs +2870 -882
- package/build/brilliantsole.cjs.map +1 -1
- package/build/brilliantsole.js +2477 -782
- package/build/brilliantsole.js.map +1 -1
- package/build/brilliantsole.ls.js +2260 -592
- package/build/brilliantsole.ls.js.map +1 -1
- package/build/brilliantsole.min.js +1 -1
- package/build/brilliantsole.min.js.map +1 -1
- package/build/brilliantsole.module.d.ts +302 -116
- package/build/brilliantsole.module.js +2468 -782
- package/build/brilliantsole.module.js.map +1 -1
- package/build/brilliantsole.module.min.d.ts +302 -116
- package/build/brilliantsole.module.min.js +1 -1
- package/build/brilliantsole.module.min.js.map +1 -1
- package/build/brilliantsole.node.module.d.ts +295 -113
- package/build/brilliantsole.node.module.js +2860 -882
- package/build/brilliantsole.node.module.js.map +1 -1
- package/build/dts/BS-output.d.ts +10 -0
- package/build/dts/BS.d.ts +21 -9
- package/build/dts/CameraManager.d.ts +72 -0
- package/build/dts/Device.d.ts +53 -16
- package/build/dts/DeviceInformationManager.d.ts +4 -4
- package/build/dts/DeviceManager.d.ts +3 -0
- package/build/dts/FileTransferManager.d.ts +18 -8
- package/build/dts/InformationManager.d.ts +8 -5
- package/build/dts/TfliteManager.d.ts +22 -2
- package/build/dts/WifiManager.d.ts +61 -0
- package/build/dts/connection/BaseConnectionManager.d.ts +37 -3
- package/build/dts/connection/ClientConnectionManager.d.ts +11 -2
- package/build/dts/connection/bluetooth/BluetoothConnectionManager.d.ts +1 -0
- package/build/dts/connection/bluetooth/NobleConnectionManager.d.ts +3 -1
- package/build/dts/connection/bluetooth/WebBluetoothConnectionManager.d.ts +2 -0
- package/build/dts/connection/bluetooth/bluetoothUUIDs.d.ts +2 -2
- package/build/dts/connection/udp/UDPConnectionManager.d.ts +28 -0
- package/build/dts/connection/webSocket/WebSocketConnectionManager.d.ts +25 -0
- package/build/dts/devicePair/DevicePair.d.ts +14 -10
- package/build/dts/devicePair/DevicePairPressureSensorDataManager.d.ts +8 -4
- package/build/dts/devicePair/DevicePairSensorDataManager.d.ts +2 -2
- package/build/dts/scanner/BaseScanner.d.ts +4 -1
- package/build/dts/scanner/NobleScanner.d.ts +2 -1
- package/build/dts/sensor/MotionSensorDataManager.d.ts +5 -2
- package/build/dts/sensor/SensorDataManager.d.ts +5 -4
- package/build/dts/server/BaseClient.d.ts +6 -3
- package/build/dts/server/ServerUtils.d.ts +1 -1
- package/build/dts/server/websocket/WebSocketUtils.d.ts +1 -1
- package/build/dts/utils/CenterOfPressureHelper.d.ts +2 -2
- package/build/dts/utils/Console.d.ts +2 -0
- package/build/dts/utils/MathUtils.d.ts +2 -0
- package/build/dts/utils/ThrottleUtils.d.ts +2 -0
- package/build/dts/vibration/VibrationManager.d.ts +19 -2
- package/build/index.d.ts +299 -113
- package/build/index.node.d.ts +292 -110
- package/examples/3d/scene.html +19 -5
- package/examples/3d/script.js +90 -17
- package/examples/3d-generic/index.html +144 -0
- package/examples/3d-generic/script.js +266 -0
- package/examples/balance/script.js +2 -1
- package/examples/basic/index.html +232 -18
- package/examples/basic/script.js +746 -106
- package/examples/bottango/index.html +11 -1
- package/examples/bottango/script.js +2 -2
- package/examples/center-of-pressure/index.html +114 -114
- package/examples/center-of-pressure/script.js +1 -1
- package/examples/device-pair/index.html +58 -58
- package/examples/device-pair/script.js +12 -8
- package/examples/edge-impulse/script.js +135 -44
- package/examples/edge-impulse-test/README.md +11 -0
- package/examples/edge-impulse-test/edge-impulse-standalone.js +7228 -0
- package/examples/edge-impulse-test/edge-impulse-standalone.wasm +0 -0
- package/examples/edge-impulse-test/index.html +75 -0
- package/examples/edge-impulse-test/run-impulse.js +135 -0
- package/examples/edge-impulse-test/script.js +200 -0
- package/examples/gloves/edge-impulse-standalone.js +7228 -0
- package/examples/gloves/edge-impulse-standalone.wasm +0 -0
- package/examples/gloves/index.html +119 -0
- package/examples/gloves/run-impulse.js +135 -0
- package/examples/gloves/scene.html +124 -0
- package/examples/gloves/script.js +931 -0
- package/examples/graph/index.html +11 -1
- package/examples/graph/script.js +94 -37
- package/examples/pressure/index.html +180 -12
- package/examples/pressure/script.js +144 -7
- package/examples/punch/index.html +135 -0
- package/examples/punch/punch.tflite +0 -0
- package/examples/punch/script.js +169 -0
- package/examples/recording/index.html +191 -183
- package/examples/server/index.html +109 -23
- package/examples/server/script.js +322 -111
- package/examples/ukaton-firmware-update/index.html +20 -0
- package/examples/ukaton-firmware-update/manifest.json +11 -0
- package/examples/ukaton-firmware-update/merged-firmware.bin +0 -0
- package/examples/utils/aframe/aframe-master.min.js +2 -0
- package/examples/utils/aframe/bs-vibration.js +150 -0
- package/examples/utils/aframe/force-pushable.js +80 -0
- package/examples/utils/aframe/grabbable-anchor.js +46 -0
- package/examples/utils/aframe/grabbable-listener.js +31 -0
- package/examples/utils/aframe/grabbable-physics-body.js +190 -0
- package/examples/utils/aframe/grow-shrink.js +25 -0
- package/examples/utils/aframe/hand-punch.js +119 -0
- package/examples/utils/aframe/my-obb-collider.js +293 -0
- package/examples/utils/aframe/occlude-hand-tracking-controls.js +47 -0
- package/examples/utils/aframe/occlude-mesh.js +42 -0
- package/examples/utils/aframe/palm-up-detector.js +47 -0
- package/examples/utils/aframe/shadow-material.js +20 -0
- package/examples/utils/aframe/soft-shadow-light.js +9 -0
- package/examples/webxr/script.js +3 -3
- package/examples/webxr-2/assets/3d/soccerBall.glb +0 -0
- package/examples/webxr-2/assets/audio/shellBounce.wav +0 -0
- package/examples/webxr-2/assets/audio/shellHit.wav +0 -0
- package/examples/webxr-2/assets/audio/shellKick.wav +0 -0
- package/examples/webxr-2/assets/audio/soccerBounce.wav +0 -0
- package/examples/webxr-2/assets/audio/soccerKick.mp3 +0 -0
- package/examples/webxr-2/assets/images/shellTexture.png +0 -0
- package/examples/webxr-2/components/bs-ankle.js +337 -0
- package/examples/webxr-2/components/coin.js +84 -0
- package/examples/webxr-2/components/custom-wrap.js +17 -0
- package/examples/webxr-2/components/goomba.js +3250 -0
- package/examples/webxr-2/components/init-shell-material.js +215 -0
- package/examples/webxr-2/components/platter.js +172 -0
- package/examples/webxr-2/components/shell.js +374 -0
- package/examples/webxr-2/components/soccer-ball.js +250 -0
- package/examples/webxr-2/components/squashed-goomba.js +249 -0
- package/examples/webxr-2/edge-impulse-standalone.js +7228 -0
- package/examples/webxr-2/edge-impulse-standalone.wasm +0 -0
- package/examples/webxr-2/index.html +996 -0
- package/examples/webxr-2/kick.tflite +0 -0
- package/examples/webxr-2/kick2.tflite +0 -0
- package/examples/webxr-2/run-impulse.js +135 -0
- package/examples/webxr-2/script.js +384 -0
- package/package.json +2 -1
- package/src/.prettierrc +4 -0
- package/src/BS.ts +66 -9
- package/src/CameraManager.ts +499 -0
- package/src/Device.ts +620 -92
- package/src/DeviceInformationManager.ts +22 -11
- package/src/DeviceManager.ts +94 -25
- package/src/FileTransferManager.ts +146 -21
- package/src/FirmwareManager.ts +1 -1
- package/src/InformationManager.ts +62 -20
- package/src/TfliteManager.ts +172 -26
- package/src/WifiManager.ts +323 -0
- package/src/connection/BaseConnectionManager.ts +145 -30
- package/src/connection/ClientConnectionManager.ts +47 -11
- package/src/connection/bluetooth/BluetoothConnectionManager.ts +14 -3
- package/src/connection/bluetooth/NobleConnectionManager.ts +155 -42
- package/src/connection/bluetooth/WebBluetoothConnectionManager.ts +104 -35
- package/src/connection/bluetooth/bluetoothUUIDs.ts +40 -13
- package/src/connection/udp/UDPConnectionManager.ts +356 -0
- package/src/connection/websocket/WebSocketConnectionManager.ts +282 -0
- package/src/devicePair/DevicePair.ts +145 -49
- package/src/devicePair/DevicePairPressureSensorDataManager.ts +72 -24
- package/src/devicePair/DevicePairSensorDataManager.ts +5 -5
- package/src/scanner/BaseScanner.ts +49 -11
- package/src/scanner/NobleScanner.ts +81 -17
- package/src/sensor/BarometerSensorDataManager.ts +1 -1
- package/src/sensor/MotionSensorDataManager.ts +22 -7
- package/src/sensor/PressureSensorDataManager.ts +47 -13
- package/src/sensor/SensorConfigurationManager.ts +75 -24
- package/src/sensor/SensorDataManager.ts +107 -26
- package/src/server/BaseClient.ts +192 -37
- package/src/server/BaseServer.ts +201 -43
- package/src/server/ServerUtils.ts +39 -9
- package/src/server/udp/UDPServer.ts +74 -23
- package/src/server/udp/UDPUtils.ts +9 -2
- package/src/server/websocket/WebSocketClient.ts +30 -9
- package/src/server/websocket/WebSocketServer.ts +1 -1
- package/src/server/websocket/WebSocketUtils.ts +4 -2
- package/src/utils/CenterOfPressureHelper.ts +5 -5
- package/src/utils/Console.ts +62 -9
- package/src/utils/MathUtils.ts +31 -1
- package/src/utils/ParseUtils.ts +25 -6
- package/src/utils/ThrottleUtils.ts +62 -0
- package/src/utils/Timer.ts +1 -1
- package/src/utils/checksum.ts +1 -1
- package/src/utils/mcumgr.js +1 -1
- package/src/vibration/VibrationManager.ts +166 -40
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { createConsole } from "../../utils/Console.ts";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
isInNode,
|
|
4
|
+
isInBrowser,
|
|
5
|
+
isInBluefy,
|
|
6
|
+
isInWebBLE,
|
|
7
|
+
} from "../../utils/environment.ts";
|
|
8
|
+
import {
|
|
9
|
+
addEventListeners,
|
|
10
|
+
removeEventListeners,
|
|
11
|
+
} from "../../utils/EventUtils.ts";
|
|
4
12
|
import {
|
|
5
13
|
serviceUUIDs,
|
|
6
14
|
optionalServiceUUIDs,
|
|
@@ -9,10 +17,13 @@ import {
|
|
|
9
17
|
getCharacteristicProperties,
|
|
10
18
|
} from "./bluetoothUUIDs.ts";
|
|
11
19
|
import BluetoothConnectionManager from "./BluetoothConnectionManager.ts";
|
|
12
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
BluetoothCharacteristicName,
|
|
22
|
+
BluetoothServiceName,
|
|
23
|
+
} from "./bluetoothUUIDs.ts";
|
|
13
24
|
import { ConnectionType } from "../BaseConnectionManager.ts";
|
|
14
25
|
|
|
15
|
-
const _console = createConsole("WebBluetoothConnectionManager", { log:
|
|
26
|
+
const _console = createConsole("WebBluetoothConnectionManager", { log: false });
|
|
16
27
|
|
|
17
28
|
type WebBluetoothInterface = webbluetooth.Bluetooth | Bluetooth;
|
|
18
29
|
|
|
@@ -42,12 +53,19 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
42
53
|
return this.device!.id;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
get canUpdateFirmware() {
|
|
57
|
+
return this.#characteristics.has("smp");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#boundBluetoothCharacteristicEventListeners: {
|
|
61
|
+
[eventType: string]: EventListener;
|
|
62
|
+
} = {
|
|
46
63
|
characteristicvaluechanged: this.#onCharacteristicvaluechanged.bind(this),
|
|
47
64
|
};
|
|
48
|
-
#boundBluetoothDeviceEventListeners: { [eventType: string]: EventListener } =
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
#boundBluetoothDeviceEventListeners: { [eventType: string]: EventListener } =
|
|
66
|
+
{
|
|
67
|
+
gattserverdisconnected: this.#onGattserverdisconnected.bind(this),
|
|
68
|
+
};
|
|
51
69
|
|
|
52
70
|
static get isSupported() {
|
|
53
71
|
return Boolean(bluetooth);
|
|
@@ -56,7 +74,7 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
56
74
|
return "webBluetooth";
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
#device
|
|
77
|
+
#device?: BluetoothDevice;
|
|
60
78
|
get device() {
|
|
61
79
|
return this.#device;
|
|
62
80
|
}
|
|
@@ -66,7 +84,10 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
66
84
|
return;
|
|
67
85
|
}
|
|
68
86
|
if (this.#device) {
|
|
69
|
-
removeEventListeners(
|
|
87
|
+
removeEventListeners(
|
|
88
|
+
this.#device,
|
|
89
|
+
this.#boundBluetoothDeviceEventListeners
|
|
90
|
+
);
|
|
70
91
|
}
|
|
71
92
|
if (newDevice) {
|
|
72
93
|
addEventListeners(newDevice, this.#boundBluetoothDeviceEventListeners);
|
|
@@ -82,7 +103,8 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
#services: Map<BluetoothServiceName, BluetoothService> = new Map();
|
|
85
|
-
#characteristics: Map<BluetoothCharacteristicName, BluetoothCharacteristic> =
|
|
106
|
+
#characteristics: Map<BluetoothCharacteristicName, BluetoothCharacteristic> =
|
|
107
|
+
new Map();
|
|
86
108
|
|
|
87
109
|
async connect() {
|
|
88
110
|
await super.connect();
|
|
@@ -125,7 +147,10 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
125
147
|
const service = services[serviceIndex] as BluetoothService;
|
|
126
148
|
_console.log({ service });
|
|
127
149
|
const serviceName = getServiceNameFromUUID(service.uuid)!;
|
|
128
|
-
_console.assertWithError(
|
|
150
|
+
_console.assertWithError(
|
|
151
|
+
serviceName,
|
|
152
|
+
`no name found for service uuid "${service.uuid}"`
|
|
153
|
+
);
|
|
129
154
|
_console.log(`got "${serviceName}" service`);
|
|
130
155
|
service.name = serviceName;
|
|
131
156
|
this.#services.set(serviceName, service);
|
|
@@ -133,20 +158,33 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
133
158
|
const characteristics = await service.getCharacteristics();
|
|
134
159
|
_console.log(`got characteristics for "${serviceName}" service`);
|
|
135
160
|
for (const characteristicIndex in characteristics) {
|
|
136
|
-
const characteristic = characteristics[
|
|
161
|
+
const characteristic = characteristics[
|
|
162
|
+
characteristicIndex
|
|
163
|
+
] as BluetoothCharacteristic;
|
|
137
164
|
_console.log({ characteristic });
|
|
138
|
-
const characteristicName = getCharacteristicNameFromUUID(
|
|
165
|
+
const characteristicName = getCharacteristicNameFromUUID(
|
|
166
|
+
characteristic.uuid
|
|
167
|
+
)!;
|
|
139
168
|
_console.assertWithError(
|
|
140
169
|
Boolean(characteristicName),
|
|
141
170
|
`no name found for characteristic uuid "${characteristic.uuid}" in "${serviceName}" service`
|
|
142
171
|
);
|
|
143
|
-
_console.log(
|
|
172
|
+
_console.log(
|
|
173
|
+
`got "${characteristicName}" characteristic in "${serviceName}" service`
|
|
174
|
+
);
|
|
144
175
|
characteristic.name = characteristicName;
|
|
145
176
|
this.#characteristics.set(characteristicName, characteristic);
|
|
146
|
-
addEventListeners(
|
|
147
|
-
|
|
177
|
+
addEventListeners(
|
|
178
|
+
characteristic,
|
|
179
|
+
this.#boundBluetoothCharacteristicEventListeners
|
|
180
|
+
);
|
|
181
|
+
const characteristicProperties =
|
|
182
|
+
characteristic.properties ||
|
|
183
|
+
getCharacteristicProperties(characteristicName);
|
|
148
184
|
if (characteristicProperties.notify) {
|
|
149
|
-
_console.log(
|
|
185
|
+
_console.log(
|
|
186
|
+
`starting notifications for "${characteristicName}" characteristic`
|
|
187
|
+
);
|
|
150
188
|
await characteristic.startNotifications();
|
|
151
189
|
}
|
|
152
190
|
if (characteristicProperties.read) {
|
|
@@ -161,18 +199,30 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
161
199
|
}
|
|
162
200
|
async #removeEventListeners() {
|
|
163
201
|
if (this.device) {
|
|
164
|
-
removeEventListeners(
|
|
202
|
+
removeEventListeners(
|
|
203
|
+
this.device,
|
|
204
|
+
this.#boundBluetoothDeviceEventListeners
|
|
205
|
+
);
|
|
165
206
|
}
|
|
166
207
|
|
|
167
|
-
const promises = Array.from(this.#characteristics.keys()).map(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
208
|
+
const promises = Array.from(this.#characteristics.keys()).map(
|
|
209
|
+
(characteristicName) => {
|
|
210
|
+
const characteristic = this.#characteristics.get(characteristicName)!;
|
|
211
|
+
removeEventListeners(
|
|
212
|
+
characteristic,
|
|
213
|
+
this.#boundBluetoothCharacteristicEventListeners
|
|
214
|
+
);
|
|
215
|
+
const characteristicProperties =
|
|
216
|
+
characteristic.properties ||
|
|
217
|
+
getCharacteristicProperties(characteristicName);
|
|
218
|
+
if (characteristicProperties.notify) {
|
|
219
|
+
_console.log(
|
|
220
|
+
`stopping notifications for "${characteristicName}" characteristic`
|
|
221
|
+
);
|
|
222
|
+
return characteristic.stopNotifications();
|
|
223
|
+
}
|
|
174
224
|
}
|
|
175
|
-
|
|
225
|
+
);
|
|
176
226
|
|
|
177
227
|
return Promise.allSettled(promises);
|
|
178
228
|
}
|
|
@@ -199,10 +249,18 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
199
249
|
`no name found for characteristic with uuid "${characteristic.uuid}"`
|
|
200
250
|
);
|
|
201
251
|
|
|
202
|
-
_console.log(
|
|
252
|
+
_console.log(
|
|
253
|
+
`oncharacteristicvaluechanged for "${characteristicName}" characteristic`
|
|
254
|
+
);
|
|
203
255
|
const dataView = characteristic.value!;
|
|
204
|
-
_console.assertWithError(
|
|
205
|
-
|
|
256
|
+
_console.assertWithError(
|
|
257
|
+
dataView,
|
|
258
|
+
`no data found for "${characteristicName}" characteristic`
|
|
259
|
+
);
|
|
260
|
+
_console.log(
|
|
261
|
+
`data for "${characteristicName}" characteristic`,
|
|
262
|
+
Array.from(new Uint8Array(dataView.buffer))
|
|
263
|
+
);
|
|
206
264
|
|
|
207
265
|
try {
|
|
208
266
|
this.onCharacteristicValueChanged(characteristicName, dataView);
|
|
@@ -211,13 +269,21 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
211
269
|
}
|
|
212
270
|
}
|
|
213
271
|
|
|
214
|
-
async writeCharacteristic(
|
|
272
|
+
async writeCharacteristic(
|
|
273
|
+
characteristicName: BluetoothCharacteristicName,
|
|
274
|
+
data: ArrayBuffer
|
|
275
|
+
) {
|
|
215
276
|
super.writeCharacteristic(characteristicName, data);
|
|
216
277
|
|
|
217
278
|
const characteristic = this.#characteristics.get(characteristicName)!;
|
|
218
|
-
_console.assertWithError(
|
|
279
|
+
_console.assertWithError(
|
|
280
|
+
characteristic,
|
|
281
|
+
`${characteristicName} characteristic not found`
|
|
282
|
+
);
|
|
219
283
|
_console.log("writing characteristic", characteristic, data);
|
|
220
|
-
const characteristicProperties =
|
|
284
|
+
const characteristicProperties =
|
|
285
|
+
characteristic.properties ||
|
|
286
|
+
getCharacteristicProperties(characteristicName);
|
|
221
287
|
if (characteristicProperties.writeWithoutResponse) {
|
|
222
288
|
_console.log("writing without response");
|
|
223
289
|
await characteristic.writeValueWithoutResponse(data);
|
|
@@ -246,8 +312,6 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
246
312
|
}
|
|
247
313
|
async reconnect() {
|
|
248
314
|
await super.reconnect();
|
|
249
|
-
_console.log("attempting to reconnect...");
|
|
250
|
-
this.status = "connecting";
|
|
251
315
|
try {
|
|
252
316
|
await this.server!.connect();
|
|
253
317
|
} catch (error) {
|
|
@@ -264,6 +328,11 @@ class WebBluetoothConnectionManager extends BluetoothConnectionManager {
|
|
|
264
328
|
this.status = "notConnected";
|
|
265
329
|
}
|
|
266
330
|
}
|
|
331
|
+
|
|
332
|
+
remove() {
|
|
333
|
+
super.remove();
|
|
334
|
+
this.device = undefined;
|
|
335
|
+
}
|
|
267
336
|
}
|
|
268
337
|
|
|
269
338
|
export default WebBluetoothConnectionManager;
|
|
@@ -15,11 +15,16 @@ if (isInBrowser) {
|
|
|
15
15
|
|
|
16
16
|
function generateBluetoothUUID(value: string): BluetoothServiceUUID {
|
|
17
17
|
_console.assertTypeWithError(value, "string");
|
|
18
|
-
_console.assertWithError(
|
|
19
|
-
|
|
18
|
+
_console.assertWithError(
|
|
19
|
+
value.length == 4,
|
|
20
|
+
"value must be 4 characters long"
|
|
21
|
+
);
|
|
22
|
+
return `ea6d${value}-a725-4f9b-893d-c3913e33b39f`;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
function stringToCharacteristicUUID(
|
|
25
|
+
function stringToCharacteristicUUID(
|
|
26
|
+
identifier: string
|
|
27
|
+
): BluetoothCharacteristicUUID {
|
|
23
28
|
return BluetoothUUID?.getCharacteristic?.(identifier);
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -27,19 +32,32 @@ function stringToServiceUUID(identifier: string): BluetoothServiceUUID {
|
|
|
27
32
|
return BluetoothUUID?.getService?.(identifier);
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export type BluetoothServiceName =
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
export type BluetoothServiceName =
|
|
36
|
+
| "deviceInformation"
|
|
37
|
+
| "battery"
|
|
38
|
+
| "main"
|
|
39
|
+
| "smp";
|
|
40
|
+
import { DeviceInformationType } from "../../DeviceInformationManager.ts";
|
|
41
|
+
export type BluetoothCharacteristicName =
|
|
42
|
+
| DeviceInformationType
|
|
43
|
+
| "batteryLevel"
|
|
44
|
+
| "rx"
|
|
45
|
+
| "tx"
|
|
46
|
+
| "smp";
|
|
33
47
|
|
|
34
48
|
interface BluetoothCharacteristicInformation {
|
|
35
49
|
uuid: BluetoothCharacteristicUUID;
|
|
36
50
|
}
|
|
37
51
|
interface BluetoothServiceInformation {
|
|
38
52
|
uuid: BluetoothServiceUUID;
|
|
39
|
-
characteristics: {
|
|
53
|
+
characteristics: {
|
|
54
|
+
[characteristicName in BluetoothCharacteristicName]?: BluetoothCharacteristicInformation;
|
|
55
|
+
};
|
|
40
56
|
}
|
|
41
57
|
interface BluetoothServicesInformation {
|
|
42
|
-
services: {
|
|
58
|
+
services: {
|
|
59
|
+
[serviceName in BluetoothServiceName]: BluetoothServiceInformation;
|
|
60
|
+
};
|
|
43
61
|
}
|
|
44
62
|
const bluetoothUUIDs: BluetoothServicesInformation = Object.freeze({
|
|
45
63
|
services: {
|
|
@@ -101,9 +119,13 @@ export const optionalServiceUUIDs = [
|
|
|
101
119
|
];
|
|
102
120
|
export const allServiceUUIDs = [...serviceUUIDs, ...optionalServiceUUIDs];
|
|
103
121
|
|
|
104
|
-
export function getServiceNameFromUUID(
|
|
122
|
+
export function getServiceNameFromUUID(
|
|
123
|
+
serviceUUID: BluetoothServiceUUID
|
|
124
|
+
): BluetoothServiceName | undefined {
|
|
105
125
|
serviceUUID = serviceUUID.toString().toLowerCase();
|
|
106
|
-
const serviceNames = Object.keys(
|
|
126
|
+
const serviceNames = Object.keys(
|
|
127
|
+
bluetoothUUIDs.services
|
|
128
|
+
) as BluetoothServiceName[];
|
|
107
129
|
return serviceNames.find((serviceName) => {
|
|
108
130
|
const serviceInfo = bluetoothUUIDs.services[serviceName];
|
|
109
131
|
let serviceInfoUUID = serviceInfo.uuid.toString();
|
|
@@ -127,7 +149,9 @@ Object.values(bluetoothUUIDs.services).forEach((serviceInfo) => {
|
|
|
127
149
|
if (!serviceInfo.characteristics) {
|
|
128
150
|
return;
|
|
129
151
|
}
|
|
130
|
-
const characteristicNames = Object.keys(
|
|
152
|
+
const characteristicNames = Object.keys(
|
|
153
|
+
serviceInfo.characteristics
|
|
154
|
+
) as BluetoothCharacteristicName[];
|
|
131
155
|
characteristicNames.forEach((characteristicName) => {
|
|
132
156
|
const characteristicInfo = serviceInfo.characteristics[characteristicName]!;
|
|
133
157
|
if (serviceUUIDs.includes(serviceInfo.uuid)) {
|
|
@@ -148,9 +172,12 @@ export function getCharacteristicNameFromUUID(
|
|
|
148
172
|
characteristicUUID = characteristicUUID.toString().toLowerCase();
|
|
149
173
|
var characteristicName: BluetoothCharacteristicName | undefined;
|
|
150
174
|
Object.values(bluetoothUUIDs.services).some((serviceInfo) => {
|
|
151
|
-
const characteristicNames = Object.keys(
|
|
175
|
+
const characteristicNames = Object.keys(
|
|
176
|
+
serviceInfo.characteristics
|
|
177
|
+
) as BluetoothCharacteristicName[];
|
|
152
178
|
characteristicName = characteristicNames.find((_characteristicName) => {
|
|
153
|
-
const characteristicInfo =
|
|
179
|
+
const characteristicInfo =
|
|
180
|
+
serviceInfo.characteristics[_characteristicName]!;
|
|
154
181
|
let characteristicInfoUUID = characteristicInfo.uuid.toString();
|
|
155
182
|
if (characteristicUUID.length == 4) {
|
|
156
183
|
characteristicInfoUUID = characteristicInfoUUID.slice(4, 8);
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { DeviceInformationTypes } from "../../DeviceInformationManager.ts";
|
|
2
|
+
import {
|
|
3
|
+
createMessage,
|
|
4
|
+
Message,
|
|
5
|
+
MessageLike,
|
|
6
|
+
} from "../../server/ServerUtils.ts";
|
|
7
|
+
import { createConsole } from "../../utils/Console.ts";
|
|
8
|
+
import { isInNode } from "../../utils/environment.ts";
|
|
9
|
+
import {
|
|
10
|
+
addEventListeners,
|
|
11
|
+
removeEventListeners,
|
|
12
|
+
} from "../../utils/EventUtils.ts";
|
|
13
|
+
import { parseMessage } from "../../utils/ParseUtils.ts";
|
|
14
|
+
import Timer from "../../utils/Timer.ts";
|
|
15
|
+
import BaseConnectionManager, {
|
|
16
|
+
ConnectionType,
|
|
17
|
+
} from "../BaseConnectionManager.ts";
|
|
18
|
+
|
|
19
|
+
import * as dgram from "dgram";
|
|
20
|
+
|
|
21
|
+
const _console = createConsole("UDPConnectionManager", { log: false });
|
|
22
|
+
|
|
23
|
+
export const UDPSendPort = 3000;
|
|
24
|
+
|
|
25
|
+
export const UDPPingInterval = 2_000;
|
|
26
|
+
|
|
27
|
+
const SocketMessageTypes = [
|
|
28
|
+
"ping",
|
|
29
|
+
"pong",
|
|
30
|
+
"setRemoteReceivePort",
|
|
31
|
+
"batteryLevel",
|
|
32
|
+
"deviceInformation",
|
|
33
|
+
"message",
|
|
34
|
+
] as const;
|
|
35
|
+
type SocketMessageType = (typeof SocketMessageTypes)[number];
|
|
36
|
+
|
|
37
|
+
type SocketMessage = SocketMessageType | Message<SocketMessageType>;
|
|
38
|
+
function createSocketMessage(...messages: SocketMessage[]) {
|
|
39
|
+
_console.log("createSocketMessage", ...messages);
|
|
40
|
+
return createMessage(SocketMessageTypes, ...messages);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SocketDeviceInformationMessageTypes: SocketMessageType[] = [
|
|
44
|
+
"deviceInformation",
|
|
45
|
+
"batteryLevel",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
class UDPConnectionManager extends BaseConnectionManager {
|
|
49
|
+
#bluetoothId?: string;
|
|
50
|
+
get bluetoothId() {
|
|
51
|
+
return this.#bluetoothId ?? "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
defaultMtu = 2 ** 10;
|
|
55
|
+
|
|
56
|
+
constructor(ipAddress: string, bluetoothId?: string, receivePort?: number) {
|
|
57
|
+
super();
|
|
58
|
+
this.ipAddress = ipAddress;
|
|
59
|
+
this.mtu = this.defaultMtu;
|
|
60
|
+
this.#bluetoothId = bluetoothId;
|
|
61
|
+
if (receivePort) {
|
|
62
|
+
this.receivePort = receivePort;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get isAvailable() {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
static get isSupported() {
|
|
70
|
+
return isInNode;
|
|
71
|
+
}
|
|
72
|
+
static get type(): ConnectionType {
|
|
73
|
+
return "udp";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// IP ADDRESS
|
|
77
|
+
#ipAddress!: string;
|
|
78
|
+
get ipAddress() {
|
|
79
|
+
return this.#ipAddress;
|
|
80
|
+
}
|
|
81
|
+
set ipAddress(newIpAddress) {
|
|
82
|
+
this.assertIsNotConnected();
|
|
83
|
+
if (this.#ipAddress == newIpAddress) {
|
|
84
|
+
_console.log(`redundnant ipAddress assignment "${newIpAddress}"`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.#ipAddress = newIpAddress;
|
|
88
|
+
_console.log(`updated ipAddress to "${this.ipAddress}"`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// RECEIVE PORT
|
|
92
|
+
#receivePort?: number;
|
|
93
|
+
get receivePort() {
|
|
94
|
+
return this.#receivePort;
|
|
95
|
+
}
|
|
96
|
+
set receivePort(newReceivePort) {
|
|
97
|
+
this.assertIsNotConnected();
|
|
98
|
+
if (this.#receivePort == newReceivePort) {
|
|
99
|
+
_console.log(`redundnant receivePort assignment ${newReceivePort}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.#receivePort = newReceivePort;
|
|
103
|
+
_console.log(`updated receivePort to ${this.#receivePort}`);
|
|
104
|
+
if (this.#receivePort) {
|
|
105
|
+
this.#setRemoteReceivePortDataView.setUint16(0, this.#receivePort, true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// SET REMOTE RECEIVE PORT
|
|
110
|
+
#didSetRemoteReceivePort = false;
|
|
111
|
+
#setRemoteReceivePortDataView = new DataView(new ArrayBuffer(2));
|
|
112
|
+
#parseReceivePort(dataView: DataView) {
|
|
113
|
+
const parsedReceivePort = dataView.getUint16(0, true);
|
|
114
|
+
if (parsedReceivePort != this.receivePort) {
|
|
115
|
+
_console.error(
|
|
116
|
+
`incorrect receivePort (expected ${this.receivePort}, got ${parsedReceivePort})`
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.#didSetRemoteReceivePort = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// SOCKET
|
|
124
|
+
#socket?: dgram.Socket;
|
|
125
|
+
get socket() {
|
|
126
|
+
return this.#socket;
|
|
127
|
+
}
|
|
128
|
+
set socket(newSocket) {
|
|
129
|
+
if (this.#socket == newSocket) {
|
|
130
|
+
_console.log("redundant socket assignment");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_console.log("assigning socket", newSocket);
|
|
135
|
+
|
|
136
|
+
if (this.#socket) {
|
|
137
|
+
_console.log("removing existing socket...");
|
|
138
|
+
removeEventListeners(this.#socket, this.#boundSocketEventListeners);
|
|
139
|
+
try {
|
|
140
|
+
this.#socket.close();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
_console.error(error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (newSocket) {
|
|
147
|
+
addEventListeners(newSocket, this.#boundSocketEventListeners);
|
|
148
|
+
}
|
|
149
|
+
this.#socket = newSocket;
|
|
150
|
+
|
|
151
|
+
_console.log("assigned socket");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// SOCKET MESSAGING
|
|
155
|
+
#sendMessage(message: MessageLike) {
|
|
156
|
+
// this.assertIsConnected();
|
|
157
|
+
_console.log("sending socket message", message);
|
|
158
|
+
const dataView = Buffer.from(message);
|
|
159
|
+
this.#socket!.send(dataView);
|
|
160
|
+
this.#pingTimer.restart();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#sendSocketMessage(...messages: SocketMessage[]) {
|
|
164
|
+
this.#sendMessage(createSocketMessage(...messages));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// BASE CONNECTION MANAGER
|
|
168
|
+
async sendSmpMessage(data: ArrayBuffer) {
|
|
169
|
+
super.sendSmpMessage(data);
|
|
170
|
+
_console.error("smp not supported on udp");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async sendTxData(data: ArrayBuffer) {
|
|
174
|
+
super.sendTxData(data);
|
|
175
|
+
if (data.byteLength == 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.#sendSocketMessage({ type: "message", data });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// SOCKET EVENTS
|
|
182
|
+
#boundSocketEventListeners: { [eventType: string]: Function } = {
|
|
183
|
+
close: this.#onSocketClose.bind(this),
|
|
184
|
+
connect: this.#onSocketConnect.bind(this),
|
|
185
|
+
error: this.#onSocketError.bind(this),
|
|
186
|
+
listening: this.#onSocketListening.bind(this),
|
|
187
|
+
message: this.#onSocketMessage.bind(this),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
#onSocketClose() {
|
|
191
|
+
_console.log("socket.close");
|
|
192
|
+
this.status = "notConnected";
|
|
193
|
+
this.clear();
|
|
194
|
+
}
|
|
195
|
+
#onSocketConnect() {
|
|
196
|
+
_console.log("socket.connect");
|
|
197
|
+
this.#pingTimer.start(true);
|
|
198
|
+
}
|
|
199
|
+
#onSocketError(error: Error) {
|
|
200
|
+
_console.error("socket.error", error);
|
|
201
|
+
}
|
|
202
|
+
#onSocketListening() {
|
|
203
|
+
const address = this.socket!.address();
|
|
204
|
+
_console.log(`socket.listening on ${address.address}:${address.port}`);
|
|
205
|
+
this.receivePort = address.port;
|
|
206
|
+
this.socket!.connect(UDPSendPort, this.ipAddress);
|
|
207
|
+
}
|
|
208
|
+
#onSocketMessage(message: Buffer, remoteInfo: dgram.RemoteInfo) {
|
|
209
|
+
this.#pongTimeoutTimer.stop();
|
|
210
|
+
_console.log("socket.message", message.byteLength, remoteInfo);
|
|
211
|
+
const arrayBuffer = message.buffer.slice(
|
|
212
|
+
message.byteOffset,
|
|
213
|
+
message.byteOffset + message.byteLength
|
|
214
|
+
);
|
|
215
|
+
const dataView = new DataView(arrayBuffer);
|
|
216
|
+
this.#parseSocketMessage(dataView);
|
|
217
|
+
|
|
218
|
+
if (this.status == "connecting" && this.#didSetRemoteReceivePort) {
|
|
219
|
+
this.status = "connected";
|
|
220
|
+
this.#requestDeviceInformation();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#setupSocket() {
|
|
225
|
+
this.#didSetRemoteReceivePort = false;
|
|
226
|
+
this.socket = dgram.createSocket({
|
|
227
|
+
type: "udp4",
|
|
228
|
+
});
|
|
229
|
+
try {
|
|
230
|
+
if (this.receivePort) {
|
|
231
|
+
this.socket.bind(this.receivePort);
|
|
232
|
+
} else {
|
|
233
|
+
this.socket.bind();
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
_console.error(error);
|
|
237
|
+
this.disconnect();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// CONNECTION
|
|
242
|
+
async connect() {
|
|
243
|
+
await super.connect();
|
|
244
|
+
this.#setupSocket();
|
|
245
|
+
}
|
|
246
|
+
async disconnect() {
|
|
247
|
+
await super.disconnect();
|
|
248
|
+
_console.log("closing socket");
|
|
249
|
+
try {
|
|
250
|
+
this.#socket?.close();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
_console.error(error);
|
|
253
|
+
}
|
|
254
|
+
this.#pingTimer.stop();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
get canReconnect() {
|
|
258
|
+
return Boolean(this.socket);
|
|
259
|
+
}
|
|
260
|
+
async reconnect() {
|
|
261
|
+
await super.reconnect();
|
|
262
|
+
this.#setupSocket();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// PARSING
|
|
266
|
+
#parseSocketMessage(dataView: DataView) {
|
|
267
|
+
parseMessage(
|
|
268
|
+
dataView,
|
|
269
|
+
SocketMessageTypes,
|
|
270
|
+
this.#onMessage.bind(this),
|
|
271
|
+
null,
|
|
272
|
+
true
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#onMessage(messageType: SocketMessageType, dataView: DataView) {
|
|
277
|
+
_console.log(
|
|
278
|
+
`received "${messageType}" message (${dataView.byteLength} bytes)`
|
|
279
|
+
);
|
|
280
|
+
switch (messageType) {
|
|
281
|
+
case "ping":
|
|
282
|
+
this.#pong();
|
|
283
|
+
break;
|
|
284
|
+
case "pong":
|
|
285
|
+
break;
|
|
286
|
+
case "setRemoteReceivePort":
|
|
287
|
+
this.#parseReceivePort(dataView);
|
|
288
|
+
break;
|
|
289
|
+
case "batteryLevel":
|
|
290
|
+
this.onMessageReceived?.("batteryLevel", dataView);
|
|
291
|
+
break;
|
|
292
|
+
case "deviceInformation":
|
|
293
|
+
parseMessage(
|
|
294
|
+
dataView,
|
|
295
|
+
DeviceInformationTypes,
|
|
296
|
+
(deviceInformationType, dataView) => {
|
|
297
|
+
this.onMessageReceived!(deviceInformationType, dataView);
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
break;
|
|
301
|
+
case "message":
|
|
302
|
+
this.parseRxMessage(dataView);
|
|
303
|
+
break;
|
|
304
|
+
default:
|
|
305
|
+
_console.error(`uncaught messageType "${messageType}"`);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// PING
|
|
311
|
+
#pingTimer = new Timer(this.#ping.bind(this), UDPPingInterval);
|
|
312
|
+
#ping() {
|
|
313
|
+
_console.log("pinging");
|
|
314
|
+
if (this.#didSetRemoteReceivePort || !this.#receivePort) {
|
|
315
|
+
this.#sendSocketMessage("ping");
|
|
316
|
+
} else {
|
|
317
|
+
this.#sendSocketMessage({
|
|
318
|
+
type: "setRemoteReceivePort",
|
|
319
|
+
data: this.#setRemoteReceivePortDataView,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (this.isConnected) {
|
|
323
|
+
this.#pongTimeoutTimer.start();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
#pong() {
|
|
327
|
+
_console.log("ponging");
|
|
328
|
+
this.#sendSocketMessage("pong");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#pongTimeout() {
|
|
332
|
+
this.#pongTimeoutTimer.stop();
|
|
333
|
+
_console.log("pong timeout");
|
|
334
|
+
this.disconnect();
|
|
335
|
+
}
|
|
336
|
+
#pongTimeoutTimer = new Timer(() => this.#pongTimeout(), 1_000);
|
|
337
|
+
|
|
338
|
+
// DEVICE INFORMATION
|
|
339
|
+
#requestDeviceInformation() {
|
|
340
|
+
this.#sendSocketMessage(...SocketDeviceInformationMessageTypes);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
clear() {
|
|
344
|
+
super.clear();
|
|
345
|
+
this.#didSetRemoteReceivePort = false;
|
|
346
|
+
this.#pingTimer.stop();
|
|
347
|
+
this.#pongTimeoutTimer.stop();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
remove() {
|
|
351
|
+
super.remove();
|
|
352
|
+
this.socket = undefined;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export default UDPConnectionManager;
|