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