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