<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lars Knudsen 🇩🇰</title>
    <description>The latest articles on DEV Community by Lars Knudsen 🇩🇰 (@denladeside).</description>
    <link>https://dev.to/denladeside</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F134608%2F62608bd7-886f-43ee-b9e8-c41a41a4a78e.jpg</url>
      <title>DEV Community: Lars Knudsen 🇩🇰</title>
      <link>https://dev.to/denladeside</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/denladeside"/>
    <language>en</language>
    <item>
      <title>Using Gemini for Web Apps</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Tue, 23 Sep 2025 21:45:00 +0000</pubDate>
      <link>https://dev.to/denladeside/using-gemini-for-web-apps-7fn</link>
      <guid>https://dev.to/denladeside/using-gemini-for-web-apps-7fn</guid>
      <description>&lt;p&gt;For many years, I've been developing web applications using the web platform as is.&lt;/p&gt;

&lt;p&gt;Recently, as many others, I've looked at what possibilities would be to use AI to help generate templates, components and even whole apps.&lt;/p&gt;

&lt;p&gt;In this post, I've decided to let &lt;a href="https://gemini.google.com/" rel="noopener noreferrer"&gt;Google's Gemini&lt;/a&gt; generate everything for a small web demo, using web components and see how it goes.&lt;/p&gt;

&lt;h1&gt;
  
  
  A simple data set
&lt;/h1&gt;

&lt;p&gt;For data, I decided to ask Gemini to generate a JSON file containing a structured JSON of the top 100 classic novels:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prompt: "Create a structured JSON file, containing a list of the top 100 classic novels, including author, title, release year and a short synopsis"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Within seconds, I received a JSON containing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"classic_novels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Homer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Odyssey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"release_year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"synopsis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The epic tale of Odysseus, king of Ithaca, and his ten-year journey home after the Trojan War, facing mythical creatures and wrathful gods."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mary Shelley"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Frankenstein"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"release_year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1818&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"synopsis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A young scientist, Victor Frankenstein, creates a sapient creature in an unorthodox scientific experiment, leading to tragic consequences for both."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane Austen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pride and Prejudice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"release_year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1813&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"synopsis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The story of Elizabeth Bennet, who navigates issues of manners, upbringing, morality, education, and marriage in the society of the landed gentry of early 19th-century England."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"George Orwell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1984"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"release_year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1949&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"synopsis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In a dystopian superstate ruled by the totalitarian Party, Winston Smith secretly defies the regime, an act that leads him to question everything he knows."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Harper Lee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"To Kill a Mockingbird"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"release_year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1960&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"synopsis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Narrated by a young girl named Scout Finch, this novel explores themes of racial injustice and the loss of innocence in the American South during the Great Depression."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Making it visible
&lt;/h1&gt;

&lt;p&gt;Then Asking Gemini to generate a standalone HTML page, where it's possible to load the JSON and show it using only web standards, again within seconds, the output was presented:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prompt: "now make a standalone web page (HTML), where it's possible to load this file and display it in a table, where it's possible for the user to sort by author, title or release year. No external dependencies should be used and the items should be shown as a card, rendered like an old school library card - using web components but only using pure web standards"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjizf23kr4oh0iumdyko1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjizf23kr4oh0iumdyko1.png" alt="Gemini HTML1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The web view:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1vtqsqeucie7epfhbfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1vtqsqeucie7epfhbfs.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sorting works as expected - very cool :)&lt;/p&gt;

&lt;p&gt;I then asked Gemini to modify the HTML to add a view transition on clicking a card to bring it into focus and again, within seconds the result was there:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prompt: "can you change the html, so when a card is clicked, it will make a view transition to fill the screen and when clicked again, it will go back in the list"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/PeFyCgTvWUs"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h1&gt;
  
  
  Looking at the code
&lt;/h1&gt;

&lt;p&gt;When looking through the produced HTML, it's nice to see how comments explain the reasoning behind functionality that I didn't explicitly ask for, but makes sense in the context of - in this case - visualization of publishing year:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Helper to format year (e.g., -800 -&amp;gt; 800 BCE)&lt;/span&gt;
&lt;span class="nf"&gt;formatYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; BCE`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's also nice to see how Gemini has chosen to make a sort function to handle both text (title and author) and numbers (publishing year):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sortData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;novelsData&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;novelsData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;novelsData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Case-insensitive string comparison&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Numeric comparison&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For browsers not supporting the View Transition API, there is even a fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cardContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clickedCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;library-card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;clickedCard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isFocused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clickedCard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Use the View Transitions API&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;toggleFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clickedCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;toggleFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clickedCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In general, the produced JavaScript, CSS and HTML gets the job done and I am impressed.&lt;/p&gt;

&lt;h1&gt;
  
  
  Making a game
&lt;/h1&gt;

&lt;p&gt;After seeing how well Gemini handled the task, I wanted to give it a slightly harder challenge: To create a small game with keyboard input, audio and all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prompt: "Using pure web standards, create a small game in a standalone HTML file, where it's possible to control a car seen from above, using the arrow keys on the keyboard. Also include realistic generated sound effects using web audio. It should be possible to "drive" the car around on the screen, collecting coins randomly placed. There should be a score number accumulated in the top right corner"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Within seconds, it delivered:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/_MVXFKKE5-k"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE: Sound was not captured in the video, but you can try out the game &lt;a href="https://larsgk.github.io/geminiwebtest/car_game.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;It was quite fun and impressive to see how easy it was to create these fully functional web demos with a few simple requests to Gemini. Even if the output may not be exactly what was intended, the code is well structured and easy to read and modify. &lt;/p&gt;

&lt;p&gt;The code produced can be found &lt;a href="https://github.com/larsgk/geminiwebtest" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enjoy :) &lt;/p&gt;

</description>
      <category>web</category>
      <category>gemini</category>
      <category>ai</category>
    </item>
    <item>
      <title>Web NFC and Broadcast Audio URIs</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Sun, 22 Jun 2025 12:09:52 +0000</pubDate>
      <link>https://dev.to/denladeside/web-nfc-and-broadcast-audio-uris-1oh5</link>
      <guid>https://dev.to/denladeside/web-nfc-and-broadcast-audio-uris-1oh5</guid>
      <description>&lt;h1&gt;
  
  
  Reducing Connection Friction
&lt;/h1&gt;

&lt;p&gt;In a future world filled with wireless audio broadcasts, from public announcements at airports to silent TVs at the gym, finding and connecting to the right audio source can be a clumsy process. Remember, an Auracast™ assistant's job is to scan for broadcasts and provide a UI for users to join them. But how does the assistant know which of the dozens of potential broadcasts is the &lt;em&gt;right one&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;One elegant solution is the Broadcast Audio URI (BAU), a standardized link format designed to share information about an audio broadcast. While my previous posts have shown how this can be shared via QR codes, but here we will explore how a similar experience could be made, using Near Field Communication (NFC) and the Web NFC API.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Thanks to my friend &lt;a href="https://www.linkedin.com/in/kenneth-rohde-christiansen/" rel="noopener noreferrer"&gt;Kenneth&lt;/a&gt; (link) who introduced me to Web NFC, where he was one of the main &lt;a href="https://w3c.github.io/web-nfc/#acknowledgments" rel="noopener noreferrer"&gt;contributors&lt;/a&gt; to the spec.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine this: you walk up to a screen, tap your phone on a small sticker, and your earbuds are instantly tuned to the correct audio stream.&lt;/p&gt;

&lt;h1&gt;
  
  
  A Quick Recap: The Broadcast Audio URI (BAU)
&lt;/h1&gt;

&lt;p&gt;Before we look at the code, let's remember what a BAU is. It's a string that starts with &lt;code&gt;BLUETOOTH:UUID:184F&lt;/code&gt; and contains key-value pairs with information like the Broadcast ID (&lt;code&gt;BI&lt;/code&gt;), device address (&lt;code&gt;AD&lt;/code&gt;), and the Broadcast Name (&lt;code&gt;BN&lt;/code&gt;). This gives an assistant all the information it needs to find and synchronize to a specific broadcast stream.&lt;/p&gt;

&lt;p&gt;A typical BAU might look like this:&lt;br&gt;
&lt;code&gt;BLUETOOTH:UUID:184F;BN:UHVibGljIE5ld3M=;BI:DE51E9;...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This tiny piece of text is the key to unlocking a specific audio broadcast. Now, let's put it on an NFC tag.&lt;/p&gt;
&lt;h1&gt;
  
  
  Reading the URI with Web NFC
&lt;/h1&gt;

&lt;p&gt;The Web NFC API allows a web application, with the user's permission, to read from and write to NFC tags.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: In this post, we'll just cover how to the &lt;em&gt;read&lt;/em&gt; the NFC tag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The process involves two main steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Initiating a scan and waiting for the user to tap a tag.&lt;/li&gt;
&lt;li&gt; Handling the &lt;code&gt;reading&lt;/code&gt; event to process the data from the tag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's build a simple function within a hypothetical web-based Auracast™ Assistant. This function will be triggered by a "Scan NFC" button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scanForBroadcastURI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ndef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NDEFReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User activated NFC scan...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Start scanning for NFC tags&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ndef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFC Scan started. Please tap a tag.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add a one-time event listener for the 'reading' event&lt;/span&gt;
    &lt;span class="nx"&gt;ndef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFC tag read.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Iterate over the records in the message&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textDecoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textDecoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

          &lt;span class="c1"&gt;// Check if it's a valid Broadcast Audio URI&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BLUETOOTH:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Broadcast Audio URI found: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// Pass the URI to the assistant's connection logic&lt;/span&gt;
            &lt;span class="nf"&gt;tuneToBroadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// We found what we needed, no need to process other records&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Use { once: true } to automatically stop listening after one read&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`NFC Scan failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tuneToBroadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// This is where the assistant's logic would parse the URI&lt;/span&gt;
  &lt;span class="c1"&gt;// and instruct the underlying Bluetooth hardware to sync&lt;/span&gt;
  &lt;span class="c1"&gt;// to the broadcast source.&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Tuning to broadcast with data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// e.g., connectToAuracast(parseBAU(uri));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this code, we start a scan and listen for a &lt;code&gt;reading&lt;/code&gt; event. When a tag is successfully read, we decode its payload, check if there is a Broadcast Audio URI in the records, and then pass it off to another function to tune into the broadcast.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Web NFC User Experience
&lt;/h1&gt;

&lt;p&gt;With this implementation, the user journey becomes incredibly simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The user opens our web-based Broadcast Assistant on their phone.&lt;/li&gt;
&lt;li&gt; They press a "Scan NFC" button, which calls the &lt;code&gt;scanForBroadcastURI()&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt; The user taps their phone against an NFC tag placed near the audio source (e.g., on a museum exhibit, a TV screen, or a conference hall door).&lt;/li&gt;
&lt;li&gt; The web app instantly reads the Broadcast Audio URI and automatically tunes the user's connected earbuds to the correct stream.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;By combining the simple, physical interaction of NFC with the standardized data format of the Broadcast Audio URI, we can create a powerful and intuitive user experience for Auracast™. The Web NFC API provides the final, critical piece of the puzzle, allowing developers to build these advanced, hardware-integrated experiences directly for the web platform.&lt;/p&gt;

&lt;p&gt;This is yet another testament to how the modern web is closing the gap, enabling interactions that were once the exclusive domain of native applications.&lt;/p&gt;

&lt;p&gt;Enjoy ;)&lt;/p&gt;

</description>
      <category>web</category>
      <category>nfc</category>
      <category>auracast</category>
    </item>
    <item>
      <title>A Web based Broadcast Assistant</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Thu, 06 Mar 2025 07:30:00 +0000</pubDate>
      <link>https://dev.to/denladeside/a-web-based-broadcast-assistant-cld</link>
      <guid>https://dev.to/denladeside/a-web-based-broadcast-assistant-cld</guid>
      <description>&lt;h1&gt;
  
  
  Broadcast Assistants
&lt;/h1&gt;

&lt;p&gt;From: the &lt;a href="https://www.bluetooth.com/" rel="noopener noreferrer"&gt;Bluetooth SIG&lt;/a&gt; &lt;a href="https://www.bluetooth.com/auracast/how-it-works/" rel="noopener noreferrer"&gt;page on Auracast&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Auracast™ assistants scan for Auracast™ advertisements and provide a user interface (UI) to enable users to select an Auracast™ broadcast to join, similar to the UI commonly used to connect to Wi-Fi networks in public spaces.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the most normal setup, available in the market today, a user would need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pair of Auracast enabled earbuds (broadcast sinks)&lt;/li&gt;
&lt;li&gt;A phone with Auracast assistant functionality&lt;/li&gt;
&lt;li&gt;Being near one or more Auracast transmitters (broadcast sources)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the number of phones including assistants is very limited at the moment - and those assistants having very limited functionality - it could be interesting to try to &lt;a href="https://www.bluetooth.com/bluetooth-resources/how-to-build-an-auracast-assistant/" rel="noopener noreferrer"&gt;build one of our own&lt;/a&gt; to explore this new and interesting technology.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: For more details on this and LE Audio Broadcast and Unicast in general, Nick Hunn wrote an excellent book on LE Audio: &lt;a href="https://www.bluetooth.com/bluetooth-resources/le-audio-book-v2/" rel="noopener noreferrer"&gt;Introducing Bluetooth LE Audio, 2nd Edition&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Limitations in Android
&lt;/h1&gt;

&lt;p&gt;Since Android 13, 3rd party applications &lt;a href="https://android-review.googlesource.com/c/platform/packages/apps/Bluetooth/+/1846281" rel="noopener noreferrer"&gt;have been prohibited from accessing LE Audio related Bluetooth GATT Services&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Looking at the &lt;a href="https://android-review.googlesource.com/c/platform/packages/apps/Bluetooth/+/1846281/1/src/com/android/bluetooth/gatt/GattService.java#132" rel="noopener noreferrer"&gt;list of these services&lt;/a&gt;, we see that the &lt;a href="https://www.bluetooth.com/specifications/specs/broadcast-audio-scan-service-2/" rel="noopener noreferrer"&gt;Broadcast Audio Scan Service (BASS)&lt;/a&gt; is included as one of the restricted services.&lt;/p&gt;

&lt;p&gt;This means that if we want to create an application that acts as a broadcast assistant on a modern Android phone, we need to look for another solution.&lt;/p&gt;

&lt;h1&gt;
  
  
  Web and Zephyr for the Win!
&lt;/h1&gt;

&lt;p&gt;Luckily, &lt;a href="https://developer.chrome.com/docs/capabilities/usb" rel="noopener noreferrer"&gt;WebUSB&lt;/a&gt;, that works on &lt;a href="https://caniuse.com/?search=webusb" rel="noopener noreferrer"&gt;both mobile and desktop browsers&lt;/a&gt;, can be used to communicate with external hardware doing all the Bluetooth LE communication (unrestricted), while a nice and functioning UI can then be made as a pure web application.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WebUSB is part of &lt;a href="https://developer.chrome.com/docs/capabilities" rel="noopener noreferrer"&gt;Project Fugu (Web Capabilities)&lt;/a&gt;, consisting of a range of Web APIs, narrowing the gap between native apps and web. To learn more, check out this great showcase of what it can be used for: &lt;a href="https://developer.chrome.com/docs/capabilities/fugu-showcase" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/capabilities/fugu-showcase&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As mentioned in &lt;a href="https://dev.to/denladeside/a-simple-broadcast-audio-source-4e8b"&gt;an earlier post on building a broadcast audio source&lt;/a&gt;, the &lt;a href="https://zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt; contains some awesome &lt;a href="https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/bluetooth" rel="noopener noreferrer"&gt;Bluetooth LE Audio related samples&lt;/a&gt;.  It also contains a Bluetooth LE Audio stack with &lt;a href="https://docs.zephyrproject.org/4.0.0/doxygen/html/group__bt__bap.html#gac93cb4cab33d0b937e752bc0b71cad9e" rel="noopener noreferrer"&gt;great API support&lt;/a&gt; available to build a functioning broadcast assistant.&lt;/p&gt;

&lt;p&gt;Again, using the &lt;a href="https://www.nordicsemi.com/" rel="noopener noreferrer"&gt;Nordic Semiconductor&lt;/a&gt; &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle" rel="noopener noreferrer"&gt;nRF52840 Dongle&lt;/a&gt; for the Zephyr firmware part of the application, allows the user to get broadcast assistant functionality on any Android phone with a fairly recent browser and a small dongle attached via USB.&lt;/p&gt;

&lt;h1&gt;
  
  
  Modern Web
&lt;/h1&gt;

&lt;p&gt;We decided to build the web application, using pure vanilla web technology (no frameworks) as things like &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components" rel="noopener noreferrer"&gt;Web Components&lt;/a&gt;, which is &lt;a href="https://caniuse.com/?search=web%20components" rel="noopener noreferrer"&gt;supported in all major browsers&lt;/a&gt;, make it very easy to build UI elements.&lt;/p&gt;

&lt;p&gt;Also, being able to make custom classes, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/EventTarget" rel="noopener noreferrer"&gt;extending EventTarget&lt;/a&gt;, &lt;a href="https://dev.to/denladeside/web-platform-for-enterprise-applications-406p"&gt;eliminates the need&lt;/a&gt; for any special framework based event system.&lt;/p&gt;

&lt;p&gt;We also decided not to "build" (or minify) the code, as we would then be able to run the application directly from the source "as is", either using a local web server, or when &lt;a href="https://dev.toastraeusLabs/web-broadcast-assistant"&gt;hosted from github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some of the web technologies used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://kinsta.com/blog/web-components/" rel="noopener noreferrer"&gt;Web Components&lt;/a&gt;  for UI elements&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebUSB_API" rel="noopener noreferrer"&gt;WebUSB&lt;/a&gt; to connect to hardware&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web.dev/articles/web-share" rel="noopener noreferrer"&gt;Web Share API&lt;/a&gt; to share details of detected sources&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/capabilities/shape-detection#barcodedetector" rel="noopener noreferrer"&gt;Barcode Detection API&lt;/a&gt; to read &lt;a href="https://dev.to/denladeside/broadcast-audio-uri-1kkd"&gt;Broadcast Audio URIs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Using the Web Broadcast Assistant
&lt;/h1&gt;

&lt;p&gt;When the web application is loaded, it prompts the user to connect to a USB device, running the assistant firmware, made with the &lt;a href="https://zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqjmgaap799mzcd0z3cs.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqjmgaap799mzcd0z3cs.jpeg" alt="Init app" width="800" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click connect and select the device:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxjwkyn83uzwu31u7zcg.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxjwkyn83uzwu31u7zcg.jpeg" alt="Select device" width="800" height="1083"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And after granting permission to connect, the app should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3wqii8uf9ctxtgyok65.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3wqii8uf9ctxtgyok65.jpeg" alt="WebUSB connected" width="800" height="678"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Let's play some music!
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Here, we will use the broadcast source described in detail in &lt;a href="https://dev.to/denladeside/a-simple-broadcast-audio-source-4e8b"&gt;this previous post&lt;/a&gt;, where audio data is stored in flash on an &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle" rel="noopener noreferrer"&gt;nRF52840 Dongle&lt;/a&gt;, playing forever in a loop.&lt;/p&gt;

&lt;p&gt;As a broadcast audio receiver, a pair of Samsung Galaxy Buds2 Pro are used.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, scan for Auracast sources:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftlwsfxsjbekbynnvmjbz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftlwsfxsjbekbynnvmjbz.jpeg" alt="Scan sources" width="800" height="856"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then for receiver devices:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8ud8og83obz71v4wf6o.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8ud8og83obz71v4wf6o.jpeg" alt="Scan sinks" width="800" height="1109"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Connect the earbuds:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fka7cjbfqik1y60ezh5wy.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fka7cjbfqik1y60ezh5wy.jpeg" alt="Connect sinks" width="800" height="1123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And press play to listen to the broadcast source in the connected earbuds:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx63bxm40fuejyae879a.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx63bxm40fuejyae879a.jpeg" alt="Playing" width="800" height="1134"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Multiple subgroup support
&lt;/h1&gt;

&lt;p&gt;The broadcast source used above is a simple, single-subgroup source, but as described in &lt;a href="https://dev.to/denladeside/auracast-and-multiple-subgroups-4im3"&gt;this previous post&lt;/a&gt;, the &lt;a href="https://zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt; stack supports multiple subgroups for both sources (transmitters), sinks (receivers) and assistants (check the BAP broadcast samples &lt;a href="https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/bluetooth" rel="noopener noreferrer"&gt;here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Let's try to scan for sources, using the multi language source described &lt;a href="https://dev.to/denladeside/auracast-and-multiple-subgroups-4im3"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When the web assistant discovers a broadcast source with multiple subgroups, an extra selection layer becomes available:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0autus70k6kpoyf1vq04.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0autus70k6kpoyf1vq04.jpeg" alt="Multi select 1" width="800" height="1111"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, if you would like to hear the Spanish stream, just select it and press play:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtn3fje9m663e2niyk9b.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtn3fje9m663e2niyk9b.jpeg" alt="Multi select 4" width="800" height="1112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, the earbuds used to listen to the single-subgroup source above, don't seem to support multiple subgroups yet (the sound doesn't seem to play at least - maybe with a future firmware update?).&lt;/p&gt;

&lt;p&gt;However, if you want to try out a working setup, the &lt;a href="https://docs.zephyrproject.org/latest/samples/bluetooth/bap_broadcast_sink/README.html#bluetooth_bap_broadcast_sink" rel="noopener noreferrer"&gt;BAP Broadcast Sink sample&lt;/a&gt; &lt;em&gt;does&lt;/em&gt; actually support multiple subgroups. But that's a topic for another post.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Combining the &lt;a href="https://zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt; stack, running on an affordable &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle" rel="noopener noreferrer"&gt;nRF52840 Dongle&lt;/a&gt; with the power of modern web technologies turned out quite well and it has also allowed us to experiment with multiple subgroups, supported by the specs, but not yet by many devices in the market (at the time of writing at least - be sure to keep an eye out for that!).&lt;/p&gt;

</description>
      <category>web</category>
      <category>zephyr</category>
      <category>auracast</category>
    </item>
    <item>
      <title>Auracast and multiple subgroups</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Tue, 04 Mar 2025 01:10:00 +0000</pubDate>
      <link>https://dev.to/denladeside/auracast-and-multiple-subgroups-4im3</link>
      <guid>https://dev.to/denladeside/auracast-and-multiple-subgroups-4im3</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;In &lt;a href="https://dev.to/denladeside/a-simple-broadcast-audio-source-4e8b"&gt;this previous post&lt;/a&gt; on broadcast sources, we covered how to create a simple BAP broadcast source using &lt;a href="https://zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt; and a &lt;a href="https://www.nordicsemi.com/" rel="noopener noreferrer"&gt;Nordic Semiconductor&lt;/a&gt; &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle" rel="noopener noreferrer"&gt;nRF52840 Dongle&lt;/a&gt;, broadcasting a single 16 kHz mono audio stream.&lt;/p&gt;

&lt;p&gt;In this post, we will explore how to extend that to a more complex source, broadcasting several "tracks" in parallel.&lt;/p&gt;

&lt;h1&gt;
  
  
  BASE (Broadcast Audio Source Endpoint)
&lt;/h1&gt;

&lt;p&gt;The &lt;a href="https://www.bluetooth.com/" rel="noopener noreferrer"&gt;Bluetooth&lt;/a&gt; &lt;a href="https://www.bluetooth.com/specifications/specs/bap-1-0-2/" rel="noopener noreferrer"&gt;Basic Audio Profile (BAP) spec&lt;/a&gt;, &lt;a href="https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/BAP_v1.0.2/BAP_v1.0.2/out/en/index-en.html#UUID-053eafce-d8aa-71ed-8474-949359f76a95" rel="noopener noreferrer"&gt;section 3.7.2.2&lt;/a&gt; describes how to create a BASE (Broadcast Audio Source Endpoint) for the..&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Broadcast source (Level 1 - BIG), which can contain..&lt;/li&gt;
&lt;li&gt;One or more parallel groups (Level 2 - Subgroups) containing..&lt;/li&gt;
&lt;li&gt;One or more streaming channels (Level 3 - BIS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...with the possibility to specify various combinations of codec configuration and metadata on level 2 and 3.&lt;/p&gt;

&lt;p&gt;The most common case, would be e.g. a TV or phone, broadcasting a simple stereo signal (one subgroup containing two streams) with some added metadata, e.g. the title of what's currently being broadcasted (in the &lt;em&gt;ProgramInfo&lt;/em&gt; metadata field of the subgroup).&lt;/p&gt;

&lt;p&gt;This is fine for simple streaming, but what if we could utilize the flexibility of the BASE structure to create broadcast sources covering some more interesting cases?&lt;/p&gt;

&lt;h1&gt;
  
  
  Multiple subgroups
&lt;/h1&gt;

&lt;p&gt;One obvious case that could be interesting to look at, is a multi-language source, e.g. to be used for tourist attractions.&lt;/p&gt;

&lt;p&gt;In this case, mono-channel audio (for each language) with 16 kHz sample rate should be sufficient and this would also allow for 5-7 "stream tracks" (subgroups with language metadata containing one mono stream each) to fit both in the BASE structure and airtime wise (the amount of data possible to send in a given timeframe has physical limitations).&lt;/p&gt;

&lt;p&gt;Another interesting case, could be to have two subgroups with different sample rates, but otherwise same streaming content.  This would allow for high-powered receivers to receive and playback high quality audio (e.g. 48 kHz), while more resource constrained devices (like hearing aids) to receive and playback standard quality audio (e.g. 24 kHz) - at the same time, from the same broadcast source.&lt;/p&gt;

&lt;h1&gt;
  
  
  Building with Zephyr
&lt;/h1&gt;

&lt;p&gt;In order to explore these cases, a variant of the &lt;a href="https://github.com/larsgk/fixed-source" rel="noopener noreferrer"&gt;simple broadcast application&lt;/a&gt; used in a &lt;a href="https://dev.to/denladeside/a-simple-broadcast-audio-source-4e8b"&gt;previous post&lt;/a&gt; was created: &lt;a href="https://github.com/AstraeusLabs/multi-source" rel="noopener noreferrer"&gt;https://github.com/AstraeusLabs/multi-source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Different from the simple broadcast application, this one utilizes a range of pre-encoded, 1s sine wave audio snippets in 16/24/48 kHz sample rate and for each sample rate, various sine frequencies.  This allows us to create a wide variety of combinations of subgroups and streams from the same application source.&lt;/p&gt;

&lt;h1&gt;
  
  
  5 different language tracks
&lt;/h1&gt;

&lt;p&gt;In order to explore the first case with multiple languages, a customized source is created, containing 5 subgroups (distinguishable by different &lt;em&gt;Language&lt;/em&gt; specified in the metadata for each subgroup), each containing a single mono channel stream.&lt;/p&gt;

&lt;p&gt;Some highlights from the code to setup the broadcast source include the languages set for subgroups (English, German, French, Spanish and Italian):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_audio_codec_cfg&lt;/span&gt; &lt;span class="n"&gt;subgroup_codec_cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"eng"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"deu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"fra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"spa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"ita"&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;setup_broadcast_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_bap_broadcast_source&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_bap_broadcast_source_stream_param&lt;/span&gt;
        &lt;span class="n"&gt;stream_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_bap_broadcast_source_subgroup_param&lt;/span&gt;
        &lt;span class="n"&gt;subgroup_param&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_bap_broadcast_source_param&lt;/span&gt; &lt;span class="n"&gt;create_param&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0U&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ARRAY_SIZE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subgroup_param&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;subgroup_codec_cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;preset_16_mono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;codec_cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_audio_codec_cfg&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;bt_audio_codec_cfg_meta_set_lang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;subgroup_codec_cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="cm"&gt;/* MONO is implicit if omitted */&lt;/span&gt;
        &lt;span class="n"&gt;bt_audio_codec_cfg_unset_val&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;subgroup_codec_cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;BT_AUDIO_CODEC_CFG_CHAN_ALLOC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;subgroup_param&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;params_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;subgroup_param&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stream_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;subgroup_param&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;codec_cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;subgroup_codec_cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also assignment of different 1s, 16 kHz sample rate, pre-encoded sine waves of varying frequency to each mono stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0U&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ARRAY_SIZE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream_params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stream_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;stream_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;samples_per_frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;sdu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;preset_16_mono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sdu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;lc3_sine_0200_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;lc3_sine_0320_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;lc3_sine_0800_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;lc3_sine_1000_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;lc3_sine_1600_16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Reading LC3 header (%p)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"======================&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lc3bin_read_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;frame_us&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;srate_hz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;nchannels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;nsamples&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Frame size: %dus&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame_us&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sample rate: %dHz&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srate_hz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Number of channels: %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nchannels&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Number of samples: %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nsamples&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="cm"&gt;/* Store position of start and end+1 of frame blocks */&lt;/span&gt;
        &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;start_data_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;end_data_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data_ptr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nsamples&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;samples_per_frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sdu&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// TBD&lt;/span&gt;

        &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;sdu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sdu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;stream_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;bt_bap_stream_cb_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stream_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full source for this configuration can be found &lt;a href="https://github.com/AstraeusLabs/multi-source/blob/29fdc1105ff0116693ea33de86a141ce26b4ac20/app/src/main.c#L837-L937" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Capturing the streams with blueSpy
&lt;/h1&gt;

&lt;p&gt;As in the &lt;a href="https://dev.to/denladeside/a-simple-broadcast-audio-source-4e8b"&gt;previous post&lt;/a&gt;, let's try to capture the broadcast source streams using the RFcreations &lt;a href="https://www.rfcreations.com/hardware-platform/mini-moreph" rel="noopener noreferrer"&gt;mini-moreph&lt;/a&gt; together with the &lt;a href="https://www.rfcreations.com/software/bluespy" rel="noopener noreferrer"&gt;blueSpy software&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Immediately after starting the capture and selecting the broadcast device in the &lt;strong&gt;Filter Devices&lt;/strong&gt; panel, it's clear that we have successfully built a broadcast source with 5 streams of 16 kHz audio:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fom7cdnq99c1bijmsycmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fom7cdnq99c1bijmsycmu.png" alt="blueSpy" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/om7cdnq99c1bijmsycmu.png" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking closer at the &lt;strong&gt;Timeline&lt;/strong&gt; it's also clear that each stream contains slightly different frequency wave data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77p7g0iznfem062698y4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77p7g0iznfem062698y4.png" alt="Audio timeline" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/77p7g0iznfem062698y4.png" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using the live playback function in the &lt;strong&gt;Audio Export&lt;/strong&gt; for each stream also confirms this.&lt;/p&gt;

&lt;p&gt;Selecting one of the entries with BASE information in the &lt;strong&gt;Summary&lt;/strong&gt; panel and looking at the &lt;strong&gt;Details&lt;/strong&gt; tab on the right side of the screen also shows, that the BASE contains 5 subgroups with different language indications in the metadata:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvhnjfta0pd7wmpv7fv1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvhnjfta0pd7wmpv7fv1w.png" alt="BASE" width="800" height="1125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhnjfta0pd7wmpv7fv1w.png" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Even though the first Auracast capable devices available in the market, currently only seem to support the simple case of "1 subgroup with stereo audio", it's still worth exploring other use cases like the ones mentioned above.&lt;/p&gt;

&lt;p&gt;Also, it's really great to see that the RFcreations &lt;a href="https://www.rfcreations.com/hardware-platform/mini-moreph" rel="noopener noreferrer"&gt;mini-moreph&lt;/a&gt; and &lt;a href="https://www.rfcreations.com/software/bluespy" rel="noopener noreferrer"&gt;blueSpy software&lt;/a&gt; was able to capture and render this slightly more advanced source and that it was possible to build using &lt;a href="https://zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt; and the nRF52840 Dongle.&lt;/p&gt;

&lt;p&gt;The full repo containing a bunch of different source configurations can be found &lt;a href="https://github.com/AstraeusLabs/multi-source" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Enjoy ;)&lt;/p&gt;

</description>
      <category>auracast</category>
      <category>zephyr</category>
      <category>rfcreations</category>
    </item>
    <item>
      <title>A simple Broadcast Audio Source</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Mon, 03 Mar 2025 21:55:00 +0000</pubDate>
      <link>https://dev.to/denladeside/a-simple-broadcast-audio-source-4e8b</link>
      <guid>https://dev.to/denladeside/a-simple-broadcast-audio-source-4e8b</guid>
      <description>&lt;h1&gt;
  
  
  Broadcast Audio Transmitters and Receivers
&lt;/h1&gt;

&lt;p&gt;A simple BAP broadcast source (transmitter) that will be compatible with LE Audio broadcast receivers (e.g. some high-end earbuds available now) should advertise, including a field containing service data for the Broadcast Audio Announcement Service (0x1852) with a Broadcast ID as the data.&lt;br&gt;
It also needs to provide LC3 encoded audio (the actual data) in isochronous channels which the  periodic advertising train provides a pointer to.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;LC3 is the new improved audio codec providing better quality with lower bandwidth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Information about the source stream(s) is also provided through periodic advertising:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BIGinfo: Provides information needed by receivers to understand the stream (e.g. timings, possible encryption, etc.)&lt;/li&gt;
&lt;li&gt;BASE (Broadcast Audio Source Endpoint):  Contains information about the streams provided by the broadcast source, e.g. left/right channels, meta data like language and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a receiver is instructed to "tune in" to the broadcast (given Bluetooth LE address, Broadcast ID and Advertising Set ID), it will then be able to discover the correct broadcast source and synchronize to the broadcasted periodic advertising and eventually, decode and playback the audio broadcasted from the selected broadcast source (transmitter).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: This section only contains a very high level overview of the transmitter.  For more details, Nick Hunn wrote an excellent book on LE Audio: &lt;a href="https://www.bluetooth.com/bluetooth-resources/le-audio-book-v2/" rel="noopener noreferrer"&gt;Introducing Bluetooth LE Audio, 2nd Edition&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;
  
  
  Zephyr and LE Audio Broadcasting
&lt;/h1&gt;

&lt;p&gt;The &lt;a href="https://zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt; contains some great &lt;a href="https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/bluetooth" rel="noopener noreferrer"&gt;Bluetooth LE Audio related samples&lt;/a&gt;.  One of them is the &lt;a href="https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/bluetooth/bap_broadcast_source" rel="noopener noreferrer"&gt;Basic Audio Profile (BAP) Broadcast Source sample&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By default, this sample functions as a very simple broadcast transmitter, either transmitting (mock) sine wave audio or - if available and configured - functions as a USB sound card, transmitting audio provided through a USB interface (e.g. from a connected PC or phone).&lt;/p&gt;

&lt;p&gt;Although this works well for some setups, for some test and development scenarios, it would be practical to have an array of broadcast sources, broadcasting (audible) different audio with minimal configuration and hardware needed.&lt;/p&gt;
&lt;h1&gt;
  
  
  nRF52840 Dongle and pre-encoded audio
&lt;/h1&gt;

&lt;p&gt;As the sample already compiles and runs on the &lt;a href="https://www.nordicsemi.com/" rel="noopener noreferrer"&gt;Nordic Semiconductor&lt;/a&gt; &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle" rel="noopener noreferrer"&gt;nRF52840 Dongle&lt;/a&gt;, it seemed reasonable to investigate if this dongle could be used for the purpose.&lt;/p&gt;

&lt;p&gt;When building the &lt;a href="https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/bluetooth/bap_broadcast_source" rel="noopener noreferrer"&gt;&lt;code&gt;bap_broadcast_source&lt;/code&gt; sample&lt;/a&gt; for the nRF52840 (with 1024 kB flash), memory usage is the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory region         Used Size  Region Size  %age Used
           FLASH:      260908 B      1020 KB     24.98%
             RAM:       62284 B       256 KB     23.76%
        IDT_LIST:          0 GB        32 KB      0.00%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should leave around 750 kB of flash memory, which could be used to contain audio data.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;bap_broadcast_source&lt;/code&gt; sample supports on-the-fly encoding of uncompressed PCM audio samples to LC3 encoded packets for broadcasting.  But if we pre-encode the audio in LC3 format and store it in flash memory, we can eliminate the encoding step while also saving memory.&lt;/p&gt;

&lt;p&gt;Taking a closer look at the liblc3 module used to encode/decode audio in Zephyr, originating from &lt;a href="https://github.com/google/liblc3" rel="noopener noreferrer"&gt;https://github.com/google/liblc3&lt;/a&gt;, shows that it also contains practical command line tools to encode and decode *.wav files to/from LC3 format.&lt;/p&gt;

&lt;p&gt;Encoding a *.wav file, containing just above 2 minutes of 16 kHz, mono audio (&lt;strong&gt;input16KHz.wav&lt;/strong&gt; at &lt;strong&gt;4016868&lt;/strong&gt; bytes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.../liblc3/bin$ ./elc3 -b 32000 input16KHz.wav output16KHz.lc3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(32000 is the bitrate for 16 kHz LC3)&lt;/p&gt;

&lt;p&gt;The result in &lt;strong&gt;output16KHz.lc3&lt;/strong&gt; is &lt;strong&gt;527202&lt;/strong&gt; bytes of LC3 encoded audio data (including file and data packet headers), which is well below the limit of the 750 kB flash available.&lt;/p&gt;

&lt;p&gt;For more information and full source code, look &lt;a href="https://github.com/larsgk/fixed-source" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Capture with blueSpy
&lt;/h1&gt;

&lt;p&gt;In &lt;a href="https://dev.to/denladeside/capturing-the-perfect-radio-wave-16cl"&gt;an earlier post&lt;/a&gt;, I wrote about using the RFcreations &lt;a href="https://www.rfcreations.com/hardware-platform/mini-moreph" rel="noopener noreferrer"&gt;mini-moreph&lt;/a&gt; together with the &lt;a href="https://www.rfcreations.com/software/bluespy" rel="noopener noreferrer"&gt;blueSpy software&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This time, it could be interesting to see what data it is able to capture from the nRF52840 Dongle, broadcasting 16 kHz (mono channel) audio.&lt;/p&gt;

&lt;p&gt;Connecting the mini-moreph and starting capturing with the blueSpy software immediately finds the broadcast source with BT name &lt;strong&gt;Music&lt;/strong&gt; and Broadcast name &lt;strong&gt;Hold on a Sec&lt;/strong&gt;, which is chosen in the &lt;strong&gt;Filter Devices&lt;/strong&gt; tab to focus on the advertising sent from the broadcast source dongle.&lt;/p&gt;

&lt;p&gt;Looking at the timeline in the bottom, it's also possible to see the 16 kHz mono audio stream captured and correctly detected&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1twy6o9v0kd5h6frucg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1twy6o9v0kd5h6frucg.png" alt="blueSpy" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1twy6o9v0kd5h6frucg.png" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, if we select one of the entries in the summary pane, containing periodic advertising data and check out the &lt;strong&gt;Details&lt;/strong&gt; pane, it's very easy to inspect both the BIGinfo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyeopq7npywgyvxkvcdzs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyeopq7npywgyvxkvcdzs.png" alt="BIGinfo" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yeopq7npywgyvxkvcdzs.png" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the BASE, which correctly shows a single subgroup with a single mono-channel 16 kHz BIS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3to3ny3ik3waq8byh2id.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3to3ny3ik3waq8byh2id.png" alt="BASE" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3to3ny3ik3waq8byh2id.png" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another cool feature is in the &lt;strong&gt;Audio Export&lt;/strong&gt; panel, where it's both possible to listen to a selected BIS through the PC speaker and exporting to a file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcdnxenubcu0hcwxq7c9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcdnxenubcu0hcwxq7c9.png" alt="Audio Export" width="800" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zcdnxenubcu0hcwxq7c9.png" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here I noticed yet another very nice feature.  By right-clicking on the entries of captured source streams in the &lt;strong&gt;Audio Export&lt;/strong&gt; panel, it's possible to generate a &lt;a href=""&gt;Broadcast Audio URI&lt;/a&gt; (with QR code):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mu2nmb9w3xzilhke9y5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mu2nmb9w3xzilhke9y5.png" alt="Broadcast Audio URI" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mu2nmb9w3xzilhke9y5.png" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By using the simple web test page, described in &lt;a href="https://dev.to/denladeside/barcodedetector-api-for-le-audio-4ack"&gt;another post&lt;/a&gt;, I was able to verify the code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flm4aell4h6is8atzc2e2.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flm4aell4h6is8atzc2e2.jpeg" alt="QR scan" width="800" height="1320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lm4aell4h6is8atzc2e2.jpeg" rel="noopener noreferrer"&gt;&lt;em&gt;full size&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this post, we've covered how to create a simple, affordable standalone BAP Broadcast Source, broadcasting music in a loop from a tiny nRF52840 Dongle and how to capture audio and verifying BIGinfo and BASE with the RFcreations &lt;a href="https://www.rfcreations.com/hardware-platform/mini-moreph" rel="noopener noreferrer"&gt;mini-moreph&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full source code for the simple BAP broadcast source is available &lt;a href="https://github.com/larsgk/fixed-source" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Enjoy ;)&lt;/p&gt;

</description>
      <category>auracast</category>
      <category>zephyr</category>
      <category>rfcreations</category>
    </item>
    <item>
      <title>Capturing the perfect (radio) wave</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Wed, 30 Oct 2024 18:58:24 +0000</pubDate>
      <link>https://dev.to/denladeside/capturing-the-perfect-radio-wave-16cl</link>
      <guid>https://dev.to/denladeside/capturing-the-perfect-radio-wave-16cl</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;When developing and debugging bluetooth solutions, it is often necessary to know exactly what is communicated in the air between connected devices.&lt;/p&gt;

&lt;p&gt;For this purpose, a Bluetooth Protocol Analyzer (or &lt;a href="https://novelbits.io/bluetooth-low-energy-ble-sniffer-tutorial/" rel="noopener noreferrer"&gt;Bluetooth Sniffer&lt;/a&gt;) is needed and in this post, I’ll show you how to get started, using the RFcreations &lt;a href="https://www.rfcreations.com/hardware-platform/mini-moreph" rel="noopener noreferrer"&gt;mini-moreph&lt;/a&gt; together with the &lt;a href="https://www.rfcreations.com/software/bluespy" rel="noopener noreferrer"&gt;blueSpy software&lt;/a&gt;, available for download on the RCcreations website.&lt;/p&gt;

&lt;p&gt;The software is available for Linux, Windows and Mac.  A big plus for me, as I am using Linux on my main development machine.&lt;/p&gt;

&lt;h1&gt;
  
  
  Getting started with Bluetooth sniffers
&lt;/h1&gt;

&lt;p&gt;Even though I have been developing software in general for almost 40 years and wireless related for the last 10, only recently, I’ve been introduced to using Bluetooth protocol analyzers to debug Bluetooth solutions and this has been a great eye-opener for me (comparable to when I got my first oscilloscope after having done electronics “in the blind” for some years).&lt;/p&gt;

&lt;p&gt;I am not an expert (yet) but find the blueSpy software very enjoyable to use and even though the RFcreations solution can capture and analyze very advanced stuff, I’m happy to see that the UI is snappy and the UX being very intuitive.  This allows me to explore and learn while using the tool and not being required to look through a bunch of documentation at the same time.&lt;/p&gt;

&lt;h1&gt;
  
  
  Our first capture
&lt;/h1&gt;

&lt;p&gt;I thought about what would be a good first capture, and remembered, I recently made a very &lt;a href="https://github.com/larsgk/simple-web-zephyr" rel="noopener noreferrer"&gt;simple Bluetooth Low Energy demo&lt;/a&gt; using &lt;a href="https://zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr&lt;/a&gt; and &lt;a href="https://developer.chrome.com/docs/capabilities/bluetooth" rel="noopener noreferrer"&gt;Web&lt;/a&gt;, covered in an &lt;a href="https://dev.to/denladeside/getting-started-with-zephyr-and-web-bluetooth-355d"&gt;earlier post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0qo0i6amsmb09n8b54o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0qo0i6amsmb09n8b54o.jpg" alt="SimpleWebZephyr image" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After powering up the &lt;a href="https://www.nordicsemi.com/" rel="noopener noreferrer"&gt;Nordic Semiconductor&lt;/a&gt; &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle" rel="noopener noreferrer"&gt;nRF52840 Dongle&lt;/a&gt;, I connected the &lt;a href="https://www.rfcreations.com/hardware-platform/mini-moreph" rel="noopener noreferrer"&gt;mini-moreph&lt;/a&gt; via USB and started the blueSpy software.&lt;/p&gt;

&lt;p&gt;Initially, the screen looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnkfac15qojkjcpzkh9ak.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnkfac15qojkjcpzkh9ak.png" alt="blueSpy start screen" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nkfac15qojkjcpzkh9ak.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: If the device is not automatically found and you are using Linux, remember to add a udev rule to allow userspace access to the device (see the PDF manual included with the software):&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SUBSYSTEM=="usb", ATTRS{idVendor}=="2bbd", ATTRS{idProduct}=="00f3", MODE="0666"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Starting the capture
&lt;/h2&gt;

&lt;p&gt;Click the red capture button to start capturing all traffic in the air.  You should now see the “Filter devices” tab to the right quickly being filled with devices found around the analyzer.  At first it can look a bit chaotic, but clicking the search button in the upper right corner allows you to write a partial name, which should quickly bring the device of interest to the top of the table.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: I’ve also disabled WiFi capture and a few others in this example (keeping the summary view clean and focused).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hbwp5yg78tcnzmgi8fq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hbwp5yg78tcnzmgi8fq.png" alt="Filtering summary" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0hbwp5yg78tcnzmgi8fq.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now click the check mark in the “Shown” column for that device and see the filtered traffic starting to flow in the summary panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdc1ulk8dea7qia8j1f69.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdc1ulk8dea7qia8j1f69.png" alt="Filter device" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dc1ulk8dea7qia8j1f69.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We quickly see a bunch of advertising data, emitted from the ‘Simple Web Zephyr’ dongle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting from Web
&lt;/h2&gt;

&lt;p&gt;Now, from a phone, I open the &lt;a href="https://larsgk.github.io/simple-web-zephyr/single_page.html" rel="noopener noreferrer"&gt;test web page&lt;/a&gt; for the project and request a connection to the dongle.  Then we see the following initial handshake in the capture summary, including discovery of the service requested from the web application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjydqo4anlhhcrfmg3g2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjydqo4anlhhcrfmg3g2.png" alt="Connection and discovery" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zjydqo4anlhhcrfmg3g2.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If I try to press and release the button on the nRF Dongle, I see two notifications being sent from the dongle. The first with the payload value 0x01 (indicating "Pressed")...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4d3h5sb4ol0r4w7djr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4d3h5sb4ol0r4w7djr6.png" alt="Pressed notify" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h4d3h5sb4ol0r4w7djr6.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;...and the second with the payload value 0x00 (indicating "Released") - just as expected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fogcn17brc0ez9ipuu9b9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fogcn17brc0ez9ipuu9b9.png" alt="Released notify" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ogcn17brc0ez9ipuu9b9.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From the web application, I now select the color red (payload: [0xFF, 0x00, 0x00])…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyv40hlljkveyx4qblt8e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyv40hlljkveyx4qblt8e.png" alt="Write red 0xff,0x00,0x00" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yv40hlljkveyx4qblt8e.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;…followed by blue (payload: [0x00, 0x00, 0xFF])…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4nor5rc4fzd3sf8j7xj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4nor5rc4fzd3sf8j7xj.png" alt="Write blue 0x00,0x00,0xff" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l4nor5rc4fzd3sf8j7xj.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Again, the data captured is as expected, but it’s nice to verify :)&lt;/p&gt;

&lt;p&gt;Disconnecting the web application from the dongle lets the dongle firmware go back to advertising mode, which we verify in the summary pane.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrh806xt08px8e8frcxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrh806xt08px8e8frcxe.png" alt="Disconnect, advertising" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/brh806xt08px8e8frcxe.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing capture files
&lt;/h2&gt;

&lt;p&gt;Sometimes, capture files can become quite large - especially if the capture was made in an area with a lot of wireless traffic.  To help solve this problem, I found a very neat feature in the &lt;a href="https://www.rfcreations.com/software/bluespy" rel="noopener noreferrer"&gt;blueSpy&lt;/a&gt; software file menu, called “Save Advanced…”, which allows you to store just the packets shown in the current filtered summary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwxcy6t7f6zok2q9m1w1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwxcy6t7f6zok2q9m1w1.png" alt="Save capture" width="800" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwxcy6t7f6zok2q9m1w1.png" rel="noopener noreferrer"&gt;Full size&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In my case, this brought the capture file size down to ~2Mb (compared to ~90Mb for the full capture).&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;I had great fun, finally being able to see the Bluetooth traffic in the air after developing and debugging Bluetooth solutions “in the blind” for years - and &lt;a href="https://www.rfcreations.com/software/bluespy" rel="noopener noreferrer"&gt;blueSpy&lt;/a&gt; made it enjoyable.&lt;/p&gt;

&lt;p&gt;In my next post, I’ll try to capture some LE Audio Broadcast sources to see how the analyzer handles those. &lt;/p&gt;

</description>
      <category>bluetooth</category>
      <category>zephyr</category>
      <category>rfcreations</category>
    </item>
    <item>
      <title>BarcodeDetector API for LE Audio</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Sun, 14 Jul 2024 19:22:55 +0000</pubDate>
      <link>https://dev.to/denladeside/barcodedetector-api-for-le-audio-4ack</link>
      <guid>https://dev.to/denladeside/barcodedetector-api-for-le-audio-4ack</guid>
      <description>&lt;p&gt;As mentioned in my post on &lt;a href="https://dev.to/denladeside/broadcast-audio-uri-1kkd"&gt;building a Broadcast Audio Source&lt;/a&gt; the &lt;a href="https://www.bluetooth.com/specifications/specs/broadcast-audio-uniform-resource-identifier/" rel="noopener noreferrer"&gt;Broadcast Audio URI (BAU)&lt;/a&gt; spec allows sharing information about broadcast sources through QR codes, NFC tags and more.&lt;/p&gt;

&lt;p&gt;In this post, I'll show how you can make a web application that can read and parse the Broadcast Audio URI QR codes.&lt;/p&gt;

&lt;h1&gt;
  
  
  BarcodeDetector API
&lt;/h1&gt;

&lt;p&gt;Reading barcodes directly in the browser is made possible with the introduction of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector" rel="noopener noreferrer"&gt;BarcodeDetector API&lt;/a&gt; and in this post, I will show an example of how it can be used to read the Broadcast Audio URI QR code with a few lines of JavaScript.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: The BarcodeDetector API is &lt;a href="https://caniuse.com/mdn-api_barcodedetector" rel="noopener noreferrer"&gt;not yet available in all browsers&lt;/a&gt;.  For this reason, a &lt;a href="https://github.com/undecaf/barcode-detector-polyfill" rel="noopener noreferrer"&gt;polyfill&lt;/a&gt; will be used where support is still missing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reading barcodes with this API is extremely simple. It only requires passing an image source to the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector/detect" rel="noopener noreferrer"&gt;&lt;code&gt;detect()&lt;/code&gt;&lt;/a&gt; function, which returns a Promise that fulfills with an array of &lt;code&gt;DetectedBarcode&lt;/code&gt; objects.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even though we are only focused on QR codes in this post, the API supports &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats" rel="noopener noreferrer"&gt;many commonly used formats&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Lights, Camera, Action!
&lt;/h1&gt;

&lt;p&gt;First, the camera needs to be switched on and streaming a video feed to an &lt;code&gt;HTMLVideoElement&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"camera"&lt;/span&gt;
       &lt;span class="na"&gt;muted&lt;/span&gt;
       &lt;span class="na"&gt;autoplay=&lt;/span&gt;&lt;span class="s"&gt;"autoplay"&lt;/span&gt;
       &lt;span class="na"&gt;playsinline=&lt;/span&gt;&lt;span class="s"&gt;"playsinline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#camera&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;constraints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to make a barcode detector object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;barcodeDetector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BarcodeDetector&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order for the detection to work as expected, the &lt;code&gt;detect&lt;/code&gt; function needs to be called at regular intervals to scan images captured with the camera:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decodeQr&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;barcodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;barcodeDetector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;barcodes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;barcode&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;barcodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Try to parse the URI string&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseBroadcastURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;barcode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Avoid repainting if the data is already shown&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;barcode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;lastCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;barcode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Display the parsed info&lt;/span&gt;
        &lt;span class="nf"&gt;showBroadcastInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Only use the first code found&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lastCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Try again in 100ms&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decodeQr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Parsing the Broadcast Audio URI
&lt;/h1&gt;

&lt;p&gt;The scanned code should contain a string, starting with &lt;code&gt;BLUETOOTH:&lt;/code&gt;, followed by a number of fields as listed in the &lt;a href="https://www.bluetooth.com/specifications/specs/broadcast-audio-uniform-resource-identifier/" rel="noopener noreferrer"&gt;Broadcast Audio URI spec&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;A QR code example from the spec: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faodcm2qe8edu74vcm819.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faodcm2qe8edu74vcm819.png" alt="Image description" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;, containing the following data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLUETOOTH:UUID:184F;BN:SG9ja2V5;SQ:1;AT:0;AD:AABBCC001122;AS:1;BI:DE51E9;PI:F
FFF;NS:1;BS:1;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;roughly translates to:&lt;/p&gt;

&lt;p&gt;"Related to the 0x184F UUID (Broadcast Audio Scan Service), there is a Standard Quality mono channel broadcast at addresss AA:BB:CC:00:11:22 with Broadcast ID 0x0E51E9 and the Broadcast name 'Hockey'"&lt;/p&gt;

&lt;p&gt;For this PoC, I created a very simple function to parse the most common fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BROADCAST_AUDIO_URI_SCHEME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BLUETOOTH:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parseBroadcastURI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BROADCAST_AUDIO_URI_SCHEME&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="c1"&gt;// split sections (;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BROADCAST_AUDIO_URI_SCHEME&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;section&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UUID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UUID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// Broadcast ID&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Broadcast ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// Broadcast name&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Broadcast Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;base64ToBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="c1"&gt;// ... (more fields in full scouce)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  QR Scanner component
&lt;/h1&gt;

&lt;p&gt;In order to make it possible for others to more easily embed the BAU QR scanner in a web application, I decided to create a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components" rel="noopener noreferrer"&gt;web component&lt;/a&gt; that encapsulates the logic to control the video feed and display the information found in an overlay info box.&lt;/p&gt;

&lt;p&gt;In order to use it in a page, just add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;bau-scanner&amp;gt;&amp;lt;/bau-scanner&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./bau-scanner.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The element has functions to start and stop the scanning (and camera feed), which can be hooked up to button events, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"start_scanner"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Start scanner&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;'stop_scanner'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Stop scanner&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startScan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#start_scanner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stopScan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#stop_scanner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;scanner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bau-scanner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;startScan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startCamera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;stopScan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stopCamera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An example &lt;code&gt;index.html&lt;/code&gt; together with the &lt;code&gt;bau-scanner.js&lt;/code&gt; component can be found here: &lt;a href="https://github.com/larsgk/bau-source" rel="noopener noreferrer"&gt;https://github.com/larsgk/bau-source&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  See it in action
&lt;/h1&gt;

&lt;p&gt;For demonstration purposes, I decided to use two QR codes from the Broadcast Audio URI spec plus a dynamic one, generated with the Broadcast Audio Source sample covered in &lt;a href="https://dev.to/denladeside/broadcast-audio-uri-1kkd"&gt;another post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is the scanner in action, running on a mobile phone: &lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/tO0uh2rrVfg"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can try it yourself by opening this link, where the demo page is hosted: &lt;a href="https://larsgk.github.io/bau-source/" rel="noopener noreferrer"&gt;https://larsgk.github.io/bau-source/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enjoy ;)&lt;/p&gt;

</description>
      <category>webcapabilities</category>
      <category>leaudio</category>
      <category>barcodedetector</category>
    </item>
    <item>
      <title>Broadcast Audio URI</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Thu, 06 Jun 2024 20:56:37 +0000</pubDate>
      <link>https://dev.to/denladeside/broadcast-audio-uri-1kkd</link>
      <guid>https://dev.to/denladeside/broadcast-audio-uri-1kkd</guid>
      <description>&lt;h1&gt;
  
  
  Audio sharing is caring
&lt;/h1&gt;

&lt;p&gt;Until recently, most generic Bluetooth enabled speakers and headsets have been using Bluetooth Classic. With the introduction of &lt;a href="https://www.bluetooth.com/learn-about-bluetooth/feature-enhancements/le-audio/" rel="noopener noreferrer"&gt;Bluetooth LE Audio&lt;/a&gt;, a wide new range of solutions become possible and one of the most exciting ones is the possibility to broadcast audio, where the sender and receiver are no longer required to be connected (like the 1-1 Bluetooth Classic case - e.g a phone and a headset).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"LE Audio introduces broadcast audio to Bluetooth® technology, a new feature that enables an audio transmitter to broadcast to an unlimited number of nearby Bluetooth audio receivers. Broadcast audio opens significant new opportunities for innovation, including a powerful new capability, Auracast™ broadcast audio.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Auracast™ broadcast audio is a set of defined configurations of Bluetooth® broadcast audio which are specified within the Public Broadcast Profile (PBP) specification that enables new, globally interoperable audio experiences for consumers."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read more about Auracast here: &lt;a href="https://www.bluetooth.com/auracast/developers/" rel="noopener noreferrer"&gt;https://www.bluetooth.com/auracast/developers/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Sharing Links to Broadcasts
&lt;/h1&gt;

&lt;p&gt;Even though it is possible to scan for nearby broadcasts using a &lt;a href="https://www.bluetooth.com/auracast/how-it-works/" rel="noopener noreferrer"&gt;Broadcast Audio Assistant&lt;/a&gt;, sometimes, it will be more practical for users to 'tune in' to Broadcast sources using a trusted link provided through other means.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.bluetooth.com/specifications/specs/broadcast-audio-uniform-resource-identifier/" rel="noopener noreferrer"&gt;Broadcast Audio URI&lt;/a&gt; is an exciting new spec that allows just that: Information about a Broadcast Audio Source to be conveyed over an out-of-band (OOB) medium to a Broadcast Audio Assistant - or, if the Broadcast Sink supports it, directly by e.g. touching an NFC tag, containing the Broadcast Audio URI, with a speaker or earbuds.&lt;/p&gt;

&lt;p&gt;An example from the spec: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faodcm2qe8edu74vcm819.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faodcm2qe8edu74vcm819.png" alt="Image description" width="284" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This includes all the information for a Broadcast Sink to find the right broadcast.  The data in the QR code is this text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLUETOOTH:UUID:184F;BN:SG9ja2V5;SQ:1;AT:0;AD:AABBCC001122;AS:1;BI:DE51E9;PI:F
FFF;NS:1;BS:1;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which roughly translates to:&lt;/p&gt;

&lt;p&gt;"Related to the 0x184F UUID (Broadcast Audio Scan Service), there is a Standard Quality mono channel broadcast at addresss AA:BB:CC:00:11:22 with Broadcast ID 0x0E51E9 and the Broadcast name 'Hockey'"&lt;/p&gt;

&lt;h1&gt;
  
  
  Zephyr for the win!
&lt;/h1&gt;

&lt;p&gt;The &lt;a href="https://www.zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt; is a very powerful OS for embedded devices and it also includes support for &lt;a href="https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.html" rel="noopener noreferrer"&gt;LE Audio&lt;/a&gt;.  If this is the first time you hear about it, please check some of my other posts on getting started with Zephyr, e.g. &lt;a href="https://dev.to/denladeside/getting-started-with-zephyr-and-web-bluetooth-355d"&gt;Getting started with Zephyr and Web Bluetooth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the Zephyr repo, there is a &lt;a href="https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/bluetooth/bap_broadcast_source" rel="noopener noreferrer"&gt;Broadcast Audio Source sample&lt;/a&gt; that was used as a starting point.&lt;/p&gt;

&lt;p&gt;There is also great support for different displays, which we will need to display the Broadcast Audio URI in a QR code.&lt;/p&gt;

&lt;p&gt;To my happy surprise, there was already &lt;a href="https://docs.lvgl.io/master/libs/qrcode.html" rel="noopener noreferrer"&gt;QR code support&lt;/a&gt; in the &lt;a href="https://lvgl.io/" rel="noopener noreferrer"&gt;LVGL&lt;/a&gt; module included in Zephyr so I implemented some minimal QR-Code functionality to expose the required data once the Broadcast source was ready on the device. As this is a PoC, the non-changing parameters are hardcoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;gui_update_qr_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;bt_addr_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;broadcast_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;addr_str&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;name_base64&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;name_base64_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Address */&lt;/span&gt;
    &lt;span class="n"&gt;snprintk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr_str&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"%02X%02X%02X%02X%02X%02X"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="cm"&gt;/* Name */&lt;/span&gt;
    &lt;span class="n"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name_base64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;name_base64_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;name_base64&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name_base64_len&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Most fields hard coded for this demo */&lt;/span&gt;
    &lt;span class="n"&gt;snprintk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qr_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qr_data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
         &lt;span class="s"&gt;"BLUETOOTH:UUID:184F;BN:%s;SQ:1;AT:1;AD:%s;AS:0;BI:%06X;PI:FFFF;NS:1;BS:1;;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;name_base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addr_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;broadcast_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;lv_qrcode_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qr_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qr_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qr_data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;lv_task_handler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjr0o7ikrewb1iraujsc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjr0o7ikrewb1iraujsc.jpg" alt="Broadcast Audio URI QR code" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Try it out!
&lt;/h1&gt;

&lt;p&gt;Put together, here is a PoC of how a &lt;a href="https://www.bluetooth.com/specifications/specs/broadcast-audio-uri-2/" rel="noopener noreferrer"&gt;Broadcast Audio URI&lt;/a&gt; can be used to expose a dynamically generated QR code on an nRF5340 Audio DK with a screen attached.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hardware used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.nordicsemi.com/Products/Development-hardware/nRF5340-Audio-DK" rel="noopener noreferrer"&gt;nRF5340 Audio DK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.adafruit.com/product/1947" rel="noopener noreferrer"&gt;Adafruit 2.8" TFT Touch Shield&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The code is available here: &lt;a href="https://github.com/larsgk/bau-source" rel="noopener noreferrer"&gt;https://github.com/larsgk/bau-source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Connecting the board to the USB port of any host device supporting USB Audio (Windows, Linux, Mac, Android, ChromeOS, etc), will automatically make the connected board a Broadcast Audio Source, streaming whatever is played on the host device. If you have access to one of the latest Samsung mobile phones and Galaxy Buds2 Pro, you should be able to find and select the broadcast and hear it in the earbuds.&lt;/p&gt;

&lt;h1&gt;
  
  
  See it in action
&lt;/h1&gt;

&lt;p&gt;Currently, there are no implementations of the full Broadcast Audio URI flow in any device available on the market, so I have been using a dedicated web based stand-alone broadcast assistant application, modified to read and parse the Broadcast Audio URI QR code (this will be covered in a future post).  It's using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector" rel="noopener noreferrer"&gt;BarcodeDetector API&lt;/a&gt; on supported devices to read the QR code.&lt;/p&gt;

&lt;p&gt;It allows a user to scan a Broadcast Audio URI QR code and add it directly to an attached Broadcast Sink (e.g earbuds able to receive broadcasts, like the Samsung Galaxy Buds2 Pro).&lt;/p&gt;

&lt;p&gt;The result is here:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/NZ0PG1rOZYQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Credits to &lt;a href="https://www.nickhunn.com/" rel="noopener noreferrer"&gt;Nick Hunn&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/haishalom/" rel="noopener noreferrer"&gt;Hai Shalom&lt;/a&gt; and others on doing great work on the &lt;a href="https://www.bluetooth.com/specifications/specs/broadcast-audio-uri-2/" rel="noopener noreferrer"&gt;Broadcast Audio URI &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, major credits to &lt;a href="https://github.com/Thalley" rel="noopener noreferrer"&gt;Emil Gydesen&lt;/a&gt;, maintainer of the LE Audio stack in Zephyr, that made this possible.&lt;/p&gt;

</description>
      <category>leaudio</category>
      <category>web</category>
      <category>bluetooth</category>
      <category>zephyr</category>
    </item>
    <item>
      <title>Handling files in enterprise web solutions</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Mon, 04 Mar 2024 17:20:00 +0000</pubDate>
      <link>https://dev.to/denladeside/handling-files-in-enterprise-web-solutions-3mkc</link>
      <guid>https://dev.to/denladeside/handling-files-in-enterprise-web-solutions-3mkc</guid>
      <description>&lt;p&gt;Correct file handling can be cumbersome without proper system integration and selection of which applications can handle what types of files.&lt;/p&gt;

&lt;p&gt;Web solutions have previously been disconnected from this because of limitations in the browser to system integration.&lt;/p&gt;

&lt;p&gt;This is no longer the case, and this post will cover the key parts of making a solution that works.&lt;/p&gt;

&lt;p&gt;To make things more concrete, we will look at how to create a simple &lt;a href="https://geojson.org/"&gt;GeoJSON&lt;/a&gt; viewer application that will automatically open and display GeoJSON files when double-clicking them (or selecting the app on "Open with...").&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This feature is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Manifest/file_handlers#browser_compatibility"&gt;desktop only&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  File Handling in manifest.json
&lt;/h2&gt;

&lt;p&gt;In the file manager and the desktop view of most operating systems, there is a file handling UI that directs the user to use a certain app to handle the chosen file.  Previously, this was limited to native app installations, but now, it's possible for web applications to register handlers tied to PWAs that will handle files matching certain extension patterns.&lt;/p&gt;

&lt;p&gt;Thomas Steiner has created a great article about this here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.chrome.com/docs/capabilities/web-apis/file-handling"&gt;https://developer.chrome.com/docs/capabilities/web-apis/file-handling&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's needed
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;file_handlers&lt;/code&gt; entries in manifest.json&lt;/li&gt;
&lt;li&gt;Code to handle loading the files&lt;/li&gt;
&lt;li&gt;Installation of the web application&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's look at these one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;file_handlers&lt;/code&gt; in manifest.json
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: In order to make the web application 'installable', a manifest file needs to be present with minimal information as described &lt;a href="https://web.dev/articles/install-criteria"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Inside the manifest file, create a section with the key &lt;a href="https://developer.chrome.com/docs/extensions/reference/manifest/file-handlers"&gt;&lt;code&gt;file_handlers&lt;/code&gt;&lt;/a&gt;.  This contains an array of the file types that the application can handle (&lt;code&gt;accept&lt;/code&gt;), which URL to open (&lt;code&gt;action&lt;/code&gt;, must be within the scope of the installed PWA), icons and &lt;code&gt;launch_type&lt;/code&gt;, describing if new instances should be made for each file on "Open with..." selection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"file_handlers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./?log=y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"application/geo+json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;".geojson"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./images/treasure-map-256.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"256x256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"launch_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multiple-clients"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our case, we are interested in handling files with the &lt;code&gt;.geojson&lt;/code&gt; extension and we will send it to our PWA with an additional query parameter to enable debug logging (&lt;code&gt;log=y&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  File loading handler(s)
&lt;/h3&gt;

&lt;p&gt;When the PWA is opened by the OS via an "Open with..." action, the file(s) selected will be available through the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/LaunchQueue"&gt;&lt;code&gt;launchQueue&lt;/code&gt;&lt;/a&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;launchQueue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;LaunchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;launchQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;launchParams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;launchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileHandle&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;launchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Handle the file.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileHandle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadGeoJSONFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;File Handling API NOT supported&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installation and usage
&lt;/h3&gt;

&lt;p&gt;Open the application in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Manifest/file_handlers#browser_compatibility"&gt;supported browser&lt;/a&gt; and click the install icon in the address bar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fusvmj4gpu5bxqcp2u0ff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fusvmj4gpu5bxqcp2u0ff.png" alt="Install PWA" width="534" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then go to the file explorer, right click a GeoJSON file (with the &lt;code&gt;.geojson&lt;/code&gt; extension) and see how the freshly installed PWA is now registered to handle GeoJSON files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkm8pb387umeqmd8wd21g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkm8pb387umeqmd8wd21g.png" alt="Open with PWA" width="603" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On first launch, the user is prompted to check if this should be the default behavior:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ii2l854z9yvsv3hke5b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ii2l854z9yvsv3hke5b.png" alt="Default open with" width="475" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the PWA starts, the GeoJSON is loaded, passed to the map and immediately displayed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4cl170c5xenvyslbvj7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4cl170c5xenvyslbvj7.png" alt="GeoJSON loaded" width="800" height="727"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The collapsible activity log in the bottom of the screen is activated because of the additional &lt;code&gt;log=y&lt;/code&gt; passed to the application. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Windows and Edge
&lt;/h3&gt;

&lt;p&gt;The section above describes how the flow looks when using Chrome and Ubuntu.  Here we will go through the same flow, but using Edge on Windows.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Credit: &lt;a class="mentioned-user" href="https://dev.to/kennethrohde"&gt;@kennethrohde&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Installation:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26pafpf7ur9024hh0hr5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26pafpf7ur9024hh0hr5.jpeg" alt="Edge Install PWA" width="613" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;.geojson&lt;/code&gt; file with the PWA:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcq2aigbs5tkalndu9c1n.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcq2aigbs5tkalndu9c1n.jpeg" alt="Edge Open With" width="718" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Loading GeoJSON:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpblmz7s6f051lnd61k5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpblmz7s6f051lnd61k5.jpeg" alt="Edge Loading GeoJSON" width="800" height="724"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  OpenLayers
&lt;/h2&gt;

&lt;p&gt;In order to display the GeoJSON features on a map, we will use &lt;a href="https://openlayers.org/"&gt;OpenLayers&lt;/a&gt;, which is a very powerful &lt;a href="https://github.com/openlayers/openlayers"&gt;open-source&lt;/a&gt; mapping library that is also very simple to use.&lt;/p&gt;

&lt;p&gt;In this post, we will not dive into all the details, so I will just show the snippets related to initializing the map and adding the GeoJSON data on top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="c1"&gt;// Init map&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OSM&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;vectorLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;mapEl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;// Load GeoJSON&lt;/span&gt;
&lt;span class="nf"&gt;plotGeoJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GeoJSON&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;featureProjection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EPSG:3857&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;readFeatures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vectorSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;features&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;vectorLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vectorSource&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vectorSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExtent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating GeoJSON files
&lt;/h2&gt;

&lt;p&gt;Inside the project &lt;a href="https://github.com/larsgk/filehandling-geojson"&gt;repository&lt;/a&gt;, there are &lt;a href="https://github.com/larsgk/filehandling-geojson/tree/main/geojson"&gt;a few GeoJSON samples&lt;/a&gt; generated with the awesome GeoJSON editor here: &lt;a href="https://geojson.io/"&gt;geojson.io&lt;/a&gt;.  This web application let's you create features by drawing on a map and the features are then immediately reflected in a GeoJSON format on the right side of the screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0kel38tgsqrmgw6e19w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0kel38tgsqrmgw6e19w.png" alt="Screenshot of geojson.io" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TIL GitHub has a nice GeoJSON viewer built-in:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqib609vbuky2l32fgr26.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqib609vbuky2l32fgr26.png" alt="GitHub GeoJSON" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Having the option of adding file handling on a system level is a very powerful feature and also extremely useful, e.g for enterprise tools to optimize workflows.&lt;/p&gt;

&lt;p&gt;The effort involved in adding the feature in a PWA is close to nothing and personally I enjoyed experimenting with this very much.&lt;/p&gt;

&lt;p&gt;The code is available here:  &lt;a href="https://github.com/larsgk/filehandling-geojson"&gt;https://github.com/larsgk/filehandling-geojson&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application is hosted here:  &lt;a href="https://larsgk.github.io/filehandling-geojson"&gt;https://larsgk.github.io/filehandling-geojson&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enjoy ;)&lt;/p&gt;

</description>
      <category>enterprise</category>
      <category>web</category>
      <category>fugu</category>
    </item>
    <item>
      <title>Getting started with Zephyr and Web Bluetooth</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Mon, 04 Dec 2023 22:05:57 +0000</pubDate>
      <link>https://dev.to/denladeside/getting-started-with-zephyr-and-web-bluetooth-355d</link>
      <guid>https://dev.to/denladeside/getting-started-with-zephyr-and-web-bluetooth-355d</guid>
      <description>&lt;p&gt;For this post, I wrote the embedded firmware and accompanying test web page in an afternoon - and I want everyone to know that they can too.&lt;/p&gt;

&lt;p&gt;I did cheat a bit, as I am standing on the shoulders of giants, using the &lt;a href="https://www.zephyrproject.org/" rel="noopener noreferrer"&gt;Zephyr RTOS&lt;/a&gt; on the embedded side and using &lt;a href="https://developer.chrome.com/articles/bluetooth/" rel="noopener noreferrer"&gt;APIs&lt;/a&gt; in the browser that has taken quite some energy to perfect (credits go to &lt;a href="https://mastodon.social/@reillyeon@toot.cafe" rel="noopener noreferrer"&gt;@reillyeon@toot.cafe&lt;/a&gt;, &lt;a href="https://twitter.com/Vincent_Scheib" rel="noopener noreferrer"&gt;@Vincent_Scheib&lt;/a&gt;, &lt;a href="https://twitter.com/quicksave2k" rel="noopener noreferrer"&gt;@quicksave2k&lt;/a&gt; and &lt;a href="https://github.com/WebBluetoothCG/web-bluetooth/graphs/contributors" rel="noopener noreferrer"&gt;more&lt;/a&gt; ).&lt;/p&gt;

&lt;p&gt;Anyway, let's get to work!&lt;/p&gt;

&lt;h2&gt;
  
  
  What this thing does
&lt;/h2&gt;

&lt;p&gt;The firmware exposes a GATT service with a few characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting the color of the primary RGB LED&lt;/li&gt;
&lt;li&gt;Setting an ID, which will be appended to the BT name (convenient when in a workshop with multiple devices)&lt;/li&gt;
&lt;li&gt;Being notified of button presses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also enables logging over USB, if the board supports it.&lt;/p&gt;

&lt;p&gt;In the root of the &lt;a href="https://github.com/larsgk/simple-web-zephyr" rel="noopener noreferrer"&gt;repository&lt;/a&gt;, there is a very &lt;a href="https://github.com/larsgk/simple-web-zephyr/blob/main/single_page.html" rel="noopener noreferrer"&gt;simple self contained HTML file&lt;/a&gt; (so it doesn't require a web server when loaded as a file locally) to interact with the hardware via &lt;a href="https://developer.chrome.com/articles/bluetooth/" rel="noopener noreferrer"&gt;Web Bluetooth&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple GATT service
&lt;/h2&gt;

&lt;p&gt;In order to enable easy control from a web application using &lt;a href="https://developer.chrome.com/articles/bluetooth/" rel="noopener noreferrer"&gt;Web Bluetooth&lt;/a&gt;, a simple &lt;a href="https://www.bluetooth.com/bluetooth-resources/intro-to-bluetooth-gap-gatt/" rel="noopener noreferrer"&gt;GATT&lt;/a&gt; service must be created.&lt;/p&gt;

&lt;p&gt;First, we need to create UUIDs to be used for the service and characteristics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_uuid_128&lt;/span&gt; &lt;span class="n"&gt;simple_io_service_uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BT_UUID_INIT_128&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BT_UUID_128_ENCODE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x13e779a0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xbb72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x43a4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xa748&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x9781b918258c&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_uuid_128&lt;/span&gt; &lt;span class="n"&gt;simple_io_rgb_uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BT_UUID_INIT_128&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BT_UUID_128_ENCODE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x4332aca6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x6d71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x4173&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x9945&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x6653b6c684a0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_uuid_128&lt;/span&gt; &lt;span class="n"&gt;simple_io_id_uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BT_UUID_INIT_128&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BT_UUID_128_ENCODE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xb749d964&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x4efb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x408a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x82ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x7495e8af8d6d&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_uuid_128&lt;/span&gt; &lt;span class="n"&gt;simple_io_button_uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BT_UUID_INIT_128&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BT_UUID_128_ENCODE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x030de9cf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xce4b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x44d0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x8aa2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x1db9185dc069&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UUIDs are the unique identifiers used by the central device (in this case, the web application) to find the correct services and characteristics in BLE devices.  Some are short, &lt;a href="https://www.bluetooth.com/wp-content/uploads/Files/Specification/Assigned_Numbers.html#bookmark102" rel="noopener noreferrer"&gt;officially assigned, 16bit values&lt;/a&gt; - the &lt;strong&gt;Battery service&lt;/strong&gt; (= 0x180F) or &lt;strong&gt;Heart Rate service&lt;/strong&gt; (= 0x180D) - but for custom services and characteristics like these, a custom 128bit UUID must be provided.&lt;/p&gt;

&lt;p&gt;For UUID generation, I'd recommend using one of the &lt;a href="https://www.uuidtools.com/v4" rel="noopener noreferrer"&gt;many&lt;/a&gt; &lt;a href="https://uuidonline.com/" rel="noopener noreferrer"&gt;online&lt;/a&gt; &lt;a href="https://www.uuidgenerator.net/" rel="noopener noreferrer"&gt;tools&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Defining the GATT service structure in Zephyr:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Simple IO Service Declaration */&lt;/span&gt;
&lt;span class="n"&gt;BT_GATT_SERVICE_DEFINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;simple_io_svc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BT_GATT_PRIMARY_SERVICE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;simple_io_service_uuid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;BT_GATT_CHARACTERISTIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;simple_io_button_uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;BT_GATT_CHRC_NOTIFY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;BT_GATT_PERM_NONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;BT_GATT_CCC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_ccc_cfg_changed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;BT_GATT_PERM_READ&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;BT_GATT_PERM_WRITE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;BT_GATT_CUD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Button(s)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BT_GATT_PERM_READ&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;BT_GATT_CHARACTERISTIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;simple_io_rgb_uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;BT_GATT_CHRC_WRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;BT_GATT_PERM_WRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_rgb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;BT_GATT_CUD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RGB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BT_GATT_PERM_READ&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;BT_GATT_CHARACTERISTIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;simple_io_id_uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;BT_GATT_CHRC_WRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;BT_GATT_PERM_WRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;BT_GATT_CUD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Device ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BT_GATT_PERM_READ&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure defines a GATT service containing the needed characteristics.  For the sake of keeping things simple, a characteristic can be seen like a 'property' (or variable) made available over the air with a specified combination of &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt; and/or &lt;code&gt;notify&lt;/code&gt; capabilities (Note: there are more advanced options but for now, just focus on these three).&lt;/p&gt;

&lt;p&gt;We have the following defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;em&gt;button&lt;/em&gt; characteristic: Will &lt;em&gt;notify&lt;/em&gt; on button presses.&lt;/li&gt;
&lt;li&gt;An &lt;em&gt;rgb&lt;/em&gt; characteristic: To allow &lt;em&gt;writing&lt;/em&gt; an RGB value.&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;device id&lt;/em&gt; characteristic: To allow &lt;em&gt;writing&lt;/em&gt; an ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a connected device &lt;em&gt;writes&lt;/em&gt; an RGB color value (e.g. via Web Bluetooth), the &lt;code&gt;write_rgb&lt;/code&gt; function is called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="nf"&gt;write_rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_conn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_gatt_attr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint16_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint16_t&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s: len=%zu, offset=%u&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__func__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BT_GATT_ERR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BT_ATT_ERR_INVALID_OFFSET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BT_GATT_ERR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BT_ATT_ERR_INVALID_ATTRIBUTE_LEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;simple_io_cbs&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;set_rgb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;simple_io_cbs&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;set_rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This basically checks that 3 bytes are sent and if a &lt;code&gt;set_rgb&lt;/code&gt; callback is registered, this will be called using the 3 bytes as red, green and blue values.&lt;/p&gt;

&lt;p&gt;The full source can be found &lt;a href="https://github.com/larsgk/simple-web-zephyr/blob/main/app/src/simple_io_service.c" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advertising
&lt;/h2&gt;

&lt;p&gt;Correctly discovering Bluetooth Low Energy devices requires them to advertise information about who they are, what services they provide or something else to allow unique or filtered identification.  You have probably seen this when trying to pair with a new set of earbuds, keyboard or other type of device from e.g. a mobile device.&lt;/p&gt;

&lt;p&gt;In our case, we are interested in providing two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The UUID of our custom service&lt;/li&gt;
&lt;li&gt;The name of the device&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is primarily done in two steps.  First, we define the advertising payload containing the service UUID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bt_data&lt;/span&gt; &lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BT_DATA_BYTES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BT_DATA_FLAGS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BT_LE_AD_GENERAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;BT_LE_AD_NO_BREDR&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;BT_DATA_BYTES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BT_DATA_UUID128_ALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BT_UUID_SIMPLE_IO_SERVICE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the bluetooth stack is enabled and ready, we start advertising, including the name (will be added automatically by the Zephyr stack):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;bt_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s: Bluetooth initialized&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__func__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IS_ENABLED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONFIG_SETTINGS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;settings_load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;bt_simple_io_register_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;io_cbs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bt_le_adv_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BT_LE_ADV_CONN_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARRAY_SIZE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s: Advertising failed to start (err %d)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__func__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;printk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s: Advertising successfully started&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__func__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full source for the main application can be found &lt;a href="https://github.com/larsgk/simple-web-zephyr/blob/main/app/src/main.c" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloning, building and flashing
&lt;/h2&gt;

&lt;p&gt;Before cloning the application repository, go to &lt;a href="https://docs.zephyrproject.org/latest/develop/getting_started/index.html" rel="noopener noreferrer"&gt;The Zephyr getting started guide&lt;/a&gt; and install all dependencies (I'd recommend following the path with the virtual python environment).&lt;/p&gt;

&lt;p&gt;This repository contains a stand alone Zephyr application that can be fetched and initialized like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;west init &lt;span class="nt"&gt;-m&lt;/span&gt; git@github.com:larsgk/simple-web-zephyr.git &lt;span class="nt"&gt;--mr&lt;/span&gt; main my-workspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use west to fetch dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;my-workspace
west update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to the app folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;simple-web-zephyr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few scripts for building and flashing the &lt;a href="https://www.nordicsemi.com/" rel="noopener noreferrer"&gt;Nordic Semiconductor&lt;/a&gt; &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle" rel="noopener noreferrer"&gt;nRF52840 Dongle&lt;/a&gt; - run them in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;compile_app.sh
create_flash_package.sh
flash_dongle.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: You'll need to get the dongle in DFU mode by pressing the small side button with a nail. The dongle should then start fading a red light in and out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web Bluetooth test page
&lt;/h2&gt;

&lt;p&gt;In order to connect to a BLE device using Web Bluetooth, first scan for the device, using a filter.  In this case, we look for the advertising of our custom service UUID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SIMPLE_IO_SERVICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;13e779a0-bb72-43a4-a748-9781b918258c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bluetooth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestDevice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;SIMPLE_IO_SERVICE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;openDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ignore if we didn't get a device&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After connecting to the device, find the GATT service and hook up the characteristics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openDevice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPrimaryService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SIMPLE_IO_SERVICE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startButtonNotifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchRGBCharacteristic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connected to device&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;statusElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Connected to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

        &lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ongattserverdisconnected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Disconnected &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;statusElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not Connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start listening for button change notifications...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startButtonNotifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;characteristic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCharacteristic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SIMPLE_IO_BUTTON_CHAR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;characteristic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;characteristicvaluechanged&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Button = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;statusElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Button &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;released&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pressed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;characteristic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startNotifications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and connect the RGB characteristic to allow the application to set a color:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchRGBCharacteristic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rgbCharacteristic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCharacteristic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SIMPLE_IO_RGB_CHAR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setRGB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rgbCharacteristic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;rgbCharacteristic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeValueWithoutResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full source for the web application is available &lt;a href="https://github.com/larsgk/simple-web-zephyr/blob/main/single_page.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Working together
&lt;/h2&gt;

&lt;p&gt;Let's try to connect the web application to an nRF52840 dongle flashed with the Zephyr application.&lt;/p&gt;

&lt;p&gt;First, connect the dongle to a power source (e.g. a PC USB port):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjrf1wun3nbsag02z5xcw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjrf1wun3nbsag02z5xcw.jpg" alt="Spiderman connecting the dongle"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dongle should start flashing blue.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE: Spiderman wanted to make sure the dongle was properly web enabled but he is not strictly needed (please don't tell him)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then open the web application in a &lt;a href="https://caniuse.com/web-bluetooth" rel="noopener noreferrer"&gt;Web Bluetooth capable browser&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You should see something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvpk10xep0hb8kqsey5x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvpk10xep0hb8kqsey5x.png" alt="Initial view of web page loaded"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, press the &lt;strong&gt;CONNECT&lt;/strong&gt; button and a connect dialog should appear, listing the devices found that satisfies the filter given to the &lt;code&gt;requestDevice&lt;/code&gt; function.  Select the &lt;strong&gt;Simple Web Zephyr&lt;/strong&gt; device and click pair:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljy1vhv823enq45m4gve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fljy1vhv823enq45m4gve.png" alt="Request device dialog"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After successfully connecting, the page should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj246jodr3yfe4c9zx9x7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj246jodr3yfe4c9zx9x7.png" alt="Web page showing device connected"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now try to push the button on the dongle to see the status message change:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fel4ctstg746ao3rdz7n2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fel4ctstg746ao3rdz7n2.png" alt="Web page showing button pressed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This should show the page reacting to button press and release notifications sent from the device.&lt;/p&gt;

&lt;p&gt;Try to open the color picker and select e.g. red:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd1ioepk9gez3v0oefgvq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd1ioepk9gez3v0oefgvq.png" alt="Color picker open"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the light on the dongle magically changes to red, more friends come to watch the fun:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjcw3rupgtqkti8067kw3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjcw3rupgtqkti8067kw3.jpg" alt="Dongle with red LED"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final remarks
&lt;/h2&gt;

&lt;p&gt;I hope you enjoyed this introduction to Zephyr and Web Bluetooth.&lt;/p&gt;

&lt;p&gt;Try to expand the Zephyr application with more services or maybe start adding a bit more functionality to the simple web page (e.g. try to make the RGB light change color on button presses).&lt;/p&gt;

&lt;p&gt;The full source code is here: &lt;a href="https://github.com/larsgk/simple-web-zephyr" rel="noopener noreferrer"&gt;https://github.com/larsgk/simple-web-zephyr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Direct link to the test page is here: &lt;a href="https://larsgk.github.io/simple-web-zephyr/single_page.html" rel="noopener noreferrer"&gt;https://larsgk.github.io/simple-web-zephyr/single_page.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Remember: if you download the page file, you can also just load it directly in the browser, make changes and reload.&lt;/p&gt;

&lt;p&gt;Enjoy :)&lt;/p&gt;

</description>
      <category>webbluetooth</category>
      <category>zephyr</category>
      <category>iot</category>
    </item>
    <item>
      <title>Creating a Web Thermometer</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Fri, 11 Aug 2023 00:15:00 +0000</pubDate>
      <link>https://dev.to/denladeside/creating-a-web-thermometer-2137</link>
      <guid>https://dev.to/denladeside/creating-a-web-thermometer-2137</guid>
      <description>&lt;p&gt;Spiderman has joined some LEGO spacemen in a quest to web enable a BLE GATT temperature sensor and display the readings on their 1x1cm brick display...&lt;/p&gt;

&lt;p&gt;While they're working on that, I'll share what's under the hood :)&lt;/p&gt;

&lt;h1&gt;
  
  
  Temperature Sensor in Thingy:52
&lt;/h1&gt;

&lt;p&gt;The &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/Nordic-Thingy-52" rel="noopener noreferrer"&gt;Thingy:52&lt;/a&gt; by &lt;a href="https://www.nordicsemi.com/" rel="noopener noreferrer"&gt;Nordic Semiconductor&lt;/a&gt; has a lot of built-in sensors (temperature, humidity, pressure, air quality, color, accelerometer, gyroscope, and more).&lt;/p&gt;

&lt;p&gt;First, switch it on:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4oin812qhzvy790lrhhb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4oin812qhzvy790lrhhb.jpg" alt="Switching on Thingy52" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our case, we are interested in reading values from the temperature sensor, so let's take a look in the &lt;a href="https://nordicsemiconductor.github.io/Nordic-Thingy52-FW/documentation/firmware_architecture.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; for the device:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8049wrxzvqmh9s98rjq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8049wrxzvqmh9s98rjq.png" alt="Thingy52 GATT Temperature" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: The &lt;code&gt;xxxx&lt;/code&gt; in the base UUID should be replaced with numbers from the same column for service and characteristic(s).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From this, we can see two 128bit UUIDs we need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ef680200-9b35-4933-9b10-52ffa9740042&lt;/code&gt; - the "Weather station service"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ef680201-9b35-4933-9b10-52ffa9740042&lt;/code&gt; - the "Temperature characteristic"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's also a good idea to fetch the battery level, which follows the officially assigned 16bit UUID for battery service:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6i5cqcz3e15j0e3h36z0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6i5cqcz3e15j0e3h36z0.png" alt="GATT BatteryService" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Connecting to the Thingy:52 from a web application is quite easy, just remember to list the services that might be required for your app in the list of &lt;code&gt;optionalServices&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;THINGY_CONFIGURATION_SERVICE_UUID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ef680100-9b35-4933-9b10-52ffa9740042&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WEATHER_STATION_SERVICE_UUID&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ef680200-9b35-4933-9b10-52ffa9740042&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bluetooth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestDevice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;THINGY_CONFIGURATION_SERVICE_UUID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;optionalServices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;battery_service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;WEATHER_STATION_SERVICE_UUID&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Temperature data
&lt;/h2&gt;

&lt;p&gt;The temperature data is made available under the &lt;code&gt;Weather Station Service&lt;/code&gt; and in order to read the data, you'll need to subscribe to the temperature updates on the &lt;code&gt;Temperature Characteristic&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TEMPERATURE_CHARACTERISTIC_UUID&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ef680201-9b35-4933-9b10-52ffa9740042&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;startThermometerNotifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPrimaryService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WEATHER_STATION_SERVICE_UUID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;characteristic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCharacteristic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TEMPERATURE_CHARACTERISTIC_UUID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;characteristic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;characteristicvaluechanged&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;onThermometerChange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;characteristic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startNotifications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever there is an update, read out the values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;_onThermometerChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;integer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInt8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decimal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;celsius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;celsius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fahrenheit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;celsius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;kelvin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;celsius&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thermometer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;temperature&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Thermometer Web Component
&lt;/h1&gt;

&lt;p&gt;We'll need some visualization for the thermometer, and I found a very nice &lt;a href="https://jsfiddle.net/mirceageorgescu/gBW3Y/" rel="noopener noreferrer"&gt;thermometer made in pure css&lt;/a&gt; by &lt;a href="https://github.com/mirceageorgescu" rel="noopener noreferrer"&gt;Mircea Georgescu&lt;/a&gt;, which will be a good base for a simple thermometer web component.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcnrnc73s1mt6hkx9e5o4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcnrnc73s1mt6hkx9e5o4.png" alt="CSS Thermometer" width="438" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: I am definitely not a CSS expert, so sorry Mircea for the hacks I made :)&lt;/p&gt;

&lt;p&gt;For the purpose of this demo, I have hacked the linear-gradient used for the positioning of the level and the position of the temperature reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;_handleTemperature&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;celsius&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;celsius&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;deg;C`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;celsius&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translateY(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;celsius&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;perc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;celsius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;thermometer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`linear-gradient(#fff 0%, #fff &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;perc&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%, #d00 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;perc&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%, #d00 100%)`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full source of the component is &lt;a href="https://github.com/larsgk/web-thermometer/blob/main/src/thermometer-ui.js" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Building the application
&lt;/h1&gt;

&lt;p&gt;Handling the connection to the Thingy:52 will be done with a scaled down variant what I did in another post: &lt;a href="https://dev.to/denladeside/generic-sensors-and-thingy52-9oa"&gt;Generic Sensors and Thingy:52&lt;/a&gt;.  It injects a small widget in the page that handles connectivity and shows battery levels.&lt;/p&gt;

&lt;p&gt;Besides that (the &lt;code&gt;thingy52-widget&lt;/code&gt;), the application only consists of the new thermometer UI component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;thingy52-widget&amp;gt;&amp;lt;/thingy52-widget&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Web Thermometer&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;thermometer-ui&amp;gt;&amp;lt;/thermometer-ui&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  All working together
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nht0kcb24hhavwoptla.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nht0kcb24hhavwoptla.jpg" alt="Reading from a distance" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjaik84a80elshjo3h6gr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjaik84a80elshjo3h6gr.png" alt="Web Temperature APP" width="377" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/larsgk/web-thermometer" rel="noopener noreferrer"&gt;https://github.com/larsgk/web-thermometer&lt;/a&gt;&lt;br&gt;
Demo: &lt;a href="https://larsgk.github.io/web-thermometer" rel="noopener noreferrer"&gt;https://larsgk.github.io/web-thermometer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, requests, issue reports and PRs are very welcome!&lt;/p&gt;

&lt;p&gt;Enjoy ;)&lt;/p&gt;

</description>
      <category>webcomponents</category>
      <category>webbluetooth</category>
      <category>fugu</category>
    </item>
    <item>
      <title>Generic Sensors and Thingy:52</title>
      <dc:creator>Lars Knudsen 🇩🇰</dc:creator>
      <pubDate>Sun, 30 Jul 2023 20:29:49 +0000</pubDate>
      <link>https://dev.to/denladeside/generic-sensors-and-thingy52-9oa</link>
      <guid>https://dev.to/denladeside/generic-sensors-and-thingy52-9oa</guid>
      <description>&lt;h1&gt;
  
  
  Sensor APIs
&lt;/h1&gt;

&lt;p&gt;For many years, mobile phones have contained a wide range of sensors - primarily to help provide a good user experience when using the device.  E.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proximity sensors, to detect if the user is holding the phone close to the face, preventing accidental input&lt;/li&gt;
&lt;li&gt;Ambient light sensors, to adjust the screen backlight&lt;/li&gt;
&lt;li&gt;Accelerometers to automatically switch orientation or silence the device on orientation change&lt;/li&gt;
&lt;li&gt;GPS receivers and magnetometers to support maps and navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During the early days of modern web browsers on these devices, &lt;a href="https://www.webondevices.com/9-javascript-apis-accessing-device-sensors/" rel="noopener noreferrer"&gt;different approaches&lt;/a&gt; have been made to make a subset of these sensors available to web applications.&lt;/p&gt;

&lt;p&gt;Around 2015, primarily driven by Intel, an effort was done to standardize sensor APIs to provide a consistent way for web applications to retrieve sensor data. The result is the &lt;a href="https://www.w3.org/TR/generic-sensor/" rel="noopener noreferrer"&gt;Generic Sensor API spec&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Today, a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Sensor_APIs" rel="noopener noreferrer"&gt;wide range of sensors&lt;/a&gt; are made available using this API, some are &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AmbientLightSensor" rel="noopener noreferrer"&gt;behind a flag&lt;/a&gt; and more proposals are on the way, e.g. the &lt;a href="https://kenchris.github.io/thermometer/" rel="noopener noreferrer"&gt;TemperatureSensor&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Developer Experience
&lt;/h1&gt;

&lt;p&gt;On desktop computers, these sensors are (usually) not available. This requires developers to have a target phone connected, to run - on target - to get proper sensor data while testing the application under development. It's also possible to spoof the data by using &lt;a href="https://developer.chrome.com/docs/devtools/sensors/" rel="noopener noreferrer"&gt;sensor emulation&lt;/a&gt; in e.g. Chrome DevTools.&lt;/p&gt;

&lt;p&gt;Sometimes neither of these approaches are convenient.  E.g. what if you want to interact with the screen UI while emulating accelerometer based control of a game or maybe the phone you have available just doesn't provide all the sensors you want to test with.&lt;/p&gt;

&lt;p&gt;A third approach proposed here, is to introduce a module that allows the developer to connect to an external device to get live sensor data, while still being able to develop and test on a desktop.&lt;/p&gt;

&lt;p&gt;Ideally, this could be provided inside e.g. DevTools as an extension to the emulation, by capturing sensor data from a connected mobile phone and injecting the data in the application under development.  Until that happens, because of hardware connectivity APIs like &lt;a href="https://developer.chrome.com/articles/bluetooth/" rel="noopener noreferrer"&gt;Web Bluetooth&lt;/a&gt;, &lt;a href="https://developer.chrome.com/articles/usb/" rel="noopener noreferrer"&gt;Web USB&lt;/a&gt; and the &lt;a href="https://developer.chrome.com/articles/serial/" rel="noopener noreferrer"&gt;Serial API&lt;/a&gt;, it's possible to build such a solution already today.&lt;/p&gt;

&lt;h1&gt;
  
  
  Thingy:52
&lt;/h1&gt;

&lt;p&gt;The &lt;a href="https://www.nordicsemi.com/Products/Development-hardware/Nordic-Thingy-52" rel="noopener noreferrer"&gt;Thingy:52&lt;/a&gt; by &lt;a href="https://www.nordicsemi.com/" rel="noopener noreferrer"&gt;Nordic Semiconductor&lt;/a&gt; is a very powerful little unit, packing a lot of onboard sensors and actuators (Audio, button, temperature, humidity, pressure, air quality, color, light, accelerometer, gyroscope, compass/magnetometer, RGB LED and NFC). which are all exposed over &lt;a href="https://learn.adafruit.com/introduction-to-bluetooth-low-energy/gatt" rel="noopener noreferrer"&gt;BLE GATT&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The GATT protocol documentation for the device is available &lt;a href="https://nordicsemiconductor.github.io/Nordic-Thingy52-FW/documentation/firmware_architecture.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Connecting to the Thingy:52 from a web application is quite easy, just remember to list the services that might be required for your app in the list of &lt;code&gt;optionalServices&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bluetooth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestDevice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ef680100-9b35-4933-9b10-52ffa9740042&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;optionalServices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;battery_service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ef680200-9b35-4933-9b10-52ffa9740042&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ef680300-9b35-4933-9b10-52ffa9740042&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ef680400-9b35-4933-9b10-52ffa9740042&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ef680500-9b35-4933-9b10-52ffa9740042&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Hardware driver
&lt;/h1&gt;

&lt;p&gt;In order to make it easily possible to extend the solution with different hardware devices in the future, a simple 'hardware driver' is created.&lt;/p&gt;

&lt;p&gt;I decided to make a singleton instance of a class that extends &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/EventTarget" rel="noopener noreferrer"&gt;EventTarget&lt;/a&gt; and then add the Web Bluetooth hardware access calls inside the class.  This allows any component subscribing to the driver instance (using &lt;code&gt;Thingy52Driver.addEventListener(...)&lt;/code&gt;) to receive sensor data updates without having any knowledge of the underlying Bluetooth APIs and GATT protocol.&lt;/p&gt;

&lt;p&gt;E.g. on Thermometer GATT characteristic updates, an event is emitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;_onThermometerChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;integer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decimal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;celsius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;celsius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fahrenheit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;celsius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;kelvin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;celsius&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thermometer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;temperature&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full implementation is available &lt;a href="https://github.com/larsgk/genericsensor-thingy52/blob/main/src/thingy52-driver.js" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Hijacking the local sensor classes
&lt;/h1&gt;

&lt;p&gt;To make things easy for developers, I decided to make a simple mechanism that allows for specific sensor classes to be 'hijacked', receiving data from a connected Thingy:52 and not from potential local sensors. This is done by replacing the sensor classes available on the global object with hybrids that expose the same API but retrieve sensor data via Web Bluetooth.&lt;/p&gt;

&lt;p&gt;The developer only needs to inject the following (between the comments) in the top of the main html file (usually index.html):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- BEGIN GenericSensor Thingy52 initialization --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/genericsensor-thingy52.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;replaceSensors&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/sensors.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;replaceSensors&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accelerometer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AmbientLightSensor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gyroscope&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Magnetometer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TemperatureSensor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- END GenericSensor Thingy52 initialization --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To connect, disconnect and to see battery level of the Thingy:52, a small controller widget is injected in the lower left corner of the web application.&lt;/p&gt;

&lt;p&gt;A small &lt;a href="https://larsgk.github.io/genericsensor-thingy52/" rel="noopener noreferrer"&gt;demo app&lt;/a&gt; is provided with the &lt;a href="https://github.com/larsgk/genericsensor-thingy52" rel="noopener noreferrer"&gt;repo&lt;/a&gt; containing the driver.&lt;/p&gt;

&lt;p&gt;When connected and up and running, it looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsurpgimyml4rhjotu6in.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsurpgimyml4rhjotu6in.png" alt="Demo app page view" width="779" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Final thoughts
&lt;/h1&gt;

&lt;p&gt;Having an option to only fetch and expose sensor data from a connected device - without the need to also run the full application on the target device would be a nice addition to DevTools.  However, I was pleased to see how easy it was to build this solution by using off the shelf hardware like the Thingy:52 combined with the Web Bluetooth API.&lt;/p&gt;

&lt;p&gt;A link to the repo is here: &lt;a href="https://github.com/larsgk/genericsensor-thingy52" rel="noopener noreferrer"&gt;https://github.com/larsgk/genericsensor-thingy52&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A link to the simple live demo page is here: &lt;a href="https://larsgk.github.io/genericsensor-thingy52/" rel="noopener noreferrer"&gt;https://larsgk.github.io/genericsensor-thingy52/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, request, issue reports and PRs are very welcome!&lt;/p&gt;

&lt;p&gt;Enjoy ;)&lt;/p&gt;

</description>
      <category>sensors</category>
      <category>webbluetooth</category>
      <category>fugu</category>
    </item>
  </channel>
</rss>
