<?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>Forem: Hauke J.</title>
    <description>The latest articles on Forem by Hauke J. (@hauju).</description>
    <link>https://forem.com/hauju</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%2F456688%2Fc61c83d0-149f-4b05-bd7b-901da0f280a3.jpeg</url>
      <title>Forem: Hauke J.</title>
      <link>https://forem.com/hauju</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/hauju"/>
    <language>en</language>
    <item>
      <title>Add a Feedback Widget to Your Next.js App in 5 Minutes</title>
      <dc:creator>Hauke J.</dc:creator>
      <pubDate>Tue, 28 Apr 2026 10:14:38 +0000</pubDate>
      <link>https://forem.com/hauju/add-a-feedback-widget-to-your-nextjs-app-in-5-minutes-4n03</link>
      <guid>https://forem.com/hauju/add-a-feedback-widget-to-your-nextjs-app-in-5-minutes-4n03</guid>
      <description>&lt;p&gt;You shipped a Next.js app. Users are visiting. When something breaks, they just leave. No email, no bug report, no context about what they were doing when it broke.&lt;/p&gt;

&lt;p&gt;This guide adds a floating feedback button to every page. Users can screenshot, annotate, and submit without leaving the page. One script tag, no npm packages, App Router and Pages Router both supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're adding
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;SeggWat&lt;/a&gt; is a feedback widget your users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click on any page (it's a floating button)&lt;/li&gt;
&lt;li&gt;Use to capture their viewport, then annotate with arrows, rectangles, text, and a blackout tool for sensitive bits&lt;/li&gt;
&lt;li&gt;Use to file bug reports, feature requests, or general praise (each gets categorized)&lt;/li&gt;
&lt;li&gt;Pin to a specific release via version tracking&lt;/li&gt;
&lt;li&gt;Trigger without dropping a single cookie. GDPR-friendly, EU-hosted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Submissions land in a dashboard with Kanban triage, analytics, and an Ideas Portal where users can vote on features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Next.js project (App Router or Pages Router, both work)&lt;/li&gt;
&lt;li&gt;A free &lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;SeggWat account&lt;/a&gt; (14-day trial, no credit card)&lt;/li&gt;
&lt;li&gt;Your project key from the SeggWat dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Option 1: App Router (Next.js 13+)
&lt;/h2&gt;

&lt;p&gt;The cleanest place for the script is your root layout. That puts the button on every page without duplication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add the script to your root layout
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;app/layout.tsx&lt;/code&gt; and load SeggWat through Next.js's built-in &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Script&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;next/script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;children&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="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
          &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://seggwat.com/static/widgets/v1/seggwat-feedback.js"&lt;/span&gt;
          &lt;span class="na"&gt;data-project-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_PROJECT_KEY"&lt;/span&gt;
          &lt;span class="na"&gt;data-enable-screenshots&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
          &lt;span class="na"&gt;data-button-position&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bottom-right"&lt;/span&gt;
          &lt;span class="na"&gt;data-button-color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#6366f1"&lt;/span&gt;
          &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;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;That's it. Every page in your app now has a feedback button.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Tag feedback with your app version (optional)
&lt;/h3&gt;

&lt;p&gt;If you want to know which release a bug was reported against, set &lt;code&gt;data-version&lt;/code&gt;. Read it from &lt;code&gt;package.json&lt;/code&gt; or an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://seggwat.com/static/widgets/v1/seggwat-feedback.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-project-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_PROJECT_KEY"&lt;/span&gt;
  &lt;span class="na"&gt;data-enable-screenshots&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="na"&gt;data-version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_APP_VERSION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.1.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every submission is now tagged with the running release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Identify logged-in users (optional)
&lt;/h3&gt;

&lt;p&gt;If your users are authenticated, link feedback to their identity with &lt;code&gt;setUser()&lt;/code&gt;. Drop this in a client component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/SeggWatUser.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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;useEffect&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;react&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;useSession&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;next-auth/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// or your auth provider&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SeggWatUser&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SeggwatFeedback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SeggwatFeedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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="p"&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;session&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;Render &lt;code&gt;&amp;lt;SeggWatUser /&amp;gt;&lt;/code&gt; in your layout next to the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Pages Router
&lt;/h2&gt;

&lt;p&gt;Pages Router users add the script to &lt;code&gt;_app.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pages/_app.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppProps&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;next/app&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="nx"&gt;Script&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;next/script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageProps&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AppProps&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;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pageProps&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://seggwat.com/static/widgets/v1/seggwat-feedback.js"&lt;/span&gt;
        &lt;span class="na"&gt;data-project-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_PROJECT_KEY"&lt;/span&gt;
        &lt;span class="na"&gt;data-enable-screenshots&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
        &lt;span class="na"&gt;data-button-position&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bottom-right"&lt;/span&gt;
        &lt;span class="na"&gt;data-button-color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#6366f1"&lt;/span&gt;
        &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;Same result, same one tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding more widgets
&lt;/h2&gt;

&lt;p&gt;SeggWat ships several widget types. Here are the others worth knowing about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Helpful rating (great for docs)
&lt;/h3&gt;

&lt;p&gt;If you have a &lt;code&gt;/docs&lt;/code&gt; section, drop a "Was this page helpful?" prompt at the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Only on docs pages&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://seggwat.com/static/widgets/v1/seggwat-helpful.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-project-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_PROJECT_KEY"&lt;/span&gt;
  &lt;span class="na"&gt;data-container&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#helpful-container"&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// In your docs component:&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"helpful-container"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Star rating (product pages, features)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://seggwat.com/static/widgets/v1/seggwat-rating.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-project-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_PROJECT_KEY"&lt;/span&gt;
  &lt;span class="na"&gt;data-container&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#rating-container"&lt;/span&gt;
  &lt;span class="na"&gt;data-style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"stars"&lt;/span&gt;
  &lt;span class="na"&gt;data-max-stars&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same widget supports a smiley variant via &lt;code&gt;data-style="smiley"&lt;/code&gt; if stars aren't your aesthetic.&lt;/p&gt;

&lt;h3&gt;
  
  
  NPS survey
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://seggwat.com/static/widgets/v1/seggwat-nps.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-project-key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_PROJECT_KEY"&lt;/span&gt;
  &lt;span class="na"&gt;data-mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"banner"&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NPS only fires when its triggers match (URL pattern, custom event, or time-on-page), and respects per-visitor cooldown so you don't hammer the same person twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customization cheat sheet
&lt;/h2&gt;

&lt;p&gt;Everything is configured through &lt;code&gt;data-&lt;/code&gt; attributes. No CSS overrides.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Values&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-button-position&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bottom-right&lt;/code&gt;, &lt;code&gt;right-side&lt;/code&gt;, &lt;code&gt;icon-only&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bottom-right&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-button-color&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any hex color&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#2563eb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-enable-screenshots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any string&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-language&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;en&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, &lt;code&gt;sv&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Auto-detect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-show-powered-by&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Dark mode
&lt;/h2&gt;

&lt;p&gt;The widget watches &lt;code&gt;prefers-color-scheme&lt;/code&gt; and matches the user's system preference. No setting required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works well in Next.js
&lt;/h2&gt;

&lt;p&gt;A few things worth flagging:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;strategy="afterInteractive"&lt;/code&gt; defers loading until after hydration, so the widget doesn't drag down your Lighthouse score.&lt;/li&gt;
&lt;li&gt;The widget is a vanilla script, not a React component, so it sidesteps server components and streaming SSR entirely. No hydration mismatches.&lt;/li&gt;
&lt;li&gt;Static export (&lt;code&gt;output: 'export'&lt;/code&gt;) works fine. The script loads from a CDN at runtime.&lt;/li&gt;
&lt;li&gt;Edge Runtime is fine for the same reason. Everything happens client-side.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What you get in the dashboard
&lt;/h2&gt;

&lt;p&gt;Once submissions roll in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A live feed that updates as feedback arrives&lt;/li&gt;
&lt;li&gt;A Kanban board for triage: New, Active, Assigned, Resolved, Closed&lt;/li&gt;
&lt;li&gt;Trend charts by type and over time&lt;/li&gt;
&lt;li&gt;An Ideas Portal where users upvote feature requests (auto-populated from feedback)&lt;/li&gt;
&lt;li&gt;GitHub integration: turn an idea into an issue, status syncs both ways&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SeggWat&lt;/th&gt;
&lt;th&gt;Hotjar&lt;/th&gt;
&lt;th&gt;Canny&lt;/th&gt;
&lt;th&gt;Google Forms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In-page widget&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot annotation&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idea voting portal&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR / EU-hosted&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ (US)&lt;/td&gt;
&lt;td&gt;❌ (US)&lt;/td&gt;
&lt;td&gt;❌ (US)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No cookies&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP server (AI-native)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starting price&lt;/td&gt;
&lt;td&gt;$6/mo&lt;/td&gt;
&lt;td&gt;$32/mo&lt;/td&gt;
&lt;td&gt;$79/mo&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;You've got a feedback widget on every page of your Next.js app. Users can screenshot bugs, request features, and rate pages without leaving their session.&lt;/p&gt;

&lt;p&gt;About five minutes of work, ten lines of code, zero npm dependencies.&lt;/p&gt;

&lt;p&gt;Next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The full Next.js example is on &lt;a href="https://github.com/SeggWat/examples-seggwat/tree/main/nextjs" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;Sign up for SeggWat&lt;/a&gt; (14-day free trial)&lt;/li&gt;
&lt;li&gt;Not on Next.js? See the &lt;a href="https://seggwat.com/docs?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;React guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;For programmatic access, the &lt;a href="https://seggwat.com/docs?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;REST API&lt;/a&gt; is documented too&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;SeggWat is an EU-hosted, GDPR-compliant feedback tool built in Rust. No cookies, no tracking scripts, no data sold elsewhere. &lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;Try it free.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>rust</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building dioxus-docs-kit</title>
      <dc:creator>Hauke J.</dc:creator>
      <pubDate>Wed, 22 Apr 2026 09:44:24 +0000</pubDate>
      <link>https://forem.com/hauju/building-dioxus-docs-kit-50c1</link>
      <guid>https://forem.com/hauju/building-dioxus-docs-kit-50c1</guid>
      <description>&lt;p&gt;After moving my docs into my Dioxus app, the next question was pretty obvious.&lt;/p&gt;

&lt;p&gt;What does a reusable docs setup for Dioxus actually need?&lt;/p&gt;

&lt;p&gt;I did not want a giant CMS.&lt;br&gt;
I did not want runtime content fetching.&lt;br&gt;
I definitely did not want a fragile pile of custom glue code that works once and then becomes a weird private framework I regret later.&lt;/p&gt;

&lt;p&gt;I wanted something boring, solid, and reusable.&lt;/p&gt;

&lt;p&gt;That is basically how &lt;code&gt;dioxus-docs-kit&lt;/code&gt; happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the kit includes
&lt;/h2&gt;

&lt;p&gt;Right now, the core pieces are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MDX content rendering&lt;/li&gt;
&lt;li&gt;sidebar navigation from &lt;code&gt;_nav.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;compile-time embedded content&lt;/li&gt;
&lt;li&gt;full-text search&lt;/li&gt;
&lt;li&gt;page navigation&lt;/li&gt;
&lt;li&gt;optional OpenAPI reference pages&lt;/li&gt;
&lt;li&gt;theme switching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which is a fairly unsexy list, honestly.&lt;/p&gt;

&lt;p&gt;That is a compliment.&lt;/p&gt;

&lt;p&gt;These are exactly the boring pieces I kept wanting whenever I looked at docs setups for real app projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why compile-time embedding matters
&lt;/h2&gt;

&lt;p&gt;One of my favorite parts of the setup is that docs content is embedded at compile time.&lt;/p&gt;

&lt;p&gt;The build helper reads &lt;code&gt;_nav.json&lt;/code&gt;, finds the referenced &lt;code&gt;.mdx&lt;/code&gt; files, and generates &lt;code&gt;include_str!()&lt;/code&gt; calls for them.&lt;/p&gt;

&lt;p&gt;So the result is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;content ships with the app binary&lt;/li&gt;
&lt;li&gt;there is no separate content service&lt;/li&gt;
&lt;li&gt;there is no runtime file discovery mess&lt;/li&gt;
&lt;li&gt;deployment stays straightforward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For self-hosted apps, this is great.&lt;/p&gt;

&lt;p&gt;The docs are just there. No second system pretending it is not a second system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigation stays explicit
&lt;/h2&gt;

&lt;p&gt;Navigation is driven by a &lt;code&gt;_nav.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;I like that because it keeps the structure intentional.&lt;/p&gt;

&lt;p&gt;I do not want a docs system making clever guesses about what my sidebar should look like based on whatever folder scanning logic happened to be easy to implement.&lt;/p&gt;

&lt;p&gt;I would rather define the structure once and keep it explicit.&lt;/p&gt;

&lt;p&gt;Boring wins again.&lt;/p&gt;

&lt;h2&gt;
  
  
  MDX without turning everything into a circus
&lt;/h2&gt;

&lt;p&gt;I wanted Markdown-style authoring with enough room for richer content when needed.&lt;/p&gt;

&lt;p&gt;That is where MDX helps.&lt;/p&gt;

&lt;p&gt;Plain Markdown is great right up until it is not. Then suddenly you want richer callouts, custom components, or tighter control over how a page is rendered.&lt;/p&gt;

&lt;p&gt;The goal here was not to make content authoring infinitely flexible.&lt;/p&gt;

&lt;p&gt;The goal was to make it flexible enough without turning the docs pipeline into a cursed build experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search is not optional anymore
&lt;/h2&gt;

&lt;p&gt;Docs without search feel fake surprisingly fast.&lt;/p&gt;

&lt;p&gt;Once a project grows beyond a handful of pages, nobody wants to browse around politely. They want the answer.&lt;/p&gt;

&lt;p&gt;So full-text search is built in.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but it is one of the first things that makes a docs system feel real instead of homemade.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAPI pages are a big quality-of-life win
&lt;/h2&gt;

&lt;p&gt;A lot of app docs eventually need API reference pages.&lt;/p&gt;

&lt;p&gt;That is usually where people give up and bolt on yet another separate tool.&lt;/p&gt;

&lt;p&gt;I did not want that.&lt;/p&gt;

&lt;p&gt;So the kit supports optional OpenAPI integration.&lt;/p&gt;

&lt;p&gt;Same shell. Same navigation. Same deployment. No weird handoff into a different docs universe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for real projects
&lt;/h2&gt;

&lt;p&gt;This is not a crate for demo screenshots.&lt;/p&gt;

&lt;p&gt;It is useful when a Dioxus product has real docs pressure.&lt;/p&gt;

&lt;p&gt;A few obvious examples from my own stuff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SeggWat&lt;/strong&gt; could use it for setup docs, widget docs, API references, and educational docs around feedback workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;infra.page&lt;/strong&gt; could use it for integration setup guides, self-hosting docs, and example dashboard templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StepShots&lt;/strong&gt; could use it for feature docs, workflow guides, and onboarding content that should be searchable instead of buried&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the kind of product context I built it around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Theme switching and app-native integration
&lt;/h2&gt;

&lt;p&gt;Because this lives inside a Dioxus app, the docs can use the same broader UI approach as the rest of the product.&lt;/p&gt;

&lt;p&gt;That sounds small until you live with a split setup for a while.&lt;/p&gt;

&lt;p&gt;Theme switching feels more natural. Routing feels more natural. The whole thing stops feeling like a branded iframe with better typography.&lt;/p&gt;

&lt;h2&gt;
  
  
  One small but very real integration footgun
&lt;/h2&gt;

&lt;p&gt;There is also one annoying practical detail the repo handles explicitly.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;dioxus-docs-kit&lt;/code&gt; is pulled from crates.io, Tailwind CSS 4 cannot scan &lt;code&gt;~/.cargo&lt;/code&gt; paths for class usage.&lt;/p&gt;

&lt;p&gt;So the repo ships a safelist file you can copy into your project and reference from your Tailwind setup.&lt;/p&gt;

&lt;p&gt;Not glamorous. Still the kind of thing that absolutely matters in a real integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual goal
&lt;/h2&gt;

&lt;p&gt;The goal was never "let me build a docs framework because frameworks are fun."&lt;/p&gt;

&lt;p&gt;The goal was simpler.&lt;/p&gt;

&lt;p&gt;Make it easy to ship real docs with a Dioxus app, keep the content in the repo, and avoid turning documentation into another hosted system that slowly drifts away from the product.&lt;/p&gt;

&lt;p&gt;That is the whole idea.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/hauju/dioxus-docs-kit" rel="noopener noreferrer"&gt;https://github.com/hauju/dioxus-docs-kit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webdev</category>
      <category>documentation</category>
    </item>
    <item>
      <title>I Replaced Mintlify With Docs Inside My Dioxus App</title>
      <dc:creator>Hauke J.</dc:creator>
      <pubDate>Tue, 21 Apr 2026 12:36:33 +0000</pubDate>
      <link>https://forem.com/hauju/i-replaced-mintlify-with-docs-inside-my-dioxus-app-3de4</link>
      <guid>https://forem.com/hauju/i-replaced-mintlify-with-docs-inside-my-dioxus-app-3de4</guid>
      <description>&lt;p&gt;A lot of docs tools feel great right up until you want them to stop being "the docs tool" and start being part of your actual product.&lt;/p&gt;

&lt;p&gt;That was the point where Mintlify started annoying me.&lt;/p&gt;

&lt;p&gt;To be fair, Mintlify was not pulling my docs out of the repo. The content was still there. So this is not a "hosted docs bad" rant.&lt;/p&gt;

&lt;p&gt;The real friction was simpler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the docs still felt like a separate hosted surface&lt;/li&gt;
&lt;li&gt;I wanted tighter integration with the app itself&lt;/li&gt;
&lt;li&gt;I wanted full control over routing, rendering, and layout&lt;/li&gt;
&lt;li&gt;I wanted better control over URLs, metadata, internal linking, and the broader SEO shape&lt;/li&gt;
&lt;li&gt;and yes, downtime on the hosted side is still downtime for your docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mintlify can be totally fine. It just stopped fitting what I wanted.&lt;/p&gt;

&lt;p&gt;So I switched.&lt;/p&gt;

&lt;p&gt;I now self-host my docs inside my Dioxus app, keep the docs in the repo, and use my own docs kit instead of outsourcing the whole thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; If you are already building in Dioxus, self-hosting docs inside the app gives you simpler deployment, tighter ownership, and better control over the full docs plus SEO setup than a separate hosted docs surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I wanted docs inside the app
&lt;/h2&gt;

&lt;p&gt;Mostly because I got tired of the split.&lt;/p&gt;

&lt;p&gt;If the docs are part of the product, I want them to live like part of the product.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same repo&lt;/li&gt;
&lt;li&gt;same deployment&lt;/li&gt;
&lt;li&gt;same routing model&lt;/li&gt;
&lt;li&gt;same design system&lt;/li&gt;
&lt;li&gt;same ownership&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not want the docs to feel like a polished annex attached to the real app.&lt;/p&gt;

&lt;p&gt;I wanted them to feel native.&lt;/p&gt;

&lt;p&gt;For a solo builder, every extra moving part becomes maintenance tax eventually. You might not notice it on day one. You definitely notice it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was wrong with the hosted-docs setup
&lt;/h2&gt;

&lt;p&gt;The problem was not that Mintlify was bad at docs.&lt;/p&gt;

&lt;p&gt;The problem was that I no longer wanted docs-as-a-service.&lt;/p&gt;

&lt;p&gt;Hosted docs tools optimize for convenience first. That is fair. That is the whole pitch.&lt;/p&gt;

&lt;p&gt;But once you care about deeper integration, the tradeoff changes.&lt;/p&gt;

&lt;p&gt;I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;docs that feel like part of the app, not adjacent to it&lt;/li&gt;
&lt;li&gt;content stored right next to the code it documents&lt;/li&gt;
&lt;li&gt;a path to publish blog-style pages without bolting on another system&lt;/li&gt;
&lt;li&gt;full control over rendering and shipping&lt;/li&gt;
&lt;li&gt;tighter control over URLs, metadata, internal linking, and how docs pages support the rest of the site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point I was not really asking for a hosted docs product anymore. I was asking for docs that behave like application pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built instead
&lt;/h2&gt;

&lt;p&gt;I built &lt;code&gt;dioxus-docs-kit&lt;/code&gt;, a reusable docs framework for Dioxus 0.7.&lt;/p&gt;

&lt;p&gt;It gives me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MDX-based docs pages&lt;/li&gt;
&lt;li&gt;sidebar navigation from &lt;code&gt;_nav.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;full-text search&lt;/li&gt;
&lt;li&gt;page navigation&lt;/li&gt;
&lt;li&gt;optional OpenAPI reference pages&lt;/li&gt;
&lt;li&gt;theme switching&lt;/li&gt;
&lt;li&gt;compile-time embedded content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That feature list is nice.&lt;/p&gt;

&lt;p&gt;But the bigger win is that it lives inside the app.&lt;/p&gt;

&lt;p&gt;The docs are not a separate product anymore. They are just part of the product.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but it changes a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real examples from my own projects
&lt;/h2&gt;

&lt;p&gt;This only gets interesting when you think about real products, not toy repos.&lt;/p&gt;

&lt;p&gt;That is why I keep looking at it through projects like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://seggwat.com" rel="noopener noreferrer"&gt;SeggWat&lt;/a&gt;&lt;/strong&gt;, where docs, setup guides, widget install docs, and educational content should stay close to the product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://infra.page" rel="noopener noreferrer"&gt;infra.page&lt;/a&gt;&lt;/strong&gt;, where self-hosting docs, integration setup guides, public dashboard templates, and config references all want to live near the app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://stepshots.com" rel="noopener noreferrer"&gt;StepShots&lt;/a&gt;&lt;/strong&gt;, where onboarding docs, workflow guides, and search-friendly content are part of the product experience, not an afterthought&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all three cases, I would rather have docs live with the app than rent them as a separate surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is better for a solo builder
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. One deployment
&lt;/h3&gt;

&lt;p&gt;I deploy the app and the docs together.&lt;/p&gt;

&lt;p&gt;No extra hosted docs service. No separate deployment flow just to update documentation. Fewer places for things to drift or break.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Docs stay closer to reality
&lt;/h3&gt;

&lt;p&gt;When the docs live in the repo, right next to the code, it is harder to forget them.&lt;/p&gt;

&lt;p&gt;Not impossible, sadly. But harder.&lt;/p&gt;

&lt;p&gt;And harder is already a real improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. I own the UX
&lt;/h3&gt;

&lt;p&gt;If I want the docs to match the app, they can.&lt;/p&gt;

&lt;p&gt;If I want custom routing, layout tweaks, embedded product-specific components, or search behavior that fits the app, I can do that without fighting the boundaries of a hosted platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. I get better SEO control
&lt;/h3&gt;

&lt;p&gt;This part matters more than people admit.&lt;/p&gt;

&lt;p&gt;I do not just want docs pages to exist. I want control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL structure&lt;/li&gt;
&lt;li&gt;page metadata&lt;/li&gt;
&lt;li&gt;canonical rules&lt;/li&gt;
&lt;li&gt;internal linking&lt;/li&gt;
&lt;li&gt;how docs connect to blog content&lt;/li&gt;
&lt;li&gt;how blog content connects back to product pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much easier when the docs are part of the same system instead of a separate hosted surface with its own defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff
&lt;/h2&gt;

&lt;p&gt;Obviously this is not the right choice for everyone.&lt;/p&gt;

&lt;p&gt;If your goal is to ship decent docs this afternoon with almost no engineering effort, a hosted docs tool is still a pretty reasonable choice.&lt;/p&gt;

&lt;p&gt;But if you are already building in Dioxus, and you want docs to feel native, self-hosting starts looking a lot better.&lt;/p&gt;

&lt;p&gt;Especially if you are a solo builder and you care about long-term leverage more than short-term convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I turned it into a reusable kit
&lt;/h2&gt;

&lt;p&gt;At first this was just me scratching my own itch.&lt;/p&gt;

&lt;p&gt;Then it became pretty obvious that other Dioxus apps would want the same thing.&lt;/p&gt;

&lt;p&gt;So I pulled it into a reusable package instead of hiding the setup inside one project.&lt;/p&gt;

&lt;p&gt;That is what &lt;code&gt;dioxus-docs-kit&lt;/code&gt; is for.&lt;/p&gt;

&lt;p&gt;A docs site framework for Dioxus apps that keeps content local, ships with the app, and still gives you the important docs features people expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;In the next post, I will break down what is actually inside &lt;code&gt;dioxus-docs-kit&lt;/code&gt;, including MDX support, compile-time content embedding, search, OpenAPI pages, and theming.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/hauju/dioxus-docs-kit" rel="noopener noreferrer"&gt;https://github.com/hauju/dioxus-docs-kit&lt;/a&gt;&lt;br&gt;
Crate: &lt;a href="https://crates.io/crates/dioxus-docs-kit" rel="noopener noreferrer"&gt;https://crates.io/crates/dioxus-docs-kit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webdev</category>
      <category>documentation</category>
    </item>
    <item>
      <title>In-App Feedback for SaaS: Why Widgets Beat Forms in 2026</title>
      <dc:creator>Hauke J.</dc:creator>
      <pubDate>Thu, 16 Apr 2026 08:29:56 +0000</pubDate>
      <link>https://forem.com/hauju/in-app-feedback-for-saas-why-widgets-beat-forms-in-2026-24nm</link>
      <guid>https://forem.com/hauju/in-app-feedback-for-saas-why-widgets-beat-forms-in-2026-24nm</guid>
      <description>&lt;p&gt;In-app feedback is one of those things teams usually ignore until they realize their current setup sucks.&lt;/p&gt;

&lt;p&gt;A user hits a bug. They open support chat, send a vague email, or quietly disappear. If you are lucky, they fill out a form. If you are not, you get silence.&lt;/p&gt;

&lt;p&gt;That is why &lt;strong&gt;in-app feedback&lt;/strong&gt; matters. Instead of asking users to leave your product and explain everything from memory, you let them send feedback right where the problem happened.&lt;/p&gt;

&lt;p&gt;For SaaS teams, that usually means better bug reports, more product ideas, and fewer useless back-and-forth messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is in-app feedback?
&lt;/h2&gt;

&lt;p&gt;In-app feedback is feedback collected directly inside your product.&lt;/p&gt;

&lt;p&gt;Usually that means a small feedback button or widget that lets users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;report bugs&lt;/li&gt;
&lt;li&gt;suggest features&lt;/li&gt;
&lt;li&gt;rate a page or flow&lt;/li&gt;
&lt;li&gt;leave contextual comments&lt;/li&gt;
&lt;li&gt;attach a screenshot of what they are seeing&lt;/li&gt;
&lt;/ul&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%2Fu7xx953hl8ooy66iwm4u.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%2Fu7xx953hl8ooy66iwm4u.png" alt=" " width="504" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important part is not the UI chrome. It is the context.&lt;/p&gt;

&lt;p&gt;If feedback happens on the page where the issue appears, you get a lot more signal. You know what the user was looking at, what page they were on, and often which browser, viewport, or app version was involved.&lt;/p&gt;

&lt;p&gt;That is a completely different quality level than a generic contact form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why forms are bad at product feedback
&lt;/h2&gt;

&lt;p&gt;Forms are fine for lead capture, surveys, and support intake.&lt;/p&gt;

&lt;p&gt;They are usually bad at product feedback.&lt;/p&gt;

&lt;p&gt;Here is the problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the user has to leave the current flow&lt;/li&gt;
&lt;li&gt;they have to describe the issue from memory&lt;/li&gt;
&lt;li&gt;you have to reconstruct what actually happened&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is a stupid amount of friction for something that should take 20 seconds.&lt;/p&gt;

&lt;p&gt;A generic form also misses the details product teams actually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;page URL&lt;/li&gt;
&lt;li&gt;screenshot context&lt;/li&gt;
&lt;li&gt;browser and viewport&lt;/li&gt;
&lt;li&gt;release version&lt;/li&gt;
&lt;li&gt;category of feedback&lt;/li&gt;
&lt;li&gt;relationship to a specific feature or workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the report you receive is often some variation of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The button does not work"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cool. Which button? On which page? On mobile? After which step? Was it a bug, confusion, or just bad copy?&lt;/p&gt;

&lt;p&gt;Now somebody on the team has to play detective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why in-app feedback works better
&lt;/h2&gt;

&lt;p&gt;In-app feedback works because it catches the moment while it is still fresh.&lt;/p&gt;

&lt;p&gt;The user does not need to remember what happened. They are already there.&lt;/p&gt;

&lt;p&gt;That changes a few things immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. You get more submissions
&lt;/h3&gt;

&lt;p&gt;Every extra step kills response rate.&lt;/p&gt;

&lt;p&gt;If the feedback button is already inside the product, users are much more likely to send a quick report or idea.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The feedback is more specific
&lt;/h3&gt;

&lt;p&gt;When users can annotate a screenshot or submit feedback from the current page, the report stops being abstract.&lt;/p&gt;

&lt;p&gt;Instead of "search is weird," you get "search results overlap the filter drawer on iPhone width."&lt;/p&gt;

&lt;p&gt;That is useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Product and engineering can act faster
&lt;/h3&gt;

&lt;p&gt;Context cuts down the follow-up loop.&lt;/p&gt;

&lt;p&gt;If the report already includes the page, screenshot, and version, triage gets much easier. The team can spend less time clarifying and more time fixing.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Feedback becomes part of the product, not an external chore
&lt;/h3&gt;

&lt;p&gt;This is the bigger shift.&lt;/p&gt;

&lt;p&gt;A feedback form feels like an extra task. A widget feels like part of the app.&lt;/p&gt;

&lt;p&gt;That sounds minor, but it changes behavior. Users are more willing to report friction when the reporting path is right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good in-app feedback should include
&lt;/h2&gt;

&lt;p&gt;Not every feedback widget is worth using. Some are just forms stuffed into a modal.&lt;/p&gt;

&lt;p&gt;A solid setup should cover at least these basics.&lt;/p&gt;

&lt;h2&gt;
  
  
  In-app feedback essentials for SaaS teams
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Feedback button or embedded entry point
&lt;/h3&gt;

&lt;p&gt;Users need an obvious way to send feedback without hunting for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshot capture and annotation
&lt;/h3&gt;

&lt;p&gt;This is huge for bug reports and UI issues. A screenshot saves an absurd amount of clarification work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Categories or feedback types
&lt;/h3&gt;

&lt;p&gt;Bug report, idea, rating, and general feedback should not all land in one giant pile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metadata capture
&lt;/h3&gt;

&lt;p&gt;At minimum: page, timestamp, and some basic environment details. Version tracking is even better.&lt;/p&gt;

&lt;h3&gt;
  
  
  Triage workflow
&lt;/h3&gt;

&lt;p&gt;If feedback lands in email or a raw spreadsheet, you are back in chaos. It should go into a dashboard where you can sort, review, and act on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Privacy-friendly implementation
&lt;/h3&gt;

&lt;p&gt;A lot of teams do not want extra cookie banners or surveillance-heavy scripts just to collect product feedback. Fair enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where SeggWat fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;SeggWat&lt;/a&gt; is built for this exact use case.&lt;/p&gt;

&lt;p&gt;Instead of sending users to a generic form, you add a lightweight feedback widget directly to your site or app. Users can submit bug reports, feature ideas, ratings, or comments without leaving the page.&lt;/p&gt;

&lt;p&gt;A few details make it especially useful for small SaaS teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;screenshot annotation&lt;/strong&gt; for visual bug reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rating widgets&lt;/strong&gt; including NPS, star, and helpful ratings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ideas portal&lt;/strong&gt; if you want voting and roadmap-style feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;developer-friendly integration&lt;/strong&gt; instead of a giant install project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;privacy-first setup&lt;/strong&gt; with EU hosting and no cookie-heavy nonsense&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a better fit for builders who want product feedback infrastructure without buying an enterprise suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  When in-app feedback is worth adding
&lt;/h2&gt;

&lt;p&gt;If your product has any of these problems, you probably need it now, not later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users report bugs with no context&lt;/li&gt;
&lt;li&gt;feature requests are scattered across email, chat, and notes&lt;/li&gt;
&lt;li&gt;support keeps forwarding vague product complaints&lt;/li&gt;
&lt;li&gt;you are shipping fast and need tighter feedback loops&lt;/li&gt;
&lt;li&gt;you keep saying "we should probably add a feedback widget"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is usually the giveaway.&lt;/p&gt;

&lt;p&gt;Early-stage SaaS teams often think they are too small for a proper feedback system. I think the opposite is true.&lt;/p&gt;

&lt;p&gt;When you only have a small number of active users, each signal matters more. Losing context at that stage is expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a simple form is still enough
&lt;/h2&gt;

&lt;p&gt;To be fair, sometimes a plain form is fine.&lt;/p&gt;

&lt;p&gt;If you just need a basic survey or a one-off contact page, do not overcomplicate it.&lt;/p&gt;

&lt;p&gt;But if your goal is ongoing product feedback inside a web app, forms hit their limit fast.&lt;/p&gt;

&lt;p&gt;They are detached from the product, bad at visual context, and annoying for users at exactly the moment you want the least friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical takeaway
&lt;/h2&gt;

&lt;p&gt;If you want better product decisions, you need better raw input.&lt;/p&gt;

&lt;p&gt;For SaaS teams, &lt;strong&gt;in-app feedback&lt;/strong&gt; is usually the simplest way to get that. It keeps feedback close to the experience, improves report quality, and makes it much easier to close the loop.&lt;/p&gt;

&lt;p&gt;A generic form can collect words.&lt;/p&gt;

&lt;p&gt;An in-app feedback widget can collect usable product signals.&lt;/p&gt;

&lt;p&gt;That difference is the whole game.&lt;/p&gt;

&lt;p&gt;If you want to try it without bolting on enterprise nonsense, &lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;SeggWat&lt;/a&gt; is a straightforward place to start.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>sideprojects</category>
      <category>sass</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why I Picked Rust for a Production-Ready Micro SaaS</title>
      <dc:creator>Hauke J.</dc:creator>
      <pubDate>Wed, 15 Apr 2026 08:11:24 +0000</pubDate>
      <link>https://forem.com/hauju/why-i-picked-rust-for-a-production-ready-micro-saas-3bjg</link>
      <guid>https://forem.com/hauju/why-i-picked-rust-for-a-production-ready-micro-saas-3bjg</guid>
      <description>&lt;p&gt;A lot of people hear "micro SaaS" and immediately reach for the usual stack.&lt;/p&gt;

&lt;p&gt;Use JavaScript. Ship fast. Fix the weird production issues later. Maybe never.&lt;/p&gt;

&lt;p&gt;That works, sometimes. But if you are building a product you actually want to run for years, the stack decision is not just about how fast you can get v1 online. It is about how much nonsense you are signing up to maintain.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://seggwat.com" rel="noopener noreferrer"&gt;SeggWat&lt;/a&gt; in Rust because I wanted a product that feels boring in production, cheap to run, and pleasant to extend. For a small SaaS, that matters more than trendy takes about founder speed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Rust is a strong choice for a micro SaaS if you care about reliability, low memory usage, predictable performance, and long-term maintainability. It is a worse choice if your main goal is hacking together a throwaway MVP in a weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual argument against Rust
&lt;/h2&gt;

&lt;p&gt;The common pushback is easy to summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rust is slower to write&lt;/li&gt;
&lt;li&gt;The ecosystem is "harder"&lt;/li&gt;
&lt;li&gt;Hiring is tougher&lt;/li&gt;
&lt;li&gt;You do not need that much performance for a small SaaS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fair enough. If your product is a landing page, a Stripe webhook, and three cron jobs, Rust might indeed be overkill.&lt;/p&gt;

&lt;p&gt;But that is not most SaaS products for very long.&lt;/p&gt;

&lt;p&gt;Once you have real users, state, authentication, background jobs, integrations, analytics, dashboards, and support edge cases, production complexity shows up fast. At that point, the question changes from "what helps me ship fastest this weekend?" to "what will break least often six months from now?"&lt;/p&gt;

&lt;p&gt;That is where Rust starts looking much better.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually wanted from the stack
&lt;/h2&gt;

&lt;p&gt;When building SeggWat, I did not optimize for resume points or language fashion. I optimized for a few practical things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Low operational overhead&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good performance on modest infrastructure&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidence when refactoring&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strong support for concurrent workloads&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A product that would not slowly turn into a haunted house&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SeggWat handles feedback widgets, screenshot-related flows, ratings, idea tracking, dashboard features, and integrations. It is not Google-scale, but it is also not a toy script. I wanted the backend to stay lean and predictable as features accumulated.&lt;/p&gt;

&lt;p&gt;Rust fit that goal better than the usual dynamic-language default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability matters more than raw speed
&lt;/h2&gt;

&lt;p&gt;People often frame Rust as a performance language. That is true, but for SaaS the bigger win is reliability.&lt;/p&gt;

&lt;p&gt;Memory safety without garbage-collector pauses is nice. The real prize is catching entire categories of bugs before deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use-after-free bugs&lt;/li&gt;
&lt;li&gt;accidental shared mutable state&lt;/li&gt;
&lt;li&gt;sloppy null-ish handling&lt;/li&gt;
&lt;li&gt;a bunch of async mistakes that become production incidents elsewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That does not make bugs disappear. Sadly, no compiler has reached wizard status yet.&lt;/p&gt;

&lt;p&gt;But Rust does force you to be explicit in ways that pay off once the codebase grows. I trust refactors more in Rust than I do in looser stacks, especially when async code, background jobs, and state transitions are involved.&lt;/p&gt;

&lt;p&gt;For a solo founder, that trust is huge. You do not have a QA department. Future-you is the QA department.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small servers, real product
&lt;/h2&gt;

&lt;p&gt;One of the underrated advantages of Rust is how much work you can get done on small machines.&lt;/p&gt;

&lt;p&gt;For a micro SaaS, infrastructure costs are not just a line item. They directly affect your margin and your stress level.&lt;/p&gt;

&lt;p&gt;A backend that starts fast, uses little memory, and handles concurrency efficiently lets you stay on simpler infrastructure longer. That buys you time. And time is basically runway with better branding.&lt;/p&gt;

&lt;p&gt;SeggWat is not trying to impress people with giant infra. Quite the opposite. I want the product to be affordable, privacy-friendly, and efficient. Rust supports that philosophy well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rust changes how you design software
&lt;/h2&gt;

&lt;p&gt;This is the part people miss.&lt;/p&gt;

&lt;p&gt;Rust is not just a faster runtime. It nudges you toward better system design.&lt;/p&gt;

&lt;p&gt;Because ownership, types, and error handling are stricter, you end up thinking more clearly about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what state exists&lt;/li&gt;
&lt;li&gt;who owns it&lt;/li&gt;
&lt;li&gt;what can fail&lt;/li&gt;
&lt;li&gt;what should be async&lt;/li&gt;
&lt;li&gt;where boundaries between modules should live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That extra friction can feel annoying early on. Then, later, it feels like a gift.&lt;/p&gt;

&lt;p&gt;In many fast-moving SaaS codebases, complexity gets hidden until it bites you. In Rust, complexity tends to show up earlier, while it is still cheap to fix.&lt;/p&gt;

&lt;p&gt;I will take earlier discomfort over late-night production archaeology every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff is real, not imaginary
&lt;/h2&gt;

&lt;p&gt;I am not going to do the weird marketing thing where Rust magically has no downside.&lt;/p&gt;

&lt;p&gt;It does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rust is slower for rough prototypes
&lt;/h3&gt;

&lt;p&gt;If you are validating whether anyone wants the product at all, a higher-level stack may let you test ideas faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some libraries are less polished than the mainstream equivalents
&lt;/h3&gt;

&lt;p&gt;The web ecosystem is good now, much better than it used to be, but you still hit rough edges.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compile times can be annoying
&lt;/h3&gt;

&lt;p&gt;Not catastrophic. Still annoying.&lt;/p&gt;

&lt;h3&gt;
  
  
  You need to enjoy explicitness at least a little
&lt;/h3&gt;

&lt;p&gt;If you hate types, ownership, and reading compiler messages, Rust will feel like punishment instead of leverage.&lt;/p&gt;

&lt;p&gt;So yes, there is a cost. The point is that the cost buys something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rust still made sense for SeggWat
&lt;/h2&gt;

&lt;p&gt;SeggWat is a feedback product for SaaS teams. It needs to be trustworthy.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;widgets should stay lightweight&lt;/li&gt;
&lt;li&gt;the backend should be stable&lt;/li&gt;
&lt;li&gt;privacy-sensitive data should be handled carefully&lt;/li&gt;
&lt;li&gt;the product should not need bloated infrastructure to stay responsive&lt;/li&gt;
&lt;li&gt;new features should not make the system feel shakier every month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rust maps well to that.&lt;/p&gt;

&lt;p&gt;It also matches the audience. A lot of SeggWat users are technical founders and developer-led teams. "Built in Rust" is not the whole value proposition, but it does signal a certain engineering mindset: practical, efficient, and not interested in enterprise fluff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I recommend Rust for every micro SaaS?
&lt;/h2&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;I would recommend Rust when most of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you plan to run the product for years, not months&lt;/li&gt;
&lt;li&gt;you care about low infra costs&lt;/li&gt;
&lt;li&gt;reliability matters a lot&lt;/li&gt;
&lt;li&gt;the product has meaningful backend logic&lt;/li&gt;
&lt;li&gt;you are comfortable trading some early velocity for long-term calm&lt;/li&gt;
&lt;li&gt;you actually like working in Rust&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not recommend Rust if you are optimizing for the fastest possible no-code-to-v1 sprint, or if the backend is so thin that the language choice barely matters.&lt;/p&gt;

&lt;p&gt;Use the boring tool that matches the real shape of the product.&lt;/p&gt;

&lt;p&gt;For SeggWat, that boring tool turned out to be Rust. Which is a very Rust sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger lesson
&lt;/h2&gt;

&lt;p&gt;The stack question is usually framed too narrowly.&lt;/p&gt;

&lt;p&gt;People ask, "What helps me ship fastest?"&lt;/p&gt;

&lt;p&gt;A better question is, "What helps me keep shipping without hating my life?"&lt;/p&gt;

&lt;p&gt;For me, Rust wins that second question by a lot.&lt;/p&gt;

&lt;p&gt;It makes some early steps slower. Then it pays back through stability, performance, and confidence. For a production-minded micro SaaS, that is a trade I am very happy to make.&lt;/p&gt;

&lt;p&gt;If you are building a SaaS and want a feedback system that reflects those same priorities, take a look at &lt;a href="https://seggwat.com" rel="noopener noreferrer"&gt;SeggWat&lt;/a&gt;. It is built for small teams that want useful feedback without enterprise bloat, privacy headaches, or a giant integration project.&lt;/p&gt;

&lt;p&gt;What stack did you choose for your SaaS, and would you make the same call again?&lt;/p&gt;

</description>
      <category>rust</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Add a Feedback Widget to Your Astro Site in 5 Minutes</title>
      <dc:creator>Hauke J.</dc:creator>
      <pubDate>Tue, 14 Apr 2026 12:11:23 +0000</pubDate>
      <link>https://forem.com/hauju/how-to-add-a-feedback-widget-to-your-astro-site-in-5-minutes-131l</link>
      <guid>https://forem.com/hauju/how-to-add-a-feedback-widget-to-your-astro-site-in-5-minutes-131l</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Drop a single &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag into your Astro layout. No npm install, no adapter config, no island architecture gymnastics. Works with static output, SSR, and hybrid rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You shipped your Astro site. Lighthouse score is pristine. But you have no idea what your users actually think.&lt;/p&gt;

&lt;p&gt;Contact forms collect dust. Email links get ignored. And the one bug report you did get said "it's broken" with no context — no browser, no viewport, no screenshot.&lt;/p&gt;

&lt;p&gt;You need a feedback channel that's &lt;em&gt;right there&lt;/em&gt; when users hit a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You're Adding
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;SeggWat&lt;/a&gt; is an embeddable feedback widget that gives your users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A floating feedback button (bug reports, feature requests, general feedback)&lt;/li&gt;
&lt;li&gt;Built-in screenshot annotation (arrows, rectangles, blackout sensitive areas)&lt;/li&gt;
&lt;li&gt;Optional star ratings, helpful ratings, and contact forms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All feedback lands in a dashboard with Kanban triage, analytics, and a public ideas portal.&lt;/p&gt;

&lt;p&gt;No cookies. No tracking scripts. GDPR compliant by default, EU-hosted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Astro project (v2+, any rendering mode)&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;SeggWat account&lt;/a&gt; (14-day free trial)&lt;/li&gt;
&lt;li&gt;Your project key from the SeggWat dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Add the Script to Your Base Layout
&lt;/h2&gt;

&lt;p&gt;Astro's layout components are the perfect place for global scripts. Open your base layout (usually &lt;code&gt;src/layouts/Layout.astro&lt;/code&gt; or &lt;code&gt;src/layouts/BaseLayout.astro&lt;/code&gt;) and add the widget script before the closing &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/layouts/Layout.astro
interface Props {
  title: string;
}

const { title } = Astro.props;
---

&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset="UTF-8" /&amp;gt;
    &amp;lt;meta name="viewport" content="width=device-width" /&amp;gt;
    &amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;slot /&amp;gt;

    &amp;lt;!-- SeggWat Feedback Widget --&amp;gt;
    &amp;lt;script
      is:inline
      defer
      src="https://seggwat.com/static/widgets/v1/seggwat-feedback.js"
      data-project-key="YOUR_PROJECT_KEY"
      data-enable-screenshots="true"
    &amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_PROJECT_KEY&lt;/code&gt; with the key from your SeggWat dashboard → Project Settings → Widgets.&lt;/p&gt;

&lt;p&gt;That's it. Build, deploy, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Customize the Widget
&lt;/h2&gt;

&lt;p&gt;SeggWat uses &lt;code&gt;data-&lt;/code&gt; attributes for configuration — no JavaScript API calls needed for basic setup. Here are the most useful options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script
  is:inline
  defer
  src="https://seggwat.com/static/widgets/v1/seggwat-feedback.js"
  data-project-key="YOUR_PROJECT_KEY"
  data-enable-screenshots="true"
  data-button-color="#6366f1"
  data-button-position="bottom-right"
  data-version={import.meta.env.PUBLIC_APP_VERSION ?? "dev"}
&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Options&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-button-color&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#2563eb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any hex color&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-button-position&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bottom-right&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bottom-right&lt;/code&gt;, &lt;code&gt;right-side&lt;/code&gt;, &lt;code&gt;icon-only&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-enable-screenshots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Any string (e.g., &lt;code&gt;"1.2.0"&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-language&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-detect&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;en&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, &lt;code&gt;sv&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data-show-powered-by&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Version Tracking Tip
&lt;/h3&gt;

&lt;p&gt;Notice the &lt;code&gt;data-version&lt;/code&gt; attribute above? It pulls from an environment variable. Set &lt;code&gt;PUBLIC_APP_VERSION&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;PUBLIC_APP_VERSION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0.3.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every feedback submission gets tagged with the version, so you know exactly which release triggered a bug report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add Per-Page Widgets (Optional)
&lt;/h2&gt;

&lt;p&gt;Want a "Was this helpful?" widget on your docs pages? Add the helpful rating widget to specific layouts or pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/layouts/DocsLayout.astro
import Layout from './Layout.astro';

const { title } = Astro.props;
---

&amp;lt;Layout title={title}&amp;gt;
  &amp;lt;article&amp;gt;
    &amp;lt;slot /&amp;gt;
  &amp;lt;/article&amp;gt;

  &amp;lt;div class="feedback-section"&amp;gt;
    &amp;lt;p&amp;gt;Was this page helpful?&amp;lt;/p&amp;gt;
    &amp;lt;div id="helpful-widget"&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;script
    is:inline
    defer
    src="https://seggwat.com/static/widgets/v1/seggwat-helpful.js"
    data-project-key="YOUR_PROJECT_KEY"
    data-container="#helpful-widget"
  &amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/Layout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This renders an inline thumbs-up/thumbs-down widget right below your content. No floating button — it sits in the page flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Track Logged-In Users (Optional)
&lt;/h2&gt;

&lt;p&gt;If your Astro site has authentication (via middleware, cookies, or an auth adapter), you can associate feedback with specific users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script
  is:inline
  defer
  src="https://seggwat.com/static/widgets/v1/seggwat-feedback.js"
  data-project-key="YOUR_PROJECT_KEY"
  data-enable-screenshots="true"
&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;script&amp;gt;
  // After widget loads, identify the user
  document.addEventListener('DOMContentLoaded', () =&amp;gt; {
    if (window.SeggwatFeedback) {
      window.SeggwatFeedback.setUser('user-123');
    }
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now feedback submissions are tied to that user ID in your dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Works With Every Astro Rendering Mode
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Works?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static (&lt;code&gt;output: 'static'&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Script loads client-side, no adapter needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR (&lt;code&gt;output: 'server'&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Same — widget is purely client-side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid (&lt;code&gt;output: 'hybrid'&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Same behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The widget is a client-side script — it doesn't depend on how the HTML was generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Works With Starlight Too
&lt;/h2&gt;

&lt;p&gt;Building docs with &lt;a href="https://starlight.astro.build/" rel="noopener noreferrer"&gt;Starlight&lt;/a&gt;? Override the layout to inject the widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/components/StarlightFeedback.astro
---

&amp;lt;script
  is:inline
  defer
  src="https://seggwat.com/static/widgets/v1/seggwat-feedback.js"
  data-project-key="YOUR_PROJECT_KEY"
  data-enable-screenshots="true"
  data-button-position="bottom-left"
&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add it to your Starlight config's custom components. Now your docs site has a feedback button, and users can screenshot exactly which section confused them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens Next
&lt;/h2&gt;

&lt;p&gt;Once deployed, feedback starts flowing into your &lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;SeggWat dashboard&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Triage&lt;/strong&gt; — New feedback lands in a Kanban board. Drag from New → Active → Resolved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots&lt;/strong&gt; — Users' annotated screenshots show exactly what they see. No more "can you send a screenshot?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt; — Track feedback trends, satisfaction scores, team response times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ideas Portal&lt;/strong&gt; — Promote feature requests to a public portal where users can vote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Integration&lt;/strong&gt; — Create GitHub issues from ideas, with automatic status sync.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Quick Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SeggWat&lt;/th&gt;
&lt;th&gt;Contact form&lt;/th&gt;
&lt;th&gt;Google Form&lt;/th&gt;
&lt;th&gt;Hotjar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In-page feedback&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot annotation&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No cookies&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;60 seconds&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;5+ min&lt;/td&gt;
&lt;td&gt;10+ min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR (EU-hosted)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starts at&lt;/td&gt;
&lt;td&gt;$6/mo&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$32/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;One script tag. Real feedback with context.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/SeggWat/examples-seggwat/tree/main/astro" rel="noopener noreferrer"&gt;full Astro example on GitHub&lt;/a&gt; for a working integration with dynamic controls.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;Start your 14-day free trial&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Rust. Hosted in the EU. No cookies. &lt;a href="https://seggwat.com?utm_source=devto&amp;amp;utm_medium=parasite-seo&amp;amp;utm_campaign=feedback-widget" rel="noopener noreferrer"&gt;Learn more about SeggWat&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>sass</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Writing a Rust Driver for the Sensirion SEN5x Air Quality Sensor</title>
      <dc:creator>Hauke J.</dc:creator>
      <pubDate>Tue, 14 Apr 2026 06:26:17 +0000</pubDate>
      <link>https://forem.com/hauju/writing-a-rust-driver-for-the-sensirion-sen5x-air-quality-sensor-27fb</link>
      <guid>https://forem.com/hauju/writing-a-rust-driver-for-the-sensirion-sen5x-air-quality-sensor-27fb</guid>
      <description>&lt;p&gt;If you have ever tried to integrate an environmental sensor into a Rust embedded project, you know the drill: vendor SDKs are C-only, community crates may not exist for your specific sensor, and the datasheet is your best friend. I recently went through this with the Sensirion SEN5x module and want to share what I learned building a &lt;code&gt;no_std&lt;/code&gt; Rust driver from scratch.&lt;/p&gt;

&lt;p&gt;The SEN5x is a multi-sensor module from Sensirion that measures particulate matter (PM1.0, PM2.5, PM4.0, PM10), volatile organic compounds (VOC index), nitrogen oxide (NOx index), temperature, and humidity, all over a single I2C bus. That is a lot of environmental data from one package.&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%2Fr2cp5b62de0z21vkipyk.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%2Fr2cp5b62de0z21vkipyk.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is how to build a driver for it in Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A microcontroller with I2C support (I used an RP2040, but any target with &lt;code&gt;embedded-hal&lt;/code&gt; I2C traits works)&lt;/li&gt;
&lt;li&gt;The SEN5x module (SEN50, SEN54, or SEN55 -- the communication protocol is identical)&lt;/li&gt;
&lt;li&gt;A Rust toolchain with your target configured (&lt;code&gt;thumbv6m-none-eabi&lt;/code&gt; for RP2040, &lt;code&gt;thumbv7em-none-eabihf&lt;/code&gt; for STM32, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your &lt;code&gt;Cargo.toml&lt;/code&gt; dependencies will look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;embedded-hal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are intentionally keeping dependencies minimal. The driver itself only needs &lt;code&gt;embedded-hal&lt;/code&gt; traits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the SEN5x I2C Protocol
&lt;/h2&gt;

&lt;p&gt;The SEN5x uses Sensirion's standard I2C protocol, which has a few quirks compared to a typical I2C device:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Commands are 16-bit.&lt;/strong&gt; You send two bytes for each command, MSB first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every data word is 3 bytes.&lt;/strong&gt; Two bytes of data followed by a CRC-8 checksum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing matters.&lt;/strong&gt; Some commands need execution delays before you can read the response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means a typical exchange looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write: [cmd_msb, cmd_lsb]
Wait: execution time
Read:  [data_hi, data_lo, crc, data_hi, data_lo, crc, ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The CRC-8 Implementation
&lt;/h2&gt;

&lt;p&gt;Sensirion uses CRC-8 with polynomial &lt;code&gt;0x31&lt;/code&gt; and initial value &lt;code&gt;0xFF&lt;/code&gt;. This is the first thing to implement because every data word needs CRC validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;crc8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;crc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;0x80&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;crc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&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;0x31&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="n"&gt;crc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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="n"&gt;crc&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs on any &lt;code&gt;no_std&lt;/code&gt; target with no allocations. Fast and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structuring the Driver
&lt;/h2&gt;

&lt;p&gt;I like to structure embedded drivers around the &lt;code&gt;embedded-hal&lt;/code&gt; traits so they work on any platform. The core struct is generic over the I2C implementation and a delay provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;embedded_hal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;I2c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;embedded_hal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DelayNs&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;SEN5X_ADDR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0x69&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Sen5x&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Sen5x&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt;
    &lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;I2c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DelayNs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&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 default I2C address for SEN5x is &lt;code&gt;0x69&lt;/code&gt;. It is not configurable -- every SEN5x module uses this address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending Commands
&lt;/h2&gt;

&lt;p&gt;Every command follows the same pattern: write two bytes, optionally wait, optionally read back data. Let us build a helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Sen5x&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt;
    &lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;I2c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DelayNs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;write_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nn"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&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;let&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="nf"&gt;.to_be_bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.i2c&lt;/span&gt;&lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SEN5X_ADDR&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;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;read_words_no_alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;delay_us&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u16&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.write_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;I2c&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="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.delay&lt;/span&gt;&lt;span class="nf"&gt;.delay_us&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_us&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;num_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;read_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;num_words&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.i2c&lt;/span&gt;
            &lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SEN5X_ADDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;read_len&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;I2c&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="k"&gt;for&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;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;read_len&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.chunks&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="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;crc8&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;chunk&lt;/span&gt;&lt;span class="p"&gt;[&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;crc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;chunk&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="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Crc&lt;/span&gt;&lt;span class="p"&gt;);&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;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u16&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_be_bytes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;chunk&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;chunk&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="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;Ok&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;This &lt;code&gt;no_std&lt;/code&gt;-friendly version uses a fixed-size stack buffer. No heap allocations, no &lt;code&gt;alloc&lt;/code&gt; crate needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling
&lt;/h2&gt;

&lt;p&gt;A proper driver needs a clear error type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// I2C bus error&lt;/span&gt;
    &lt;span class="nf"&gt;I2c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="cd"&gt;/// CRC checksum mismatch&lt;/span&gt;
    &lt;span class="n"&gt;Crc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="cd"&gt;/// Command not allowed in current state&lt;/span&gt;
    &lt;span class="n"&gt;NotAllowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="cd"&gt;/// Sensor reported internal error&lt;/span&gt;
    &lt;span class="n"&gt;Internal&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 error is generic over the I2C error type so you get the platform's original error information propagated through. &lt;code&gt;NotAllowed&lt;/code&gt; catches invalid command sequences (for example, reading measurements before starting them), and &lt;code&gt;Internal&lt;/code&gt; surfaces faults reported by the sensor itself. Enable the &lt;code&gt;thiserror&lt;/code&gt; feature for &lt;code&gt;std::error::Error&lt;/code&gt; integration, or &lt;code&gt;defmt&lt;/code&gt; for embedded-friendly formatting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Measurements
&lt;/h2&gt;

&lt;p&gt;The SEN5x measurement flow is: start measurement, wait for data-ready, read the values. Here are the key commands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start Measurement&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x0021&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Begin continuous measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop Measurement&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x0104&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop measurement mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Ready&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x0202&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check if new data is available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read Measured Values&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x03C4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read all sensor values&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Putting it together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SensorData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;mass_concentration_pm1p0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;mass_concentration_pm2p5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;mass_concentration_pm4p0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;mass_concentration_pm10p0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;ambient_humidity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;ambient_temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;voc_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;nox_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Sen5x&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt;
    &lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;I2c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DelayNs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.write_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0x0021&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;I2c&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="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.delay&lt;/span&gt;&lt;span class="nf"&gt;.delay_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;data_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u16&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="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.read_words_no_alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0x0202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buf&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="nf"&gt;Ok&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;0x01&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;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.read_words_no_alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0x03C4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;words&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SensorData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;mass_concentration_pm1p0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;words&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;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mass_concentration_pm2p5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;words&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="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mass_concentration_pm4p0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;words&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="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mass_concentration_pm10p0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;words&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="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ambient_humidity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;words&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="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i16&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ambient_temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;words&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="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i16&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;200.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;voc_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;words&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="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;nox_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;10.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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note about the value scaling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mass_concentration_pm*&lt;/code&gt; values are unsigned 16-bit divided by 10 (unit: ug/m3)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ambient_temperature&lt;/code&gt; is &lt;strong&gt;signed&lt;/strong&gt; 16-bit divided by 200 (unit: degrees Celsius)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ambient_humidity&lt;/code&gt; is &lt;strong&gt;signed&lt;/strong&gt; 16-bit divided by 100 (unit: %RH)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;voc_index&lt;/code&gt; and &lt;code&gt;nox_index&lt;/code&gt; are unsigned 16-bit divided by 10 (dimensionless index)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signed/unsigned distinction is important. Casting a &lt;code&gt;u16&lt;/code&gt; to &lt;code&gt;i16&lt;/code&gt; before converting to &lt;code&gt;f32&lt;/code&gt; handles the two's complement correctly for temperature and humidity values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the Driver
&lt;/h2&gt;

&lt;p&gt;Here is what the application code looks like on an RP2040:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#![no_std]&lt;/span&gt;
&lt;span class="nd"&gt;#![no_main]&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;rp2040_hal&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;hal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;hal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;hal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;defmt_rtt&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;panic_probe&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[hal::entry]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;pac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;hal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;pac&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Peripherals&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;hal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Sio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="py"&gt;.SIO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;hal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;gpio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Pins&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="py"&gt;.IO_BANK0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="py"&gt;.PADS_BANK0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sio&lt;/span&gt;&lt;span class="py"&gt;.gpio_bank0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="py"&gt;.RESETS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;i2c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;I2C&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;i2c0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="py"&gt;.I2C0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pins&lt;/span&gt;&lt;span class="py"&gt;.gpio4&lt;/span&gt;&lt;span class="nf"&gt;.into_function&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;  &lt;span class="c1"&gt;// SDA&lt;/span&gt;
        &lt;span class="n"&gt;pins&lt;/span&gt;&lt;span class="py"&gt;.gpio5&lt;/span&gt;&lt;span class="nf"&gt;.into_function&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;  &lt;span class="c1"&gt;// SCL&lt;/span&gt;
        &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="nf"&gt;.kHz&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="py"&gt;.RESETS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;125&lt;/span&gt;&lt;span class="nf"&gt;.MHz&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="py"&gt;.TIMER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="py"&gt;.RESETS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;sensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Sen5x&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;sensor&lt;/span&gt;&lt;span class="nf"&gt;.start_measurement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sensor&lt;/span&gt;&lt;span class="nf"&gt;.data_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sensor&lt;/span&gt;&lt;span class="nf"&gt;.measurement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nn"&gt;defmt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"PM2.5: {} ug/m3, Temp: {} C, Humidity: {} %RH, VOC: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="py"&gt;.mass_concentration_pm2p5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="py"&gt;.ambient_temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="py"&gt;.ambient_humidity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="py"&gt;.voc_index&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// SEN5x updates roughly every 1 second&lt;/span&gt;
        &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="nf"&gt;.delay_ms&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, readable, and fully type-safe. No unsafe blocks, no C FFI, no raw pointer arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing with Mock I2C
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;embedded-hal-mock&lt;/code&gt; crate lets you write unit tests for your driver on your development machine. You define expected I2C transactions and the mock verifies them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[cfg(test)]&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&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="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;embedded_hal_mock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;eh1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Mock&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;I2cMock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;I2cTransaction&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;embedded_hal_mock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;eh1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NoopDelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_crc8&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;crc8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0xBE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0xEF&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;0x92&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;crc8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;0x81&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[test]&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;test_data_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;expectations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nn"&gt;I2cTransaction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SEN5X_ADDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0x02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0x02&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="nn"&gt;I2cTransaction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SEN5X_ADDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;crc8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;])]),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;i2c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;I2cMock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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;expectations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;NoopDelay&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;sensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Sen5x&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i2c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nd"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sensor&lt;/span&gt;&lt;span class="nf"&gt;.data_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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;This catches protocol bugs before you flash hardware. Highly recommended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Always validate CRC.&lt;/strong&gt; I2C buses in real hardware are noisy. I initially skipped CRC checks during development and got intermittent garbage readings that were hard to debug. CRC validation catches these immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Respect the timing.&lt;/strong&gt; The SEN5x datasheet specifies minimum delays between sending a command and reading the response. Skipping these delays causes NACK responses. The delays are not optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The &lt;code&gt;embedded-hal&lt;/code&gt; 1.0 migration was worth it.&lt;/strong&gt; If you are still on &lt;code&gt;embedded-hal&lt;/code&gt; 0.2, the &lt;code&gt;I2c&lt;/code&gt; trait in 1.0 is cleaner and combines read/write/write-read into a single trait. The migration is straightforward and the result is more ergonomic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Test with mock I2C.&lt;/strong&gt; You can write and validate your entire driver logic without touching hardware. This speeds up development significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Existing Crates?
&lt;/h2&gt;

&lt;p&gt;You might find existing SEN5x crates on crates.io. Before reaching for one, consider whether it targets &lt;code&gt;embedded-hal&lt;/code&gt; 1.0 (released stable in early 2024). Many embedded sensor crates still target 0.2, and mixing HAL versions in one project creates compatibility headaches. Building your own driver is also an excellent way to understand the sensor's protocol deeply, which helps when debugging real hardware issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The Sensirion SEN5x is a capable module that packs a lot of environmental sensing into a single I2C device. Rust's &lt;code&gt;embedded-hal&lt;/code&gt; ecosystem makes it straightforward to write a portable driver that works across microcontroller families.&lt;/p&gt;

&lt;p&gt;The combination of strong typing, zero-cost abstractions, and &lt;code&gt;no_std&lt;/code&gt; support makes Rust genuinely productive for embedded sensor work. No runtime overhead, no hidden allocations, and the compiler catches protocol mistakes at build time instead of at 3 AM when your sensor readings go wrong.&lt;/p&gt;

&lt;p&gt;If you are working on IoT or environmental monitoring projects in Rust, I would love to hear about your setup. Drop a comment or find me on &lt;a href="https://github.com/hauju" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me for more embedded Rust content and practical driver guides. You can also check out &lt;a href="https://oxidt.com" rel="noopener noreferrer"&gt;oxidt.com&lt;/a&gt; for more articles on Rust development.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>opensource</category>
      <category>iot</category>
    </item>
  </channel>
</rss>
