xref: /aosp_15_r20/external/pigweed/pw_web/docs.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1.. _module-pw_web:
2
3---------
4pw_web
5---------
6Pigweed provides an NPM package with modules to build web apps for Pigweed
7devices.
8
9Getting Started
10===============
11
12Easiest way to get started is to follow the :ref:`Sense tutorial <showcase-sense-tutorial-intro>`
13and flash a Raspberry Pico board.
14
15Once you have a device running Pigweed, you can connect to it using just your web browser.
16
17Installation
18-------------
19If you have a bundler set up, you can install ``pigweedjs`` in your web application by:
20
21.. code-block:: bash
22
23   $ npm install --save pigweedjs
24
25
26After installing, you can import modules from ``pigweedjs`` in this way:
27
28.. code-block:: javascript
29
30   import { pw_rpc, pw_tokenizer, Device, WebSerial } from 'pigweedjs';
31
32Import Directly in HTML
33^^^^^^^^^^^^^^^^^^^^^^^
34If you don't want to set up a bundler, you can also load Pigweed directly in
35your HTML page by:
36
37.. code-block:: html
38
39   <script src="https://unpkg.com/pigweedjs/dist/index.umd.js"></script>
40   <script>
41     const { pw_rpc, pw_hdlc, Device, WebSerial } from Pigweed;
42   </script>
43
44Modules
45=======
46.. _module-pw_web-device:
47
48Device
49------
50Device class is a helper API to connect to a device over serial and call RPCs
51easily.
52
53To initialize device, it needs a ``ProtoCollection`` instance. ``pigweedjs``
54includes a default one which you can use to get started, you can also generate
55one from your own ``.proto`` files using ``pw_proto_compiler``.
56
57``Device`` goes through all RPC methods in the provided ProtoCollection. For
58each RPC, it reads all the fields in ``Request`` proto and generates a
59JavaScript function to call that RPC and also a helper method to create a request.
60It then makes this function available under ``rpcs.*`` namespaced by its package name.
61
62Device has following public API:
63
64- ``constructor(ProtoCollection, WebSerialTransport <optional>, channel <optional>, rpcAddress <optional>)``
65- ``connect()`` - Shows browser's WebSerial connection dialog and let's user
66  make device selection
67- ``rpcs.*`` - Device API enumerates all RPC services and methods present in the
68  provided proto collection and makes them available as callable functions under
69  ``rpcs``. Example: If provided proto collection includes Pigweed's Echo
70  service ie. ``pw.rpc.EchoService.Echo``, it can be triggered by calling
71  ``device.rpcs.pw.rpc.EchoService.Echo.call(request)``. The functions return
72  a ``Promise`` that resolves an array with status and response.
73
74Using Device API with Sense
75^^^^^^^^^^^^^^^^^^^^^^^^^^^
76Sense project uses ``pw_log_rpc``; an RPC-based logging solution. Sense
77also uses pw_tokenizer to tokenize strings and save device space. Below is an
78example that streams logs using the ``Device`` API.
79
80.. code-block:: html
81
82   <h1>Hello Pigweed</h1>
83   <button onclick="connect()">Connect</button>
84   <br /><br />
85   <code></code>
86   <script src="https://unpkg.com/pigweedjs/dist/index.umd.js"></script>
87   <script src="https://unpkg.com/pigweedjs/dist/protos/collection.umd.js"></script>
88   <script>
89     const { Device, pw_tokenizer } = Pigweed;
90     const { ProtoCollection } = PigweedProtoCollection;
91     const tokenDBCsv = `...` // Load token database here
92
93     const device = new Device(new ProtoCollection());
94     const detokenizer = new pw_tokenizer.Detokenizer(tokenDBCsv);
95
96     async function connect(){
97       await device.connect();
98       const req = device.rpcs.pw.log.Logs.Listen.createRequest()
99       const logs = device.rpcs.pw.log.Logs.Listen.call(req);
100       for await (const msg of logs){
101           msg.getEntriesList().forEach((entry) => {
102             const frame = entry.getMessage();
103             const detokenized = detokenizer.detokenizeUint8Array(frame);
104             document.querySelector('code').innerHTML += detokenized + "<br/>";
105           });
106       }
107       console.log("Log stream ended with status", logs.call.status);
108     }
109   </script>
110
111The above example requires a token database in CSV format. You can generate one
112from the Sense's ``.elf`` file by running:
113
114.. code-block:: bash
115
116   $ pw_tokenizer/py/pw_tokenizer/database.py create \
117   --database db.csv bazel-bin/apps/blinky/rp2040_blinky.elf
118
119You can then load this CSV in JavaScript using ``fetch()`` or by just copying
120the contents into the ``tokenDBCsv`` variable in the above example.
121
122WebSerialTransport
123------------------
124To help with connecting to WebSerial and listening for serial data, a helper
125class is also included under ``WebSerial.WebSerialTransport``. Here is an
126example usage:
127
128.. code-block:: javascript
129
130   import { WebSerial, pw_hdlc } from 'pigweedjs';
131
132   const transport = new WebSerial.WebSerialTransport();
133   const decoder = new pw_hdlc.Decoder();
134
135   // Present device selection prompt to user
136   await transport.connect();
137
138   // Or connect to an existing `SerialPort`
139   // await transport.connectPort(port);
140
141   // Listen and decode HDLC frames
142   transport.chunks.subscribe((item) => {
143     const decoded = decoder.process(item);
144     for (const frame of decoded) {
145       if (frame.address === 1) {
146         const decodedLine = new TextDecoder().decode(frame.data);
147         console.log(decodedLine);
148       }
149     }
150   });
151
152   // Later, close all streams and close the port.
153   transport.disconnect();
154
155Individual Modules
156==================
157Following Pigweed modules are included in the NPM package:
158
159- `pw_hdlc <https://pigweed.dev/pw_hdlc/#typescript>`_
160- `pw_rpc <https://pigweed.dev/pw_rpc/ts/>`_
161- `pw_tokenizer <https://pigweed.dev/pw_tokenizer/#typescript>`_
162- `pw_transfer <https://pigweed.dev/pw_transfer/#typescript>`_
163
164Log Viewer Component
165====================
166The NPM package also includes a log viewer component that can be embedded in any
167webapp. The component works with Pigweed's RPC stack out-of-the-box but also
168supports defining your own log source. See :ref:`module-pw_web-log-viewer` for
169component interaction details.
170
171The component is composed of the component itself and a log source. Here is a
172simple example app that uses a mock log source:
173
174.. code-block:: html
175
176   <div id="log-viewer-container"></div>
177   <script src="https://unpkg.com/pigweedjs/dist/logging.umd.js"></script>
178   <script>
179
180     const { createLogViewer, MockLogSource } = PigweedLogging;
181     const logSource = new MockLogSource();
182     const containerEl = document.querySelector(
183       '#log-viewer-container'
184     );
185
186     let unsubscribe = createLogViewer(logSource, containerEl);
187     logSource.start(); // Start producing mock logs
188
189   </script>
190
191The code above will render a working log viewer that just streams mock
192log entries.
193
194It also comes with an RPC log source with support for detokenization. Here is an
195example app using that:
196
197.. code-block:: html
198
199   <div id="log-viewer-container"></div>
200   <script src="https://unpkg.com/pigweedjs/dist/index.umd.js"></script>
201   <script src="https://unpkg.com/pigweedjs/dist/protos/collection.umd.js"></script>
202   <script src="https://unpkg.com/pigweedjs/dist/logging.umd.js"></script>
203   <script>
204
205     const { Device, pw_tokenizer } = Pigweed;
206     const { ProtoCollection } = PigweedProtoCollection;
207     const { createLogViewer, PigweedRPCLogSource } = PigweedLogging;
208
209     const device = new Device(new ProtoCollection());
210     const logSource = new PigweedRPCLogSource(device, "CSV TOKEN DB HERE");
211     const containerEl = document.querySelector(
212       '#log-viewer-container'
213     );
214
215     let unsubscribe = createLogViewer(logSource, containerEl);
216
217   </script>
218
219Custom Log Source
220-----------------
221You can define a custom log source that works with the log viewer component by
222just extending the abstract `LogSource` class and emitting the `logEntry` events
223like this:
224
225.. code-block:: typescript
226
227   import { LogSource, LogEntry, Level } from 'pigweedjs/logging';
228
229   export class MockLogSource extends LogSource {
230     constructor(){
231       super();
232       // Do any initializations here
233       // ...
234       // Then emit logs
235       const log1: LogEntry = {
236
237       }
238       this.publishLogEntry({
239         level: Level.INFO,
240         timestamp: new Date(),
241         fields: [
242           { key: 'level', value: level }
243           { key: 'timestamp', value: new Date().toISOString() },
244           { key: 'source', value: "LEFT SHOE" },
245           { key: 'message', value: "Running mode activated." }
246         ]
247       });
248     }
249   }
250
251After this, you just need to pass your custom log source object
252to `createLogViewer()`. See implementation of
253`PigweedRPCLogSource <https://cs.opensource.google/pigweed/pigweed/+/main:ts/logging_source_rpc.ts>`_
254for reference.
255
256Column Order
257------------
258Column Order can be defined on initialization with the optional ``columnOrder`` parameter.
259Only fields that exist in the Log Source will render as columns in the Log Viewer.
260
261.. code-block:: typescript
262
263   createLogViewer(logSource, root, { columnOrder })
264
265``columnOrder`` accepts an ``string[]`` and defaults to ``[log_source, time, timestamp]``
266
267.. code-block:: typescript
268
269   createLogViewer(
270    logSource,
271    root,
272    { columnOrder: ['log_source', 'time', 'timestamp'] }
273
274  )
275
276Note, columns will always start with ``level`` and end with ``message``, these fields do not need to be defined.
277Columns are ordered in the following format:
278
2791. ``level``
2802. ``columnOrder``
2813. Fields that exist in Log Source but not listed will be added here.
2824. ``message``
283
284
285Accessing and Modifying Log Views
286---------------------------------
287
288It can be challenging to access and manage log views directly through JavaScript or HTML due to the
289shadow DOM boundaries generated by custom elements. To facilitate this, the ``Log Viewer``
290component has a public property, ``logViews``, which returns an array containing all child log
291views. Here is an example that modifies the ``viewTitle`` and ``searchText`` properties of two log
292views:
293
294.. code-block:: typescript
295
296   const logViewer = containerEl.querySelector('log-viewer');
297   const views = logViewer?.logViews;
298
299   if (views) {
300     views[0].viewTitle = 'Device A Logs';
301     views[0].searchText = 'device:A';
302
303     views[1].viewTitle = 'Device B Logs';
304     views[1].searchText = 'device:B';
305   }
306
307Alternatively, you can define a state object containing nodes with their respective properties and
308pass this state object to the ``Log Viewer`` during initialization. Here is how you can achieve
309that:
310
311.. code-block:: typescript
312
313   const childNodeA: ViewNode = new ViewNode({
314     type: NodeType.View,
315     viewTitle: 'Device A Logs',
316     searchText: 'device:A'
317   });
318
319   const childNodeB: ViewNode = new ViewNode({
320     type: NodeType.View,
321     viewTitle: 'Device B Logs',
322     searchText: 'device:B'
323   });
324
325   const rootNode: ViewNode = new ViewNode({
326     type: NodeType.Split,
327     orientation: Orientation.Vertical,
328     children: [childNodeA, childNodeB]
329   });
330
331   const options = { state: { rootNode: rootNode } };
332   createLogViewer(logSources, containerEl, options);
333
334Note that the relevant types and enums should be imported from
335``log-viewer/src/shared/view-node.ts``.
336
337Color Scheme
338------------
339The log viewer web component provides the ability to set the color scheme
340manually, overriding any default or system preferences.
341
342To set the color scheme, first obtain a reference to the ``log-viewer`` element
343in the DOM. A common way to do this is by using ``querySelector()``:
344
345.. code-block:: javascript
346
347   const logViewer = document.querySelector('log-viewer');
348
349You can then set the color scheme dynamically by updating the component's
350`colorScheme` property or by setting a value for the `colorscheme` HTML attribute.
351
352.. code-block:: javascript
353
354   logViewer.colorScheme = 'dark';
355
356.. code-block:: javascript
357
358   logViewer.setAttribute('colorscheme', 'dark');
359
360The color scheme can be set to ``'dark'``, ``'light'``, or the default ``'auto'``
361which allows the component to adapt to the preferences in the operating system
362settings.
363
364Material Icon Font (Subsetting)
365-------------------------------
366.. inclusive-language: disable
367
368The Log Viewer uses a subset of the Material Symbols Rounded icon font fetched via the `Google Fonts API <https://developers.google.com/fonts/docs/css2#forming_api_urls>`_. However, we also provide a subset of this font for offline usage at `GitHub <https://github.com/google/material-design-icons/blob/master/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.woff2>`_
369with codepoints listed in the `codepoints <https://github.com/google/material-design-icons/blob/master/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.codepoints>`_ file.
370
371(It's easiest to look up the codepoints at `fonts.google.com <https://fonts.google.com/icons?selected=Material+Symbols+Rounded>`_ e.g. see
372the sidebar shows the Codepoint for `"home" <https://fonts.google.com/icons?selected=Material+Symbols+Rounded:home:FILL@0;wght@0;GRAD@0;opsz@NaN>`_ is e88a).
373
374The following icons with codepoints are curently used:
375
376* delete_sweep e16c
377* error e000
378* warning f083
379* cancel e5c9
380* bug_report e868
381* info e88e
382* view_column e8ec
383* brightness_alert f5cf
384* wrap_text e25b
385* more_vert e5d4
386* play_arrow e037
387* stop e047
388
389To save load time and bandwidth, we provide a pre-made subset of the font with
390just the codepoints we need, which reduces the font size from 3.74MB to 12KB.
391
392We use fonttools (https://github.com/fonttools/fonttools) to create the subset.
393To create your own subset, find the codepoints you want to add and:
394
3951. Start a python virtualenv and install fonttools
396
397.. code-block:: bash
398
399   virtualenv env
400   source env/bin/activate
401   pip install fonttools brotli
402
4032. Download the the raw `MaterialSybmolsRounded woff2 file <https://github.com/google/material-design-icons/tree/master/variablefont>`_
404
405.. code-block:: bash
406
407   # line below for example, the url is not stable: e.g.
408   curl -L -o MaterialSymbolsRounded.woff2 \
409     "https://github.com/google/material-design-icons/raw/master/variablefont/MaterialSymbolsRounded%5BFILL,GRAD,opsz,wght%5D.woff2"
410
4113. Run fonttools, passing in the unicode codepoints of the necessary glyphs.
412   (The points for letters a-z, numbers 0-9 and underscore character are
413   necessary for creating ligatures)
414
415.. warning::  Ensure there are no spaces in the list of codepoints.
416.. code-block:: bash
417
418   fonttools subset MaterialSymbolsRounded.woff2 \
419      --unicodes=5f-7a,30-39,e16c,e000,e002,e8b2,e5c9,e868,,e88e,e8ec,f083,f5cf,e25b,e5d4,e037,e047 \
420      --no-layout-closure \
421      --output-file=material_symbols_rounded_subset.woff2 \
422      --flavor=woff2
423
4244. Update ``material_symbols_rounded_subset.woff2`` in ``log_viewer/src/assets``
425   with the new subset
426
427.. inclusive-language: enable
428
429Shoelace
430--------
431We currently use Split Panel from the `Shoelace <https://github.com/shoelace-style/shoelace>`_
432library to enable resizable split views within the log viewer.
433
434To provide flexibility in different environments, we've introduced a property ``useShoelaceFeatures``
435in the ``LogViewer`` component. This flag allows developers to enable or disable the import and
436usage of Shoelace components based on their needs.
437
438By default, the ``useShoelaceFeatures`` flag is set to ``true``, meaning Shoelace components will
439be used and resizable split views are made available. To disable Shoelace components, set this
440property to ``false`` as shown below:
441
442.. code-block:: javascript
443
444   const logViewer = document.querySelector('log-viewer');
445   logViewer.useShoelaceFeatures = false;
446
447When ``useShoelaceFeatures`` is set to ``false``, the  <sl-split-panel> component from Shoelace will
448not be imported or used within the log viewer.
449
450Guides
451======
452
453.. toctree::
454  :maxdepth: 1
455
456  testing
457  log_viewer
458  repl
459