| // Copyright 2022 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| import objectPath from 'object-path'; |
| import {Decoder, Encoder} from 'pigweedjs/pw_hdlc'; |
| import { |
| Client, |
| Channel, |
| ServiceClient, |
| UnaryMethodStub, |
| MethodStub, |
| ServerStreamingMethodStub |
| } from 'pigweedjs/pw_rpc'; |
| import {WebSerialTransport} from '../transport/web_serial_transport'; |
| import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler'; |
| |
| function protoFieldToMethodName(string) { |
| return string.split("_").map(titleCase).join(""); |
| } |
| function titleCase(string) { |
| return string.charAt(0).toUpperCase() + string.slice(1); |
| } |
| |
| export class Device { |
| private protoCollection: ProtoCollection; |
| private transport: WebSerialTransport; |
| private decoder: Decoder; |
| private encoder: Encoder; |
| private rpcAddress: number; |
| private nameToMethodArgumentsMap: any; |
| client: Client; |
| rpcs: any |
| |
| constructor( |
| protoCollection: ProtoCollection, |
| transport: WebSerialTransport = new WebSerialTransport(), |
| rpcAddress: number = 82) { |
| this.transport = transport; |
| this.rpcAddress = rpcAddress; |
| this.protoCollection = protoCollection; |
| this.decoder = new Decoder(); |
| this.encoder = new Encoder(); |
| this.nameToMethodArgumentsMap = {}; |
| const channels = [ |
| new Channel(1, (bytes) => { |
| const hdlcBytes = this.encoder.uiFrame(this.rpcAddress, bytes); |
| this.transport.sendChunk(hdlcBytes); |
| })]; |
| this.client = |
| Client.fromProtoSet(channels, this.protoCollection); |
| |
| this.setupRpcs(); |
| } |
| |
| async connect() { |
| await this.transport.connect(); |
| this.transport.chunks.subscribe((item) => { |
| const decoded = this.decoder.process(item); |
| for (const frame of decoded) { |
| if (frame.address === this.rpcAddress) { |
| this.client.processPacket(frame.data); |
| } |
| } |
| }); |
| } |
| |
| getMethodArguments(fullPath) { |
| return this.nameToMethodArgumentsMap[fullPath]; |
| } |
| |
| private setupRpcs() { |
| let rpcMap = {}; |
| let channel = this.client.channel(); |
| let servicesKeys = Array.from(channel.services.keys()); |
| servicesKeys.forEach((serviceKey) => { |
| objectPath.set(rpcMap, serviceKey, |
| this.mapServiceMethods(channel.services.get(serviceKey)) |
| ); |
| }); |
| this.rpcs = rpcMap; |
| } |
| |
| private mapServiceMethods(service: ServiceClient) { |
| let methodMap = {}; |
| let methodKeys = Array.from(service.methodsByName.keys()); |
| methodKeys |
| .filter((method: any) => |
| service.methodsByName.get(method) instanceof UnaryMethodStub |
| || service.methodsByName.get(method) instanceof ServerStreamingMethodStub) |
| .forEach(key => { |
| let fn = this.createMethodWrapper( |
| service.methodsByName.get(key), |
| key, |
| `${service.name}.${key}` |
| ); |
| methodMap[key] = fn; |
| }); |
| return methodMap; |
| } |
| |
| private createMethodWrapper( |
| realMethod: MethodStub, |
| methodName: string, |
| fullMethodPath: string) { |
| if (realMethod instanceof UnaryMethodStub) { |
| return this.createUnaryMethodWrapper( |
| realMethod, |
| methodName, |
| fullMethodPath); |
| } |
| else if (realMethod instanceof ServerStreamingMethodStub) { |
| return this.createServerStreamingMethodWrapper( |
| realMethod, |
| methodName, |
| fullMethodPath); |
| } |
| } |
| |
| private createUnaryMethodWrapper( |
| realMethod: UnaryMethodStub, |
| methodName: string, |
| fullMethodPath: string) { |
| const requestType = |
| realMethod.method.descriptor.getInputType().replace(/^\./, ''); |
| const requestProtoDescriptor = |
| this.protoCollection.getDescriptorProto(requestType); |
| const requestFields = requestProtoDescriptor.getFieldList(); |
| const functionArguments = requestFields |
| .map(field => field.getName()) |
| .concat( |
| 'return this(arguments);' |
| ); |
| |
| // We store field names so REPL can show hints in autocomplete using these. |
| this.nameToMethodArgumentsMap[fullMethodPath] = requestFields |
| .map(field => field.getName()); |
| |
| // We create a new JS function dynamically here that takes |
| // proto message fields as arguments and calls the actual RPC method. |
| let fn = new Function(...functionArguments).bind((args) => { |
| const request = new realMethod.method.requestType(); |
| requestFields.forEach((field, index) => { |
| request[`set${titleCase(field.getName())}`](args[index]); |
| }) |
| return realMethod.call(request); |
| }); |
| return fn; |
| } |
| |
| private createServerStreamingMethodWrapper( |
| realMethod: ServerStreamingMethodStub, |
| methodName: string, |
| fullMethodPath: string) { |
| const requestType = realMethod.method.descriptor.getInputType().replace(/^\./, ''); |
| const requestProtoDescriptor = |
| this.protoCollection.getDescriptorProto(requestType); |
| const requestFields = requestProtoDescriptor.getFieldList(); |
| const functionArguments = requestFields |
| .map(field => field.getName()) |
| .concat( |
| [ |
| 'onNext', |
| 'onComplete', |
| 'onError', |
| 'return this(arguments);' |
| ] |
| ); |
| |
| // We store field names so REPL can show hints in autocomplete using these. |
| this.nameToMethodArgumentsMap[fullMethodPath] = requestFields |
| .map(field => field.getName()); |
| |
| // We create a new JS function dynamically here that takes |
| // proto message fields as arguments and calls the actual RPC method. |
| let fn = new Function(...functionArguments).bind((args) => { |
| const request = new realMethod.method.requestType(); |
| requestFields.forEach((field, index) => { |
| request[`set${protoFieldToMethodName(field.getName())}`](args[index]); |
| }) |
| const callbacks = Array.from(args).slice(requestFields.length); |
| // @ts-ignore |
| return realMethod.invoke(request, callbacks[0], callbacks[1], callbacks[2]); |
| }); |
| return fn; |
| } |
| } |