// 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; } }