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