• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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