1
+ /* eslint-disable max-depth */
2
+ import {
3
+ BinaryReader ,
4
+ FileDescriptorProto ,
5
+ MethodOptions ,
6
+ } from '@bufbuild/protobuf' ;
1
7
import {
2
8
Code ,
3
9
ConnectError ,
4
10
createClient ,
5
11
type Transport ,
6
12
} from '@connectrpc/connect' ;
13
+ import { createAsyncIterable } from '@connectrpc/connect/protocol' ;
14
+ import { safety_heartbeat_monitored as safteyHeartbeatMonitored } from '../gen/common/v1/common_pb' ;
15
+ import { ServerReflection } from '../gen/grpc/reflection/v1/reflection_connect' ;
16
+ import {
17
+ FileDescriptorResponse ,
18
+ ListServiceResponse ,
19
+ ServerReflectionRequest ,
20
+ } from '../gen/grpc/reflection/v1/reflection_pb' ;
7
21
import { RobotService } from '../gen/robot/v1/robot_connect' ;
8
22
import {
9
23
SendSessionHeartbeatRequest ,
@@ -22,11 +36,14 @@ const timeoutBlob = new Blob(
22
36
) ;
23
37
24
38
export default class SessionManager {
39
+ public static heartbeatMonitoredMethods : Record < string , boolean > = { } ;
40
+
25
41
public readonly transport : Transport ;
26
42
27
43
private currentSessionID = '' ;
28
44
private sessionsSupported : boolean | undefined ;
29
45
private heartbeatIntervalMs : number | undefined ;
46
+ private host = '' ;
30
47
31
48
private starting : Promise < void > | undefined ;
32
49
@@ -35,7 +52,11 @@ export default class SessionManager {
35
52
return createClient ( RobotService , transport ) ;
36
53
}
37
54
38
- constructor ( private deferredTransport : ( ) => Transport ) {
55
+ constructor (
56
+ host : string ,
57
+ private deferredTransport : ( ) => Transport
58
+ ) {
59
+ this . host = host ;
39
60
this . transport = new SessionTransport ( this . deferredTransport , this ) ;
40
61
}
41
62
@@ -167,6 +188,7 @@ export default class SessionManager {
167
188
( Number ( heartbeatWindow . seconds ) * 1e3 +
168
189
heartbeatWindow . nanos / 1e6 ) /
169
190
5 ;
191
+ await this . applyHeartbeatMonitoredMethods ( ) ;
170
192
resolve ( ) ;
171
193
this . heartbeat ( ) . catch ( console . error ) ; // eslint-disable-line no-console
172
194
} ) ( )
@@ -180,4 +202,83 @@ export default class SessionManager {
180
202
181
203
return this . getSessionMetadataInner ( ) ;
182
204
}
205
+
206
+ private async applyHeartbeatMonitoredMethods ( ) : Promise < void > {
207
+ try {
208
+ const client = createClient ( ServerReflection , this . transport ) ;
209
+ const request = new ServerReflectionRequest ( {
210
+ host : this . host ,
211
+ messageRequest : { case : 'listServices' , value : '' } ,
212
+ } ) ;
213
+ const responseStream = client . serverReflectionInfo (
214
+ createAsyncIterable ( [ request ] ) ,
215
+ { timeoutMs : 10_000 }
216
+ ) ;
217
+ for await ( const serviceResponse of responseStream ) {
218
+ const fdpRequests = (
219
+ serviceResponse . messageResponse . value as ListServiceResponse
220
+ ) . service . map ( ( service ) => {
221
+ return new ServerReflectionRequest ( {
222
+ messageRequest : {
223
+ case : 'fileContainingSymbol' ,
224
+ value : service . name ,
225
+ } ,
226
+ } ) ;
227
+ } ) ;
228
+ const fdpResponseStream = client . serverReflectionInfo (
229
+ createAsyncIterable ( fdpRequests ) ,
230
+ { timeoutMs : 10_000 }
231
+ ) ;
232
+ for await ( const fdpResponse of fdpResponseStream ) {
233
+ for ( const fdp of (
234
+ fdpResponse . messageResponse . value as FileDescriptorResponse
235
+ ) . fileDescriptorProto ) {
236
+ const protoFile = FileDescriptorProto . fromBinary ( fdp ) ;
237
+ for ( const service of protoFile . service ) {
238
+ for ( const method of service . method ) {
239
+ SessionManager . heartbeatMonitoredMethods [
240
+ `/${ protoFile . package } .${ service . name } /${ method . name } `
241
+ ] = SessionManager . hasHeartbeatOption ( method . options ) ;
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ } catch {
248
+ // If can't get heartbeat monitored methods via reflection, use defaults.
249
+ SessionManager . heartbeatMonitoredMethods = {
250
+ '/viam.component.arm.v1.ArmService/MoveToPosition' : true ,
251
+ '/viam.component.arm.v1.ArmService/MoveToJointPositions' : true ,
252
+ '/viam.component.arm.v1.ArmService/MoveThroughJointPositions' : true ,
253
+ '/viam.component.base.v1.BaseService/MoveStraight' : true ,
254
+ '/viam.component.base.v1.BaseService/Spin' : true ,
255
+ '/viam.component.base.v1.BaseService/SetPower' : true ,
256
+ '/viam.component.base.v1.BaseService/SetVelocity' : true ,
257
+ '/viam.component.gantry.v1.GantryService/MoveToPosition' : true ,
258
+ '/viam.component.gripper.v1.GripperService/Open' : true ,
259
+ '/viam.component.gripper.v1.GripperService/Grab' : true ,
260
+ '/viam.component.motor.v1.MotorService/SetPower' : true ,
261
+ '/viam.component.motor.v1.MotorService/GoFor' : true ,
262
+ '/viam.component.motor.v1.MotorService/GoTo' : true ,
263
+ '/viam.component.motor.v1.MotorService/SetRPM' : true ,
264
+ '/viam.component.servo.v1.ServoService/Move' : true ,
265
+ } ;
266
+ }
267
+ }
268
+
269
+ private static hasHeartbeatOption ( options ?: MethodOptions ) : boolean {
270
+ if ( ! options ) {
271
+ return false ;
272
+ }
273
+ const reader = new BinaryReader ( options . toBinary ( ) ) ;
274
+ while ( reader . pos < reader . len ) {
275
+ const tag = reader . tag ( ) ;
276
+ const [ fieldNumber ] = tag ;
277
+ if ( fieldNumber === safteyHeartbeatMonitored . field . no ) {
278
+ return true ;
279
+ }
280
+ reader . string ( ) ;
281
+ }
282
+ return false ;
283
+ }
183
284
}
0 commit comments