ソケット層(socket)¶
socket/* は 端末(Jetson/Raspberry Pi)とサーバの双方向通信を担う層です。
接続管理(register/disconnect)、ACK 応答のハンドリング、HTTP→Socket ブリッジ(音量トグル)を提供します。
- 参照ソース:
socket/index.js,socket/commonRequests.js,socket/deviceRegistry.js,socket/toggleVolume.js,requestStores.js
構成(役割別)¶
- index.js:
initSocket(server)で Socket.IO を初期化し、ACK を受ける
thumbnailResponse/versionsResponse/patchMigStateResponse/volumeStatusChanged
getIO()を公開(サービス層から利用) - commonRequests.js:ACK 共通ハンドラ
handleVersionResponse()/handlePatchMigResponse()/handleListResponse() - deviceRegistry.js:デバイス登録・離脱の管理
registerDevice/disconnect、deviceSocketsマップの更新 - toggleVolume.js:HTTP ルートから音量トグルを発火し、ACK(
volumeStatusChanged)を待つ
setGetIO(fn)はテスト用の差し替えで利用 - requestStores.js(ルート):共有ストア
deviceSockets/requests/thumbnailRequests
初期化と配線(
socket/index.js)¶
initSocket(server)(抜粋仕様)
io = new Server(server, { path: '/socket.io', cors: { origin: '*', methods: ['GET','POST'] } })deviceRegistry.bind(io)を呼び出しio.use(...)で mTLS 判定(Host がdevice.api.xrobotics.jpの場合のみ)x-client-certから certId を算出 → IoT で thingName 解決socket.data.mtlsDeviceIdを設定し、mtlsLastSeenStoreに last-seen を記録io.on('connection', socket => { ...汎用 ACK をバインド... })getIO()をエクスポート(サービス層や HTTP ハンドラから利用)
汎用 ACK(接続毎にバインド)¶
thumbnailResponse({ requestId, buffer, error })
→thumbnailRequestsを解決/タイムアウト解除volumeStatusChanged({ requestId, muted })
→requestsを解決(toggleVolumeの応答、resolveFn(muted))versionsResponse(data)→handleVersionResponse(data, requests)patchMigStateResponse(data)→handlePatchMigResponse(data, requests)
CORS/Origin
本番環境では origin: '*' を適切なドメインに制限してください。
デバイス登録(
socket/deviceRegistry.js)¶
イベント¶
registerDevice(deviceId)
mTLS 検証済み接続の場合はsocket.data.mtlsDeviceIdを優先し、deviceId ⇢ socket.idを登録disconnect(reason)
deviceSocketsを逆引きしてdeviceIdを削除
ヘルパ¶
getSocketId(deviceId)/isDeviceOnline(deviceId)
デバイス登録の挙動¶
1) デバイスが接続 → registerDevice を送信
2) サーバは deviceId ⇢ socket.id を deviceSockets に記録
3) 切断時は該当 deviceId のエントリを削除
接続ハンドラの重複
deviceRegistry.bind(io) と socket/index.js の双方で io.on('connection') を使います。
1 接続につき 2 つの connection ハンドラが動く点に注意してください。
バージョン・パッチ状態 ACK(
socket/commonRequests.js)¶
versionsResponse¶
data = { requestId, serverVersion, uiVersion, farmVersion, error }- 同一
requestIdの待機者に
{ server, ui, farm }で解決/errorなら reject
patchMigStateResponse¶
data = { requestId, state, error }- 同一
requestIdの待機者に{ state }で解決/errorなら reject
サムネ ACK(
thumbnailResponseinsocket/index.js)¶
data = { requestId, buffer, error }thumbnailRequestsから該当requestIdを取得- タイムアウト解除 →
resolve(buffer)/reject(new Error(error))
HTTP → Socket ブリッジ(音量トグル:
socket/toggleVolume.js)¶
エクスポート¶
setGetIO(fn):テストでgetIOを差し替えるためのフックtoggleVolumeHandler(req, res):POST /api/commands/sendから呼ばれる
音量トグルの挙動¶
1) deviceId を検証 → deviceSockets から sockId を取得
2) getIO() で sock を取り出し、requestId = uuidv4() を生成
3) requests.set(requestId, { resolveFn, rejectFn, timeout }) を登録(10s)
4) sock.emit('toggleVolume', { requestId }) を送信
5) index.js の volumeStatusChanged({ requestId, muted }) で一致したら
clearTimeout → requests.delete(requestId) → res.json({ muted })
ACK 付きユーティリティ(
services/socket/emitWithAck.js)¶
deviceSettingsService/deviceWifiServiceはemitWithAckを利用して ACK を待機- ACK イベントは
configResponse/configUpdated/wifiNetworksResponse/wifiNetworkDeleted
共有ストア(
requestStores.js)¶
deviceSockets = new Map()
Key:deviceId(文字列)
Value:socket.id(文字列)
接続/切断時にdeviceRegistryが更新requests = new Map()
Key:requestId(UUID)
Value: ルートにより{ resolve, reject, timeout }または
{ resolveFn, rejectFn, timeout }の形を取るthumbnailRequests = new Map()
Key:requestId(UUID)
Value:{ resolve, reject, timeout }
後始末(必須)
いずれの Map も resolve/reject 時に clearTimeout と delete を必ず実施してください。
リスナも同様に解除しないとメモリリークの原因になります。
生成/変更されるもの¶
- 生成:メモリ上の Map(
deviceSockets,requests,thumbnailRequests)のみ - 恒久的ファイル・永続設定の変更はありません
注意¶
相関の原則
すべての往復で requestId を発行し、ACK 側で同一性を検証します。
並列処理
同じイベントを並列に投げる場合でも、requestId 単位で待機者を分離してください。
Origin 制限
本番では cors.origin を厳格に。/socket.io パスも変更を検討できます。