sesame-kit 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/LICENSE +26 -0
- package/LICENSE.biz3 +21 -0
- package/README.ja.md +225 -0
- package/README.md +222 -0
- package/bin/sesame.js +8 -0
- package/clients/js/sesame-client.mjs +208 -0
- package/clients/python/pyproject.toml +5 -0
- package/clients/python/sesame_client.py +323 -0
- package/clients/python/setup.cfg +11 -0
- package/docs/architecture.ja.md +132 -0
- package/docs/architecture.md +105 -0
- package/docs/commands.ja.md +316 -0
- package/docs/commands.md +308 -0
- package/docs/library.ja.md +152 -0
- package/docs/library.md +152 -0
- package/docs/migration.ja.md +13 -0
- package/docs/migration.md +13 -0
- package/package.json +114 -0
- package/src/access.js +375 -0
- package/src/account.js +36 -0
- package/src/auth.js +248 -0
- package/src/ble/devicemodel.js +164 -0
- package/src/ble/index.js +185 -0
- package/src/ble/protocol.js +319 -0
- package/src/ble/session.js +235 -0
- package/src/ble/transport.js +279 -0
- package/src/cli/access.js +373 -0
- package/src/cli/company.js +104 -0
- package/src/cli/iot.js +400 -0
- package/src/cli/org.js +788 -0
- package/src/cli/presetir.js +188 -0
- package/src/cli/schedule.js +83 -0
- package/src/cli/serve.js +308 -0
- package/src/cli.js +1815 -0
- package/src/client.js +957 -0
- package/src/company.js +147 -0
- package/src/config.js +575 -0
- package/src/crypto.js +162 -0
- package/src/devices.js +228 -0
- package/src/index.js +55 -0
- package/src/iot.js +513 -0
- package/src/ir.js +341 -0
- package/src/itemcodes.js +29 -0
- package/src/lock.js +194 -0
- package/src/org.js +803 -0
- package/src/paths.js +30 -0
- package/src/presetir.js +525 -0
- package/src/prompts.js +74 -0
- package/src/schedule.js +108 -0
- package/src/serve/daemon.js +251 -0
- package/src/serve/framing/grpc.js +145 -0
- package/src/serve/framing/http.js +144 -0
- package/src/serve/framing/ndjson.js +75 -0
- package/src/serve/framing/socket.js +73 -0
- package/src/serve/framing/stdio.js +28 -0
- package/src/serve/framing/token.js +36 -0
- package/src/serve/framing/ws.js +56 -0
- package/src/serve/grpc-methods.generated.json +378 -0
- package/src/serve/jsonrpc.js +164 -0
- package/src/serve/registry.js +226 -0
- package/src/serve/rpc-params.generated.json +1746 -0
- package/src/serve/sesame.proto +470 -0
- package/src/session-ui.js +181 -0
- package/src/sharekey.js +130 -0
- package/src/tokens.js +53 -0
- package/src/transport.js +634 -0
- package/src/util.js +26 -0
- package/types/access.d.ts +193 -0
- package/types/access.d.ts.map +1 -0
- package/types/account.d.ts +13 -0
- package/types/account.d.ts.map +1 -0
- package/types/auth.d.ts +80 -0
- package/types/auth.d.ts.map +1 -0
- package/types/ble/devicemodel.d.ts +212 -0
- package/types/ble/devicemodel.d.ts.map +1 -0
- package/types/ble/index.d.ts +160 -0
- package/types/ble/index.d.ts.map +1 -0
- package/types/ble/protocol.d.ts +201 -0
- package/types/ble/protocol.d.ts.map +1 -0
- package/types/ble/session.d.ts +129 -0
- package/types/ble/session.d.ts.map +1 -0
- package/types/ble/transport.d.ts +67 -0
- package/types/ble/transport.d.ts.map +1 -0
- package/types/cli/access.d.ts +6 -0
- package/types/cli/access.d.ts.map +1 -0
- package/types/cli/company.d.ts +6 -0
- package/types/cli/company.d.ts.map +1 -0
- package/types/cli/iot.d.ts +6 -0
- package/types/cli/iot.d.ts.map +1 -0
- package/types/cli/org.d.ts +6 -0
- package/types/cli/org.d.ts.map +1 -0
- package/types/cli/presetir.d.ts +6 -0
- package/types/cli/presetir.d.ts.map +1 -0
- package/types/cli/schedule.d.ts +6 -0
- package/types/cli/schedule.d.ts.map +1 -0
- package/types/cli/serve.d.ts +2 -0
- package/types/cli/serve.d.ts.map +1 -0
- package/types/cli.d.ts +2 -0
- package/types/cli.d.ts.map +1 -0
- package/types/client.d.ts +463 -0
- package/types/client.d.ts.map +1 -0
- package/types/company.d.ts +94 -0
- package/types/company.d.ts.map +1 -0
- package/types/config.d.ts +111 -0
- package/types/config.d.ts.map +1 -0
- package/types/crypto.d.ts +61 -0
- package/types/crypto.d.ts.map +1 -0
- package/types/devices.d.ts +116 -0
- package/types/devices.d.ts.map +1 -0
- package/types/index.d.ts +23 -0
- package/types/index.d.ts.map +1 -0
- package/types/iot.d.ts +312 -0
- package/types/iot.d.ts.map +1 -0
- package/types/ir.d.ts +147 -0
- package/types/ir.d.ts.map +1 -0
- package/types/itemcodes.d.ts +21 -0
- package/types/itemcodes.d.ts.map +1 -0
- package/types/lock.d.ts +89 -0
- package/types/lock.d.ts.map +1 -0
- package/types/org.d.ts +468 -0
- package/types/org.d.ts.map +1 -0
- package/types/paths.d.ts +10 -0
- package/types/paths.d.ts.map +1 -0
- package/types/presetir.d.ts +286 -0
- package/types/presetir.d.ts.map +1 -0
- package/types/prompts.d.ts +39 -0
- package/types/prompts.d.ts.map +1 -0
- package/types/schedule.d.ts +71 -0
- package/types/schedule.d.ts.map +1 -0
- package/types/serve/daemon.d.ts +133 -0
- package/types/serve/daemon.d.ts.map +1 -0
- package/types/serve/framing/grpc.d.ts +14 -0
- package/types/serve/framing/grpc.d.ts.map +1 -0
- package/types/serve/framing/http.d.ts +14 -0
- package/types/serve/framing/http.d.ts.map +1 -0
- package/types/serve/framing/ndjson.d.ts +19 -0
- package/types/serve/framing/ndjson.d.ts.map +1 -0
- package/types/serve/framing/socket.d.ts +14 -0
- package/types/serve/framing/socket.d.ts.map +1 -0
- package/types/serve/framing/stdio.d.ts +11 -0
- package/types/serve/framing/stdio.d.ts.map +1 -0
- package/types/serve/framing/token.d.ts +11 -0
- package/types/serve/framing/token.d.ts.map +1 -0
- package/types/serve/framing/ws.d.ts +13 -0
- package/types/serve/framing/ws.d.ts.map +1 -0
- package/types/serve/jsonrpc.d.ts +118 -0
- package/types/serve/jsonrpc.d.ts.map +1 -0
- package/types/serve/registry.d.ts +41 -0
- package/types/serve/registry.d.ts.map +1 -0
- package/types/session-ui.d.ts +36 -0
- package/types/session-ui.d.ts.map +1 -0
- package/types/sharekey.d.ts +35 -0
- package/types/sharekey.d.ts.map +1 -0
- package/types/tokens.d.ts +20 -0
- package/types/tokens.d.ts.map +1 -0
- package/types/transport.d.ts +138 -0
- package/types/transport.d.ts.map +1 -0
- package/types/util.d.ts +20 -0
- package/types/util.d.ts.map +1 -0
- package/vendor/biz3/README.md +37 -0
- package/vendor/biz3/constants/cmdCode.d.ts +48 -0
- package/vendor/biz3/constants/cmdCode.d.ts.map +1 -0
- package/vendor/biz3/constants/cmdCode.js +92 -0
- package/vendor/biz3/constants/messageConstants.d.ts +28 -0
- package/vendor/biz3/constants/messageConstants.d.ts.map +1 -0
- package/vendor/biz3/constants/messageConstants.js +30 -0
- package/vendor/biz3/constants/sesameDeviceModel.d.ts +75 -0
- package/vendor/biz3/constants/sesameDeviceModel.d.ts.map +1 -0
- package/vendor/biz3/constants/sesameDeviceModel.js +77 -0
- package/vendor/biz3/package.json +5 -0
package/src/auth.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Cognito 認証。
|
|
2
|
+
//
|
|
3
|
+
// Ported from biz3 (CANDY-HOUSE/biz3, MIT):
|
|
4
|
+
// - vendor reference: references_web/src/api/useAuthState.js (AWS Amplify Auth.signIn / Auth.signUp ベース)
|
|
5
|
+
// - vendor reference: references_web/src/aws-exports.js (region / UserPool / Client ID)
|
|
6
|
+
//
|
|
7
|
+
// Amplify はブラウザ前提 (localStorage / IndexedDB) のため Node では使えず、
|
|
8
|
+
// @aws-sdk/client-cognito-identity-provider 直叩きに置換している。
|
|
9
|
+
// 振る舞いは biz3 と同じ:
|
|
10
|
+
// - User Pool: ap-northeast-1_bY2byhlCa (biz / consumer 共有)
|
|
11
|
+
// - CUSTOM_AUTH passwordless: USERNAME → CUSTOM_CHALLENGE (email にコード) → コード回答
|
|
12
|
+
// - 新規ユーザーは dummy password "Aa123456" で SignUp してから sign-in (useAuthState.js:109-122)
|
|
13
|
+
//
|
|
14
|
+
// biz3 との唯一の機能的相違: Client ID を biz3 の `21u50hboia4s5q0sbk6pbdfmss` から、
|
|
15
|
+
// 公式 iOS/Android/chat.candyhouse.co と同じ Consumer Client `6ialca0p8u0lsgvbmvsljfm305` に
|
|
16
|
+
// 差し替え。これで refreshToken が事実上失効しなくなる。
|
|
17
|
+
//
|
|
18
|
+
// 状態は TokenStore (load/save/clear + loadPending/savePending/clearPending) に永続化を委譲。
|
|
19
|
+
// CLI からは FileTokenStore、ライブラリ消費者は独自実装を渡せる。
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
CognitoIdentityProviderClient,
|
|
23
|
+
InitiateAuthCommand,
|
|
24
|
+
RespondToAuthChallengeCommand,
|
|
25
|
+
SignUpCommand,
|
|
26
|
+
} from "@aws-sdk/client-cognito-identity-provider";
|
|
27
|
+
|
|
28
|
+
const COGNITO_REGION = "ap-northeast-1";
|
|
29
|
+
const USER_POOL_ID = "ap-northeast-1_bY2byhlCa";
|
|
30
|
+
// 公式アプリ (iOS/Android Sesame, chat.candyhouse.co) と同じ client。
|
|
31
|
+
// biz3 aws-exports.js:5 の userPoolWebClientId と一致 (一次資料で確認済み)。
|
|
32
|
+
export const CONSUMER_CLIENT_ID = "6ialca0p8u0lsgvbmvsljfm305";
|
|
33
|
+
// 注 (出所未確認): biz.candyhouse.co の管理 web client とされる値だが、
|
|
34
|
+
// biz3 の現行ソース (references_web) には 1 件も現れない (旧実装由来の値)。
|
|
35
|
+
// デフォルトでは使われず export のみ。biz3 web 自体も aws-exports の consumer client を使う。
|
|
36
|
+
export const BIZ_CLIENT_ID = "21u50hboia4s5q0sbk6pbdfmss";
|
|
37
|
+
// デフォルトは consumer (公式アプリと同じ寿命)
|
|
38
|
+
const DEFAULT_CLIENT_ID = CONSUMER_CLIENT_ID;
|
|
39
|
+
// 公式が新規 sign-up 時に使う ダミーパスワード (Cognito policy 通過用)
|
|
40
|
+
const DUMMY_PASSWORD = "Aa123456";
|
|
41
|
+
|
|
42
|
+
const cognito = new CognitoIdentityProviderClient({ region: COGNITO_REGION });
|
|
43
|
+
|
|
44
|
+
/** JWT を decode して exp を返す (秒、UNIX時間)。失敗時は 0。 */
|
|
45
|
+
function jwtExp(token) {
|
|
46
|
+
try {
|
|
47
|
+
const payload = token.split(".")[1];
|
|
48
|
+
const json = Buffer.from(payload, "base64").toString("utf8");
|
|
49
|
+
return JSON.parse(json).exp || 0;
|
|
50
|
+
} catch {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** idToken の aud claim (= clientId) を返す。 */
|
|
56
|
+
function jwtAud(token) {
|
|
57
|
+
try {
|
|
58
|
+
const payload = token.split(".")[1];
|
|
59
|
+
const json = Buffer.from(payload, "base64").toString("utf8");
|
|
60
|
+
return JSON.parse(json).aud || null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* idToken の `sub` claim (= Cognito user UUID) を返す。
|
|
68
|
+
* biz3 が `gStripe.customerInfo.subUUID` として使っている値と同じで、
|
|
69
|
+
* `biz3TriggerLocker` の `history` フィールドに乗せる必要がある。
|
|
70
|
+
*/
|
|
71
|
+
export function jwtSub(token) {
|
|
72
|
+
try {
|
|
73
|
+
const payload = token.split(".")[1];
|
|
74
|
+
const json = Buffer.from(payload, "base64").toString("utf8");
|
|
75
|
+
return JSON.parse(json).sub || null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 失効していない idToken を返す。必要なら refresh する。
|
|
83
|
+
* 失効まで `marginSec` 以下なら早期 refresh する (デフォルト 60秒)。
|
|
84
|
+
*
|
|
85
|
+
* @param {{load:Function, save:Function}} store
|
|
86
|
+
*/
|
|
87
|
+
export async function getValidIdToken(store, { marginSec = 60 } = {}) {
|
|
88
|
+
const t = store.load();
|
|
89
|
+
if (!t) {
|
|
90
|
+
throw new Error("No tokens stored. `sesame login <email>` で sign-in してください。");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const now = Math.floor(Date.now() / 1000);
|
|
94
|
+
const exp = jwtExp(t.idToken);
|
|
95
|
+
if (t.idToken && exp - now > marginSec) {
|
|
96
|
+
return t.idToken;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!t.refreshToken) {
|
|
100
|
+
throw new Error("idToken expired and no refreshToken. Re-run login.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const clientId = t.clientId || DEFAULT_CLIENT_ID;
|
|
104
|
+
const authParameters = { REFRESH_TOKEN: t.refreshToken };
|
|
105
|
+
if (t.deviceKey) authParameters.DEVICE_KEY = t.deviceKey;
|
|
106
|
+
|
|
107
|
+
const resp = await cognito.send(
|
|
108
|
+
new InitiateAuthCommand({
|
|
109
|
+
AuthFlow: "REFRESH_TOKEN_AUTH",
|
|
110
|
+
ClientId: clientId,
|
|
111
|
+
AuthParameters: authParameters,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const r = resp.AuthenticationResult;
|
|
116
|
+
if (!r?.IdToken) {
|
|
117
|
+
throw new Error(`Cognito refresh returned no IdToken: ${JSON.stringify(resp)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
t.idToken = r.IdToken;
|
|
121
|
+
if (r.AccessToken) t.accessToken = r.AccessToken;
|
|
122
|
+
if (r.RefreshToken) t.refreshToken = r.RefreshToken; // rotation 対応
|
|
123
|
+
t.lastRefresh = new Date().toISOString();
|
|
124
|
+
store.save(t);
|
|
125
|
+
return t.idToken;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Step 1: CUSTOM_AUTH を開始。Cognito が email に確認コードを送る。
|
|
130
|
+
* 新規ユーザーの場合は SignUp してから retry。
|
|
131
|
+
*
|
|
132
|
+
* @param {{savePending:Function}} store
|
|
133
|
+
*/
|
|
134
|
+
export async function loginInitiate(store, username, { clientId = DEFAULT_CLIENT_ID } = {}) {
|
|
135
|
+
const initiate = () =>
|
|
136
|
+
cognito.send(
|
|
137
|
+
new InitiateAuthCommand({
|
|
138
|
+
AuthFlow: "CUSTOM_AUTH",
|
|
139
|
+
ClientId: clientId,
|
|
140
|
+
AuthParameters: { USERNAME: username },
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
let resp;
|
|
145
|
+
try {
|
|
146
|
+
resp = await initiate();
|
|
147
|
+
} catch (e) {
|
|
148
|
+
if (e.name === "UserNotFoundException") {
|
|
149
|
+
// 公式アプリと同じ自動 sign-up
|
|
150
|
+
await cognito.send(
|
|
151
|
+
new SignUpCommand({
|
|
152
|
+
ClientId: clientId,
|
|
153
|
+
Username: username,
|
|
154
|
+
Password: DUMMY_PASSWORD,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
resp = await initiate();
|
|
158
|
+
} else {
|
|
159
|
+
throw e;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (resp.ChallengeName !== "CUSTOM_CHALLENGE") {
|
|
164
|
+
throw new Error(`Unexpected challenge: ${resp.ChallengeName} (expected CUSTOM_CHALLENGE)`);
|
|
165
|
+
}
|
|
166
|
+
store.savePending({
|
|
167
|
+
clientId,
|
|
168
|
+
username,
|
|
169
|
+
session: resp.Session,
|
|
170
|
+
initiatedAt: new Date().toISOString(),
|
|
171
|
+
});
|
|
172
|
+
return { challenge: resp.ChallengeName, params: resp.ChallengeParameters };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Step 2: email で受け取ったコードで CUSTOM_CHALLENGE を回答。
|
|
177
|
+
* 成功するとトークンを保存し、pending 状態を消す。
|
|
178
|
+
*
|
|
179
|
+
* @param {{loadPending:Function, save:Function, clearPending:Function}} store
|
|
180
|
+
*/
|
|
181
|
+
export async function loginVerify(store, code) {
|
|
182
|
+
const s = store.loadPending();
|
|
183
|
+
if (!s) {
|
|
184
|
+
throw new Error("No pending sign-in. 先に `login <email>` を実行してください。");
|
|
185
|
+
}
|
|
186
|
+
const resp = await cognito.send(
|
|
187
|
+
new RespondToAuthChallengeCommand({
|
|
188
|
+
ClientId: s.clientId,
|
|
189
|
+
ChallengeName: "CUSTOM_CHALLENGE",
|
|
190
|
+
Session: s.session,
|
|
191
|
+
ChallengeResponses: {
|
|
192
|
+
USERNAME: s.username,
|
|
193
|
+
ANSWER: code,
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (!resp.AuthenticationResult) {
|
|
199
|
+
if (resp.ChallengeName) {
|
|
200
|
+
throw new Error(`Another challenge required: ${resp.ChallengeName}. (再 login が必要)`);
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`No AuthenticationResult: ${JSON.stringify(resp)}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const r = resp.AuthenticationResult;
|
|
206
|
+
const tokens = {
|
|
207
|
+
clientId: s.clientId,
|
|
208
|
+
idToken: r.IdToken,
|
|
209
|
+
refreshToken: r.RefreshToken,
|
|
210
|
+
accessToken: r.AccessToken,
|
|
211
|
+
deviceKey: r.NewDeviceMetadata?.DeviceKey || null,
|
|
212
|
+
deviceGroupKey: r.NewDeviceMetadata?.DeviceGroupKey || null,
|
|
213
|
+
username: s.username,
|
|
214
|
+
lastRefresh: new Date().toISOString(),
|
|
215
|
+
};
|
|
216
|
+
store.save(tokens);
|
|
217
|
+
store.clearPending();
|
|
218
|
+
return tokens;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 既存の localStorage ダンプから bootstrap (互換用)。
|
|
223
|
+
*
|
|
224
|
+
* @param {{save:Function}} store
|
|
225
|
+
*/
|
|
226
|
+
export function bootstrap(store, values) {
|
|
227
|
+
if (!values.idToken) throw new Error("idToken required");
|
|
228
|
+
if (!values.refreshToken) throw new Error("refreshToken required");
|
|
229
|
+
const clientId = jwtAud(values.idToken) || DEFAULT_CLIENT_ID;
|
|
230
|
+
const t = {
|
|
231
|
+
clientId,
|
|
232
|
+
idToken: values.idToken,
|
|
233
|
+
refreshToken: values.refreshToken,
|
|
234
|
+
accessToken: values.accessToken || null,
|
|
235
|
+
deviceKey: values.deviceKey || null,
|
|
236
|
+
username: values.username || null,
|
|
237
|
+
lastRefresh: new Date().toISOString(),
|
|
238
|
+
};
|
|
239
|
+
store.save(t);
|
|
240
|
+
return t;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const CONFIG_META = {
|
|
244
|
+
region: COGNITO_REGION,
|
|
245
|
+
userPoolId: USER_POOL_ID,
|
|
246
|
+
consumerClientId: CONSUMER_CLIENT_ID,
|
|
247
|
+
bizClientId: BIZ_CLIENT_ID,
|
|
248
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// SESAME デバイスの「型モデル」。公式 SesameSDK (Android) の CHProductModel
|
|
2
|
+
// (open/devices/base/CHDeivceProtocols.kt) と各デバイスの能力 (capability) を Node に移植したもの。
|
|
3
|
+
//
|
|
4
|
+
// SDK では能力 (lock/unlock/toggle/click/autolock) が「型ごとのインターフェース」に個別宣言されており、
|
|
5
|
+
// 共通基底 (CHDevices / CHSesameLock) は施錠操作を一切持たない。つまり型ごとに能力が非対称。
|
|
6
|
+
// ここではその非対称性をそのままテーブル化する (共通基底に lock を生やすのは原典と乖離するため避ける)。
|
|
7
|
+
//
|
|
8
|
+
// 出典: _sesame_sdk_ref/sesame-sdk/.../open/devices/{CHSesame5,CHSesameBot2,CHSesameBike2,...}.kt,
|
|
9
|
+
// CHBleManager.kt (productModel.deviceFactory() で生成する実装クラス → OS世代/能力)。
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* BLE 上の「デバイス種別 (kind)」。productType→実装クラスの多対一を、能力の単位でまとめたもの。
|
|
13
|
+
* - lock5 : Sesame5/5Pro/6/6Pro/US/miwa, BLE Connector (OS3 ロック)
|
|
14
|
+
* - bot2 : SESAME Bot2/Bot3 (OS3) — click のみ
|
|
15
|
+
* - bike2 : SESAME Bike2/Bike3 (OS3) — unlock のみ
|
|
16
|
+
* - sesame2 : Sesame2/3/4 (OS2 ロック) — BLE は別プロトコル (未実装)
|
|
17
|
+
* - botOs2 : SESAME Bot1 (OS2) — BLE 未実装
|
|
18
|
+
* - bikeOs2 : Bike1 (OS2) — BLE 未実装
|
|
19
|
+
* - biometric : Touch/Face/OpenSensor/Remote (鍵束デバイス。施錠操作なし)
|
|
20
|
+
* - hub3 : Hub3/Hub3 LTE (IoT 中継。BLE 施錠操作なし)
|
|
21
|
+
* - wifi : WifiModule2
|
|
22
|
+
*/
|
|
23
|
+
export const KIND = Object.freeze({
|
|
24
|
+
LOCK5: "lock5", BOT2: "bot2", BIKE2: "bike2",
|
|
25
|
+
SESAME2: "sesame2", BOT_OS2: "botOs2", BIKE_OS2: "bikeOs2",
|
|
26
|
+
BIOMETRIC: "biometric", HUB3: "hub3", WIFI: "wifi",
|
|
27
|
+
UNKNOWN: "unknown", // テーブルに無い model。操作を捏造せず「操作なし」にする (lock5 に化けさせない)
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* kind ごとの能力定義。**経路 (cloud / ble) ごとに操作可能な op 集合**を持ち、
|
|
32
|
+
* 実際にユーザーに見せる/送れる op は両者の **和集合** で決まる (どちらの経路でも操作できないものは出さない)。
|
|
33
|
+
* - os : 世代 (2 | 3)
|
|
34
|
+
* - cloud : この CLI が **クラウド経由**で送れる制御 op (biz3TriggerLocker: lock/unlock/toggle/click、
|
|
35
|
+
* Hub3 は biz3OperateIoT/IR: ir/relay/led)。autolock はクラウド中継で実機未反映なので含めない。
|
|
36
|
+
* - ble : この CLI が **BLE 直接**で送れる制御 op。OS2 系は BLE プロトコル未実装なので空。
|
|
37
|
+
* - mechKind : mechStatus の解釈方法 ("os3lock" 7B / "os3bot" 3B / null)
|
|
38
|
+
* - label : 表示用の種別名
|
|
39
|
+
*
|
|
40
|
+
* 出典: 型×経路の可否は調査で確定 (lock.js triggerLock=機種非依存に lock/unlock/toggle/click を中継、
|
|
41
|
+
* autolock はクラウド未反映=lock.js:127-131、Hub3 の ir/relay/led=iot.js/transport.js、
|
|
42
|
+
* OS2 BLE 未実装=ble/* 、biometric は制御 op なし=管理のみ)。
|
|
43
|
+
*/
|
|
44
|
+
const CAPS = Object.freeze({
|
|
45
|
+
[KIND.LOCK5]: { os: 3, cloud: ["lock", "unlock", "toggle"], ble: ["lock", "unlock", "toggle", "autolock"], mechKind: "os3lock", label: "SESAME (lock)" },
|
|
46
|
+
[KIND.BOT2]: { os: 3, cloud: ["click"], ble: ["click"], mechKind: "os3bot", label: "SESAME Bot" },
|
|
47
|
+
[KIND.BIKE2]: { os: 3, cloud: ["unlock"], ble: ["unlock"], mechKind: "os3bot", label: "SESAME Bike" },
|
|
48
|
+
[KIND.SESAME2]: { os: 2, cloud: ["lock", "unlock", "toggle"], ble: [], mechKind: null, label: "SESAME (OS2 lock)" },
|
|
49
|
+
[KIND.BOT_OS2]: { os: 2, cloud: ["click"], ble: [], mechKind: null, label: "SESAME Bot (OS2)" },
|
|
50
|
+
[KIND.BIKE_OS2]: { os: 2, cloud: ["unlock"], ble: [], mechKind: null, label: "SESAME Bike (OS2)" },
|
|
51
|
+
[KIND.BIOMETRIC]:{ os: 3, cloud: [], ble: [], mechKind: null, label: "SESAME Touch/Face/Sensor/Remote" },
|
|
52
|
+
[KIND.HUB3]: { os: 3, cloud: ["ir", "relay", "led"], ble: [], mechKind: null, label: "SESAME Hub3" },
|
|
53
|
+
[KIND.WIFI]: { os: 3, cloud: ["ir", "relay", "led"], ble: [], mechKind: null, label: "WiFi Module 2" },
|
|
54
|
+
[KIND.UNKNOWN]: { os: 0, cloud: [], ble: [], mechKind: null, label: "(未知のデバイス)" },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** cloud と ble の op を和集合し、自然な提示順で返す (ble 由来を先、cloud 固有を後)。 */
|
|
58
|
+
function unionOps(caps) {
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const o of [...caps.ble, ...caps.cloud]) if (!seen.has(o)) { seen.add(o); out.push(o); }
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* productType (整数) → { model, kind }。
|
|
67
|
+
* 値は CHProductModel enum (CHDeivceProtocols.kt:28-252) と deviceFactory() の生成クラスに準拠。
|
|
68
|
+
* pType 12 は SDK でも欠番。
|
|
69
|
+
*/
|
|
70
|
+
export const PRODUCT_TYPES = Object.freeze({
|
|
71
|
+
0: { model: "sesame_2", kind: KIND.SESAME2 },
|
|
72
|
+
1: { model: "wm_2", kind: KIND.WIFI },
|
|
73
|
+
2: { model: "ssmbot_1", kind: KIND.BOT_OS2 },
|
|
74
|
+
3: { model: "bike_1", kind: KIND.BIKE_OS2 },
|
|
75
|
+
4: { model: "sesame_4", kind: KIND.SESAME2 },
|
|
76
|
+
5: { model: "sesame_5", kind: KIND.LOCK5 },
|
|
77
|
+
6: { model: "bike_2", kind: KIND.BIKE2 },
|
|
78
|
+
7: { model: "sesame_5_pro", kind: KIND.LOCK5 },
|
|
79
|
+
8: { model: "open_sensor_1", kind: KIND.BIOMETRIC },
|
|
80
|
+
9: { model: "ssm_touch_pro", kind: KIND.BIOMETRIC },
|
|
81
|
+
10: { model: "ssm_touch", kind: KIND.BIOMETRIC },
|
|
82
|
+
11: { model: "BLE_Connector_1", kind: KIND.LOCK5 },
|
|
83
|
+
13: { model: "hub_3", kind: KIND.HUB3 },
|
|
84
|
+
14: { model: "remote", kind: KIND.BIOMETRIC },
|
|
85
|
+
15: { model: "remote_nano", kind: KIND.BIOMETRIC },
|
|
86
|
+
16: { model: "sesame_5_us", kind: KIND.LOCK5 },
|
|
87
|
+
17: { model: "bot_2", kind: KIND.BOT2 },
|
|
88
|
+
18: { model: "sesame_face_Pro", kind: KIND.BIOMETRIC },
|
|
89
|
+
19: { model: "sesame_face", kind: KIND.BIOMETRIC },
|
|
90
|
+
20: { model: "sesame_6", kind: KIND.LOCK5 },
|
|
91
|
+
21: { model: "sesame_6_pro", kind: KIND.LOCK5 },
|
|
92
|
+
22: { model: "sesame_face_pro_ai", kind: KIND.BIOMETRIC },
|
|
93
|
+
23: { model: "sesame_face_ai", kind: KIND.BIOMETRIC },
|
|
94
|
+
24: { model: "open_sensor_2", kind: KIND.BIOMETRIC },
|
|
95
|
+
25: { model: "ssm_touch_2", kind: KIND.BIOMETRIC },
|
|
96
|
+
26: { model: "ssm_touch_2_pro", kind: KIND.BIOMETRIC },
|
|
97
|
+
27: { model: "sesame_face_2", kind: KIND.BIOMETRIC },
|
|
98
|
+
28: { model: "ssm_face_2_pro", kind: KIND.BIOMETRIC },
|
|
99
|
+
29: { model: "sesame_miwa", kind: KIND.LOCK5 },
|
|
100
|
+
30: { model: "sesame_face_2_ai", kind: KIND.BIOMETRIC },
|
|
101
|
+
31: { model: "sesame_face_2_pro_ai", kind: KIND.BIOMETRIC },
|
|
102
|
+
32: { model: "sesame_6_pro_slidingdoor", kind: KIND.LOCK5 },
|
|
103
|
+
33: { model: "bike_3", kind: KIND.BIKE2 },
|
|
104
|
+
35: { model: "bot_3", kind: KIND.BOT2 },
|
|
105
|
+
36: { model: "hub_3_lte", kind: KIND.HUB3 },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** model 文字列 → kind の逆引き表。 */
|
|
109
|
+
const KIND_BY_MODEL = Object.freeze(
|
|
110
|
+
Object.fromEntries(Object.values(PRODUCT_TYPES).map((v) => [v.model, v.kind])),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* model 文字列から kind を返す。
|
|
115
|
+
* - model 未指定 (null/空) → lock5 (SesameBle の config-less 利用 & 既存ロック config の後方互換。
|
|
116
|
+
* config.locks は同期時にロック機種のみ whitelist + model 保存なので、ここに来るのは実質ロック)。
|
|
117
|
+
* - model 文字列がテーブルに無い → **UNKNOWN (操作なし)**。未知機種を勝手にロック扱いして解錠等を
|
|
118
|
+
* 捏造しない (Hub3 が解錠を出していた類のバグを構造的に防ぐ)。
|
|
119
|
+
* @param {string|null|undefined} model
|
|
120
|
+
* @returns {string} KIND
|
|
121
|
+
*/
|
|
122
|
+
export function kindForModel(model) {
|
|
123
|
+
if (!model) return KIND.LOCK5;
|
|
124
|
+
return KIND_BY_MODEL[model] || KIND.UNKNOWN;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* model 文字列から能力定義を返す。
|
|
129
|
+
* - cloud / ble : 各経路で操作可能な op
|
|
130
|
+
* - ops : 和集合 (UI で見せる操作・提示順)
|
|
131
|
+
* - bleSupported: BLE 制御を実装しているか (= ble.length>0)
|
|
132
|
+
* @param {string|null|undefined} model
|
|
133
|
+
* @returns {{kind:string, os:number, cloud:string[], ble:string[], ops:string[], mechKind:string|null, bleSupported:boolean, label:string}}
|
|
134
|
+
*/
|
|
135
|
+
export function capabilitiesForModel(model) {
|
|
136
|
+
const kind = kindForModel(model);
|
|
137
|
+
const caps = CAPS[kind];
|
|
138
|
+
return { kind, ...caps, ops: unionOps(caps), bleSupported: caps.ble.length > 0 };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** その model が op を (いずれかの経路で) 操作できるか。 */
|
|
142
|
+
export function supportsOp(model, op) {
|
|
143
|
+
return capabilitiesForModel(model).ops.includes(op);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** その model が (いずれかの経路で) 何か操作できるか。session の対象判定に使う。 */
|
|
147
|
+
export function isOperable(model) {
|
|
148
|
+
return capabilitiesForModel(model).ops.length > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* その model の op を運べる transport 一覧 (型×経路の能力テーブルから導出)。
|
|
153
|
+
* 例: lock5 の autolock は ["ble"]、lock は ["ble","cloud"]、hub3 の ir は ["cloud"]。
|
|
154
|
+
* @param {string|null|undefined} model
|
|
155
|
+
* @param {string} op
|
|
156
|
+
* @returns {string[]} ("ble" / "cloud" の部分集合)
|
|
157
|
+
*/
|
|
158
|
+
export function transportsForOp(model, op) {
|
|
159
|
+
const caps = capabilitiesForModel(model);
|
|
160
|
+
const t = [];
|
|
161
|
+
if (caps.ble.includes(op)) t.push("ble");
|
|
162
|
+
if (caps.cloud.includes(op)) t.push("cloud");
|
|
163
|
+
return t;
|
|
164
|
+
}
|
package/src/ble/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// SESAME BLE 直接制御の公開エントリ。
|
|
2
|
+
//
|
|
3
|
+
// クラウド (WebSocket/biz3) を介さず、PC の Bluetooth から登録済み SESAME を直接操作する。
|
|
4
|
+
// クラウドでは不可だった設定系 (autolock 等) も BLE なら本体に反映される。
|
|
5
|
+
//
|
|
6
|
+
// 使い方 (高レベル):
|
|
7
|
+
// import { SesameBle } from "sesame-kit"; // もしくは: import { ble } from "sesame-kit"
|
|
8
|
+
// await SesameBle.use({ deviceUUID, secretKey }, async (lock) => {
|
|
9
|
+
// await lock.unlock();
|
|
10
|
+
// await lock.autolock(30); // ← クラウド不可・BLE 可
|
|
11
|
+
// console.log(lock.lastStatus);
|
|
12
|
+
// });
|
|
13
|
+
//
|
|
14
|
+
// 低レベル層 (protocol/session/transport) も個別 export。独自トランスポートを注入する場合は
|
|
15
|
+
// new SesameBle({ secretKey, transport }) で差し替え可能。
|
|
16
|
+
|
|
17
|
+
import { Buffer } from "node:buffer";
|
|
18
|
+
import { SesameBleSession } from "./session.js";
|
|
19
|
+
import { createBleTransport } from "./transport.js";
|
|
20
|
+
import {
|
|
21
|
+
ITEM, MECH_STATE, historyTagBLE, autolockData,
|
|
22
|
+
} from "./protocol.js";
|
|
23
|
+
import { capabilitiesForModel } from "./devicemodel.js";
|
|
24
|
+
|
|
25
|
+
import { scanSesames, NobleTransport } from "./transport.js";
|
|
26
|
+
|
|
27
|
+
export { SesameBleSession, BleResultError } from "./session.js";
|
|
28
|
+
// SesameResultCode (デバイス層の結果コード taxonomy)。BLE エラーの .resultName で分岐可能。
|
|
29
|
+
export { RESULT as SESAME_RESULT_CODES, resultName } from "./protocol.js";
|
|
30
|
+
export { NobleTransport, createBleTransport, advToDeviceUUID, scanSesames } from "./transport.js";
|
|
31
|
+
export * as protocol from "./protocol.js";
|
|
32
|
+
export * as devicemodel from "./devicemodel.js";
|
|
33
|
+
export { capabilitiesForModel, kindForModel, supportsOp, isOperable, transportsForOp, KIND, PRODUCT_TYPES } from "./devicemodel.js";
|
|
34
|
+
|
|
35
|
+
/** deviceUUID 正規化 (照合用)。 */
|
|
36
|
+
function normId(u) { return String(u).replace(/-/g, "").toLowerCase(); }
|
|
37
|
+
|
|
38
|
+
const STATUS_WAIT_MS = 4_000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 登録済み SESAME を BLE で直接操作する高レベルファサード。
|
|
42
|
+
*/
|
|
43
|
+
export class SesameBle {
|
|
44
|
+
/**
|
|
45
|
+
* @param {{
|
|
46
|
+
* secretKey: string|Buffer, // 32hex のロック共通鍵 (cloud の `sesame devices` で取得済み)
|
|
47
|
+
* deviceUUID?: string, // 対象識別 (advertise 照合)。複数 SESAME が近接する環境で必須
|
|
48
|
+
* address?: string, // BLE アドレスで識別する代替
|
|
49
|
+
* debug?: boolean,
|
|
50
|
+
* transport?: object, // 独自トランスポート (省略時 noble)
|
|
51
|
+
* }} opts
|
|
52
|
+
*/
|
|
53
|
+
constructor({ secretKey, deviceUUID, address, model = null, debug = false, scanTimeoutMs, transport } = {}) {
|
|
54
|
+
if (!secretKey) throw new Error("secretKey required (32hex)");
|
|
55
|
+
this._transport = transport || createBleTransport({ deviceUUID, address, debug, scanTimeoutMs });
|
|
56
|
+
this._session = new SesameBleSession({ transport: this._transport, secretKey, debug });
|
|
57
|
+
this._model = model;
|
|
58
|
+
this._caps = capabilitiesForModel(model); // 型ごとの能力 (SDK CHProductModel 準拠)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** デバイスの model 文字列 (例 "sesame_5" / "bot_2")。未指定なら null。 */
|
|
62
|
+
get model() { return this._model; }
|
|
63
|
+
/** 型ごとの能力 { kind, os, ops, mechKind, bleSupported, label }。 */
|
|
64
|
+
get capabilities() { return this._caps; }
|
|
65
|
+
/** この操作を BLE で送れるか (このファサードは BLE 専用なので ble 能力で判定)。 */
|
|
66
|
+
supports(op) { return this._caps.ble.includes(op); }
|
|
67
|
+
|
|
68
|
+
/** BLE で送れない操作を弾く。SDK では型ごとに能力が非対称 (Bot は click のみ等)。 */
|
|
69
|
+
_assertOp(op) {
|
|
70
|
+
if (!this._caps.ble.includes(op)) {
|
|
71
|
+
const ok = this._caps.ble.length ? this._caps.ble.join("/") : "(BLE 施錠操作なし)";
|
|
72
|
+
throw new Error(`${this._caps.label}${this._model ? ` (${this._model})` : ""} は BLE で ${op} をサポートしません。可能な操作: ${ok}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** mechStatus publish を購読 (戻り値 unsubscribe)。 */
|
|
77
|
+
onStatus(fn) { return this._session.onStatus(fn); }
|
|
78
|
+
/** 最後に受信した mechStatus。 */
|
|
79
|
+
get lastStatus() { return this._session.lastStatus; }
|
|
80
|
+
get isConnected() { return this._session.isLoggedIn; }
|
|
81
|
+
|
|
82
|
+
/** 接続 + login。 */
|
|
83
|
+
async connect() { await this._session.connect(); return this; }
|
|
84
|
+
/** 切断。 */
|
|
85
|
+
async close() { await this._session.disconnect(); }
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 施錠 (BLE item=82)。tag は履歴に残す任意ラベル。
|
|
89
|
+
* @returns {Promise<{resultCode:number, payload:Buffer}>}
|
|
90
|
+
*/
|
|
91
|
+
lock(tag) { this._assertOp("lock"); return this._session.request(ITEM.LOCK, historyTagBLE(tag)); }
|
|
92
|
+
|
|
93
|
+
/** 解錠 (BLE item=83)。Sesame5/6 ロックと Bike2 が対応。 */
|
|
94
|
+
unlock(tag) { this._assertOp("unlock"); return this._session.request(ITEM.UNLOCK, historyTagBLE(tag)); }
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* SESAME Bot のクリック (BLE item=89)。Bot2/Bot3 のみ。
|
|
98
|
+
* @param {Buffer} [tag] 履歴タグ (UUID バイト列)
|
|
99
|
+
*/
|
|
100
|
+
click(tag) { this._assertOp("click"); return this._session.request(ITEM.CLICK, historyTagBLE(tag)); }
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* トグル (Sesame5/6 ロックのみ)。直近の mechStatus が無ければ status() を取得してから判定。
|
|
104
|
+
* locked → unlock、それ以外 → lock (CHSesame5Device.kt:128-145 準拠)。
|
|
105
|
+
*/
|
|
106
|
+
async toggle(tag) {
|
|
107
|
+
this._assertOp("toggle");
|
|
108
|
+
let s = this.lastStatus;
|
|
109
|
+
if (!s) s = await this.status().catch(() => null);
|
|
110
|
+
if (s && s.state === MECH_STATE.LOCKED) return this._session.request(ITEM.UNLOCK, historyTagBLE(tag));
|
|
111
|
+
return this._session.request(ITEM.LOCK, historyTagBLE(tag));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* オートロック設定 (BLE item=11、2byte LE 秒数。0=無効)。Sesame5/6 ロックのみ。
|
|
116
|
+
* **BLE 経由なら実機に反映される** (クラウドの biz3TriggerLocker では ack のみで未反映だった機能)。
|
|
117
|
+
* @param {number} seconds 0..65535
|
|
118
|
+
*/
|
|
119
|
+
autolock(seconds) { this._assertOp("autolock"); return this._session.request(ITEM.AUTOLOCK, autolockData(seconds)); }
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 現在の mechStatus を返す。未受信なら publish を待つ (timeout 付き)。
|
|
123
|
+
* @param {{timeoutMs?:number}} [opts]
|
|
124
|
+
* @returns {Promise<object>} parseMechStatus の結果
|
|
125
|
+
*/
|
|
126
|
+
status({ timeoutMs = STATUS_WAIT_MS } = {}) {
|
|
127
|
+
if (this._session.lastStatus) return Promise.resolve(this._session.lastStatus);
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const to = setTimeout(() => { off(); reject(new Error("mechStatus を受信できませんでした (timeout)")); }, timeoutMs);
|
|
130
|
+
const off = this._session.onStatus((s) => { clearTimeout(to); off(); resolve(s); });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 履歴を 1 バッチ取得 (BLE item=4)。payload の解析は呼び出し側 (生バイト返し)。
|
|
136
|
+
* @returns {Promise<Buffer>}
|
|
137
|
+
*/
|
|
138
|
+
async history() {
|
|
139
|
+
const r = await this._session.request(ITEM.HISTORY, Buffer.from([0x01]));
|
|
140
|
+
return r.payload;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* connect → fn → close を自動で行うヘルパー。
|
|
145
|
+
* @param {object} opts コンストラクタ opts
|
|
146
|
+
* @param {(lock:SesameBle)=>Promise<any>} fn
|
|
147
|
+
*/
|
|
148
|
+
static async use(opts, fn) {
|
|
149
|
+
const lock = new SesameBle(opts);
|
|
150
|
+
await lock.connect();
|
|
151
|
+
try { return await fn(lock); }
|
|
152
|
+
finally { await lock.close(); }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 複数ロックに**1 回のスキャン**で同時接続する (逐次スキャンを避ける正攻法)。
|
|
157
|
+
* 近接していないロックは結果に現れず即スキップ (per-device の scan timeout を払わない)。
|
|
158
|
+
* 見つかったロックへは**並行接続** (login まで)。
|
|
159
|
+
*
|
|
160
|
+
* @param {Array<{name:string, deviceUUID:string, secretKey:string, model?:string}>} entries
|
|
161
|
+
* @param {{debug?:boolean, scanTimeoutMs?:number}} [opts]
|
|
162
|
+
* @returns {Promise<{connected: Map<string, SesameBle>, unreachable: string[], failed: Array<{name:string, error:Error}>}>}
|
|
163
|
+
*/
|
|
164
|
+
static async connectMany(entries, { debug = false, scanTimeoutMs = 8_000 } = {}) {
|
|
165
|
+
const found = await scanSesames({ deviceUUIDs: entries.map((e) => e.deviceUUID), timeoutMs: scanTimeoutMs, debug });
|
|
166
|
+
const byNorm = new Map([...found.entries()].map(([uuid, p]) => [normId(uuid), p]));
|
|
167
|
+
|
|
168
|
+
const connected = new Map();
|
|
169
|
+
const unreachable = [];
|
|
170
|
+
const failed = [];
|
|
171
|
+
|
|
172
|
+
const inRange = entries.filter((e) => byNorm.has(normId(e.deviceUUID)));
|
|
173
|
+
for (const e of entries) if (!byNorm.has(normId(e.deviceUUID))) unreachable.push(e.name);
|
|
174
|
+
|
|
175
|
+
// 見つかったものは並行で connect+login (別 peripheral なので同時接続可)。
|
|
176
|
+
await Promise.all(inRange.map(async (e) => {
|
|
177
|
+
const peripheral = byNorm.get(normId(e.deviceUUID));
|
|
178
|
+
const ble = new SesameBle({ secretKey: e.secretKey, deviceUUID: e.deviceUUID, model: e.model, debug, transport: new NobleTransport({ peripheral, debug }) });
|
|
179
|
+
try { await ble.connect(); connected.set(e.name, ble); }
|
|
180
|
+
catch (error) { failed.push({ name: e.name, error }); await ble.close().catch(() => {}); }
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
return { connected, unreachable, failed };
|
|
184
|
+
}
|
|
185
|
+
}
|