blob: dbaf9635f3e99d05c4e78d63fa80ab10b2d8ff4a [file] [log] [blame]
// 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 { setPathOnObject } from './object_set';
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(fieldName: string) {
return fieldName.split('_').map(titleCase).join('');
}
function titleCase(title: string) {
return title.charAt(0).toUpperCase() + title.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 = 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: string) {
return this.nameToMethodArgumentsMap[fullPath];
}
private setupRpcs() {
const rpcMap = {};
const channel = this.client.channel()!;
const servicesKeys = Array.from(channel.services.keys());
servicesKeys.forEach((serviceKey) => {
setPathOnObject(
rpcMap,
serviceKey,
this.mapServiceMethods(channel.services.get(serviceKey)!),
);
});
this.rpcs = rpcMap;
}
private mapServiceMethods(service: ServiceClient) {
const methodMap: { [index: string]: any } = {};
const methodKeys = Array.from(service.methodsByName.keys());
methodKeys
.filter(
(method: any) =>
service.methodsByName.get(method) instanceof UnaryMethodStub ||
service.methodsByName.get(method) instanceof
ServerStreamingMethodStub,
)
.forEach((key) => {
const 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,
);
}
throw new Error(`Unknown method: ${realMethod}`);
}
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.
const fn = new Function(...functionArguments).bind((args: any[]) => {
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.
const fn = new Function(...functionArguments).bind((args: any[]) => {
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);
return realMethod.invoke(
request,
// @ts-ignore
callbacks[0],
// @ts-ignore
callbacks[1],
// @ts-ignore
callbacks[2],
);
});
return fn;
}
}