1// Copyright 2022 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15import { setPathOnObject } from './object_set'; 16import { Decoder, Encoder } from 'pigweedjs/pw_hdlc'; 17import { 18 Client, 19 Channel, 20 ServiceClient, 21 UnaryMethodStub, 22 MethodStub, 23 ServerStreamingMethodStub, 24} from 'pigweedjs/pw_rpc'; 25import { WebSerialTransport } from '../transport/web_serial_transport'; 26import { ProtoCollection } from 'pigweedjs/pw_protobuf_compiler'; 27 28function protoFieldToMethodName(fieldName: string) { 29 return fieldName.split('_').map(titleCase).join(''); 30} 31function titleCase(title: string) { 32 return title.charAt(0).toUpperCase() + title.slice(1); 33} 34 35export class Device { 36 private protoCollection: ProtoCollection; 37 private transport: WebSerialTransport; 38 private decoder: Decoder; 39 private encoder: Encoder; 40 private rpcAddress: number; 41 private nameToMethodArgumentsMap: any; 42 client: Client; 43 rpcs: any; 44 45 constructor( 46 protoCollection: ProtoCollection, 47 transport: WebSerialTransport = new WebSerialTransport(), 48 rpcAddress = 82, 49 ) { 50 this.transport = transport; 51 this.rpcAddress = rpcAddress; 52 this.protoCollection = protoCollection; 53 this.decoder = new Decoder(); 54 this.encoder = new Encoder(); 55 this.nameToMethodArgumentsMap = {}; 56 const channels = [ 57 new Channel(1, (bytes) => { 58 const hdlcBytes = this.encoder.uiFrame(this.rpcAddress, bytes); 59 this.transport.sendChunk(hdlcBytes); 60 }), 61 ]; 62 this.client = Client.fromProtoSet(channels, this.protoCollection); 63 64 this.setupRpcs(); 65 } 66 67 async connect() { 68 await this.transport.connect(); 69 this.transport.chunks.subscribe((item) => { 70 const decoded = this.decoder.process(item); 71 for (const frame of decoded) { 72 if (frame.address === this.rpcAddress) { 73 this.client.processPacket(frame.data); 74 } 75 } 76 }); 77 } 78 79 getMethodArguments(fullPath: string) { 80 return this.nameToMethodArgumentsMap[fullPath]; 81 } 82 83 private setupRpcs() { 84 const rpcMap = {}; 85 const channel = this.client.channel()!; 86 const servicesKeys = Array.from(channel.services.keys()); 87 servicesKeys.forEach((serviceKey) => { 88 setPathOnObject( 89 rpcMap, 90 serviceKey, 91 this.mapServiceMethods(channel.services.get(serviceKey)!), 92 ); 93 }); 94 this.rpcs = rpcMap; 95 } 96 97 private mapServiceMethods(service: ServiceClient) { 98 const methodMap: { [index: string]: any } = {}; 99 const methodKeys = Array.from(service.methodsByName.keys()); 100 methodKeys 101 .filter( 102 (method: any) => 103 service.methodsByName.get(method) instanceof UnaryMethodStub || 104 service.methodsByName.get(method) instanceof 105 ServerStreamingMethodStub, 106 ) 107 .forEach((key) => { 108 const fn = this.createMethodWrapper( 109 service.methodsByName.get(key)!, 110 key, 111 `${service.name}.${key}`, 112 ); 113 methodMap[key] = fn; 114 }); 115 return methodMap; 116 } 117 118 private createMethodWrapper( 119 realMethod: MethodStub, 120 methodName: string, 121 fullMethodPath: string, 122 ) { 123 if (realMethod instanceof UnaryMethodStub) { 124 return this.createUnaryMethodWrapper( 125 realMethod, 126 methodName, 127 fullMethodPath, 128 ); 129 } else if (realMethod instanceof ServerStreamingMethodStub) { 130 return this.createServerStreamingMethodWrapper( 131 realMethod, 132 methodName, 133 fullMethodPath, 134 ); 135 } 136 throw new Error(`Unknown method: ${realMethod}`); 137 } 138 139 private createUnaryMethodWrapper( 140 realMethod: UnaryMethodStub, 141 methodName: string, 142 fullMethodPath: string, 143 ) { 144 const requestType = realMethod.method 145 .descriptor!.getInputType() 146 .replace(/^\./, ''); 147 const requestProtoDescriptor = 148 this.protoCollection.getDescriptorProto(requestType)!; 149 const requestFields = requestProtoDescriptor.getFieldList()!; 150 const functionArguments = requestFields 151 .map((field) => field.getName()) 152 .concat('return this(arguments);'); 153 154 // We store field names so REPL can show hints in autocomplete using these. 155 this.nameToMethodArgumentsMap[fullMethodPath] = requestFields.map((field) => 156 field.getName(), 157 ); 158 159 // We create a new JS function dynamically here that takes 160 // proto message fields as arguments and calls the actual RPC method. 161 const fn = new Function(...functionArguments).bind((args: any[]) => { 162 const request = new realMethod.method.requestType(); 163 requestFields.forEach((field, index) => { 164 request[`set${titleCase(field.getName())}`](args[index]); 165 }); 166 return realMethod.call(request); 167 }); 168 return fn; 169 } 170 171 private createServerStreamingMethodWrapper( 172 realMethod: ServerStreamingMethodStub, 173 methodName: string, 174 fullMethodPath: string, 175 ) { 176 const requestType = realMethod.method 177 .descriptor!.getInputType() 178 .replace(/^\./, ''); 179 const requestProtoDescriptor = 180 this.protoCollection.getDescriptorProto(requestType)!; 181 const requestFields = requestProtoDescriptor.getFieldList(); 182 const functionArguments = requestFields 183 .map((field) => field.getName()) 184 .concat(['onNext', 'onComplete', 'onError', 'return this(arguments);']); 185 186 // We store field names so REPL can show hints in autocomplete using these. 187 this.nameToMethodArgumentsMap[fullMethodPath] = requestFields.map((field) => 188 field.getName(), 189 ); 190 191 // We create a new JS function dynamically here that takes 192 // proto message fields as arguments and calls the actual RPC method. 193 const fn = new Function(...functionArguments).bind((args: any[]) => { 194 const request = new realMethod.method.requestType(); 195 requestFields.forEach((field, index) => { 196 request[`set${protoFieldToMethodName(field.getName())}`](args[index]); 197 }); 198 const callbacks = Array.from(args).slice(requestFields.length); 199 return realMethod.invoke( 200 request, 201 // @ts-ignore 202 callbacks[0], 203 // @ts-ignore 204 callbacks[1], 205 // @ts-ignore 206 callbacks[2], 207 ); 208 }); 209 return fn; 210 } 211} 212