<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-10-02T16:22:41+00:00</updated><id>/feed.xml</id><title type="html">Kevin’s Blog</title><subtitle>This is my little blog about programming, electronics, and whatever I feel like writing about.</subtitle><author><name>Kevin Evans</name></author><entry><title type="html">What’s with the spam on Mastodon?</title><link href="/2025/09/17/spam-on-mastodon.html" rel="alternate" type="text/html" title="What’s with the spam on Mastodon?" /><published>2025-09-17T00:00:00+00:00</published><updated>2025-09-17T00:00:00+00:00</updated><id>/2025/09/17/spam-on-mastodon</id><content type="html" xml:base="/2025/09/17/spam-on-mastodon.html"><![CDATA[<p>I’ve been using Mastodon for a couple years now. It’s been a great experience so far: cool people, interesting perspectives, and ad-free! Right?</p>

<p>Lately, I’ve noticed a massive uptick in spam, generally stemming from the larger instances in the fediverse. Nobody seems to be talking too much about this though. This spam is usually commercial or corporate accounts promoting their product, service, website, blog post/SEO marketing bullshit. It’s rather annoying and seems to goes against what Mastodon was supposedly founded on. I tend to browse by hashtags, but this feature is practically unusable on some hashtags, primarily because it has been transformed into infinite feeds of straight-up ads.</p>

<p>On the joinmastodon.org website, they tout, front-and-center:</p>

<blockquote>
  <p>Your home feed should be filled with what matters to you most, not what a corporation thinks you should see.</p>
</blockquote>

<p>and</p>

<blockquote>
  <p>No […] ads to waste your time</p>
</blockquote>

<p>As of today, these claims are essentially false. It seems now, every marketing team <em>thinks</em> I should be reading their AI-generated garbage articles, giving clicks to their products or affiliate links, or otherwise buying their services. And I’m wasting my time sifting through these feeds just to find posts written by actual humans.</p>

<h4 id="wheres-the-spam-coming-from">Where’s the spam coming from?</h4>

<p>If you take a quick glance at the <a href="https://mastodon.social/public/local">live feed of mastodon.social</a>, it’s clearly littered with ads, moreso than my e-mail’s spam folder. Sure, they’re not ads generating revenue for the instances nor Mastodon itself—but they’re still advertisements by companies wanting in on those fedi users. With a cursory glance of about 25 posts on that live feed, I counted 10-12 obvious ads and 3-4 automated posts (probably also ads). These are companies that are clearly looking to promote their website or service, and even sometimes directly linking to products for sale. There doesn’t seem to be a way to curtail this, aside from muting or blocking each of these seemingly-infinite accounts.</p>

<p>Moderation here is not necessarily the solution here either. Although some instances (like Hachyderm) will prevent companies from creating accounts, other instances do allow this. And sometimes, it’s not so clear-cut whether, for example, an AI-generated SEO-keyword-crammed blog post is truly considered spam. The rules of some instances might strictly define spam as companies selling products, whereas others may not.</p>

<p>Nor is server selection nor defederation a solution. Yes, users can choose which server to use and users can pick an instance that may have stringent spam policies. Yes, your instance can defederate from a spammy instance. Yes, you can simply block the offending instance. But this will also block legitimate, non-corporate users. I still want to view the posts of actual people. I just don’t want Mastodon to devolve into my e-mail’s spam folder.</p>

<h3 id="a-possible-solution-marking-commercial-accounts">A possible solution: marking commercial accounts</h3>

<p>One possible solution is to mark commercial users as commercial users, similar to how Mastodon currently marks automated accounts. Corporate users would be required to self-identify their accounts as commercial and other users can decide whether or not to see their posts in their feeds. The ActivityPub spec already enables this via the <code class="language-plaintext highlighter-rouge">type</code> attribute in <a href="https://www.w3.org/TR/activitypub/#actor-objects"><code class="language-plaintext highlighter-rouge">Actor</code> objects</a>. Users can be marked as a <code class="language-plaintext highlighter-rouge">person</code>, <code class="language-plaintext highlighter-rouge">organization</code>, <code class="language-plaintext highlighter-rouge">group</code>, or a <code class="language-plaintext highlighter-rouge">service</code> (bot). But as of today, this <code class="language-plaintext highlighter-rouge">organization</code> type and filters have yet to be implemented in Mastodon.</p>

<p>In 2022, user ocdtrekkie already <a href="https://github.com/mastodon/mastodon/issues/20804">proposed</a> this change, although it doesn’t seem to have gone anywhere. I recently <a href="https://github.com/mastodon/mastodon/issues/36157">proposed adding filters for bot accounts</a> but I won’t hold my breath that this will go anywhere.</p>

<p>I’m hoping that sometime soon, Mastodon will have these features to reduce the amount of spam in feeds. But until then, I guess I’ll just have to live with blocking an endless supply of spam and/or blocking some of these larger, spammy instances.</p>

<h4 id="take-action">Take action</h4>

<p>If you’re also tired of the recent spam on Mastodon, I would encourage you to take action by 👍-ing or showing support for these Github issues:</p>

<ul>
  <li><a href="https://github.com/mastodon/mastodon/issues/20804">https://github.com/mastodon/mastodon/issues/20804</a></li>
  <li><a href="https://github.com/mastodon/mastodon/issues/36157">https://github.com/mastodon/mastodon/issues/36157</a></li>
</ul>

<p>And talk about it on Mastodon! These features are currently not yet on the <a href="https://joinmastodon.org/roadmap">Mastodon roadmap</a>, so attention must be given to these problems before the spam completely consumes the fediverse.</p>]]></content><author><name>Kevin Evans</name></author><category term="personal" /><category term="mastodon" /><summary type="html"><![CDATA[Mastodon should be ad-free, right? What gives?]]></summary></entry><entry><title type="html">Migrating to Codeberg Pages</title><link href="/2025/09/10/codeberg-migration.html" rel="alternate" type="text/html" title="Migrating to Codeberg Pages" /><published>2025-09-10T00:00:00+00:00</published><updated>2025-09-10T00:00:00+00:00</updated><id>/2025/09/10/codeberg-migration</id><content type="html" xml:base="/2025/09/10/codeberg-migration.html"><![CDATA[<p>Hello world, from <a href="https://codeberg.page">Codeberg Pages</a>!</p>

<p>I’ve gotten so tired of GitHub’s and Microsoft’s forced shenanigans—enough so that I’ve migrated over to Codeberg. It was a fairly painless experience switching over and would recommend it to anybody. There’s much less clutter, more privacy (no weird tracking &amp; ads), and its community seems to be much nicer overall.</p>

<p>Here’s how I’ve migrated over my Github Pages to Codeberg Pages, complete with an action to build the static content.</p>

<h2 id="project-structure">Project structure</h2>

<p>The project is organized using its branches:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">main</code> where the source code resides, and where I would do PRs to</li>
  <li><code class="language-plaintext highlighter-rouge">pages</code> the compiled pages generated by Jekyll, i.e. where <code class="language-plaintext highlighter-rouge">index.html</code> lives</li>
</ul>

<p>(Note: there are several ways of doing this. I’m using separate branches because it keeps the <code class="language-plaintext highlighter-rouge">main</code> branch clean of generated files.)</p>

<h2 id="actions">Actions</h2>

<p>On Github, there was a built-in action for deploying to Github pages. This made it very easy to deploy a static page.</p>

<p>On Codeberg, it required a little setup: namely a Forgejo Action. I based this Forgejo action on the <a href="https://forgejo.org/docs/latest/user/actions/quick-start/">quick start guide</a> and the <a href="https://gohugo.io/host-and-deploy/host-on-codeberg-pages/#automated-deployment-using-forgejo-actions">example Hugo action</a> which does something similar.</p>

<p>The Action works by running a series of steps when we push a commit onto <code class="language-plaintext highlighter-rouge">main</code>. The steps are:</p>

<ol>
  <li>On the build job: Cloning the repo from <code class="language-plaintext highlighter-rouge">main</code></li>
  <li>Installing dependencies (Ruby from <code class="language-plaintext highlighter-rouge">apt-get</code>, and the Jekyll dependencies)</li>
  <li>Builds the static files and places them into a temporary directory, <code class="language-plaintext highlighter-rouge">_site/</code>
    <ul>
      <li>This also copies over the <code class="language-plaintext highlighter-rouge">.domains</code> file</li>
    </ul>
  </li>
  <li>Uploads them as artifacts (I think these steps can be omitted), including hidden files like the <code class="language-plaintext highlighter-rouge">.domains</code> file</li>
  <li>On the deploy job: downloads the artifacts</li>
  <li>Copies the generated files onto the branch <code class="language-plaintext highlighter-rouge">pages</code></li>
</ol>

<p>The full workflow is listed <a href="https://codeberg.org/kevinhikaruevans/pages/src/branch/main/.forgejo/workflows/build-and-deploy.yml">in the repo</a>.</p>

<h2 id="dns">DNS</h2>

<p>I followed the instructions and use A/AAAA/TXT records to point the domain to the Codeberg page. For the TXT record, I had to use the <code class="language-plaintext highlighter-rouge">branch.repo.username</code> scheme (<code class="language-plaintext highlighter-rouge">pages.pages.kevinhikaruevans.codeberg.page</code> in this case).</p>

<p>I’m using Cloudflare for this and although the docs <a href="https://docs.codeberg.org/codeberg-pages/troubleshooting/#cloudflare-and-codeberg-pages">recommend</a> not using the Cloudflare caching, I enabled it anyway and it seems to be alright. There’s some slowness in Codeberg pages, probably caused by me living across the world from the servers, and using Cloudflare’s caching helps speed things up. I presume it would be fine to use Cloudflare’s caching here, since I’m only using a single domain.</p>]]></content><author><name>Kevin Evans</name></author><category term="personal" /><summary type="html"><![CDATA[We codeberg now]]></summary></entry><entry><title type="html">Ethics of Kagi</title><link href="/2025/08/21/kagi-ethics.html" rel="alternate" type="text/html" title="Ethics of Kagi" /><published>2025-08-21T00:00:00+00:00</published><updated>2025-08-21T00:00:00+00:00</updated><id>/2025/08/21/kagi-ethics</id><content type="html" xml:base="/2025/08/21/kagi-ethics.html"><![CDATA[<p>I recently trialed Kagi as a paid, replacement search engine for DDG and Google. It’s fairly good at reducing the amount of SEO-crammed results in the first page but I began to question whether or not to continue using it due to concerns in the community regarding their ethics and moral duties.</p>

<p>A part of <a href="https://web.archive.org/web/20250910033556/https://help.kagi.com/kagi/company/">Kagi’s mission</a> is to make the web more ethical. But I feel that some of their actions go against this mission and I ultimately cannot continue supporting Kagi in good conscience.</p>

<h2 id="support-of-russian-services">Support of Russian services</h2>

<p>A portion of your fee goes towards <a href="https://en.wikipedia.org/wiki/Yandex_Search">Yandex</a>, indirectly funding Russia’s invasion of Ukraine. Vlad, the CEO, has <a href="https://web.archive.org/web/20250915200405/https://kagifeedback.org/d/5445-reconsider-yandex-integration-due-to-the-geopolitical-status-quo/19">stated</a> that 2% of their total costs in 2024 goes towards Yandex. He has also stated that there is <strong>no</strong> intent of changing this.</p>

<p>Is it ethical to continue funding a large Russian-based company knowing full well it goes towards killing innocent lives?</p>

<p>At least other search engines like DuckDuckGo have <a href="https://web.archive.org/web/20220302093150/https://www.protocol.com/bulletins/duckduckgo-yandex-ukraine">halted</a> their partnerships with Yandex. Kagi’s refusal to do so and Vlad’s firm support of Yandex is quite telling. If others have ceased partnering with Yandex, why can’t Vlad?</p>

<p>It’s worth mentioning that Kagi <a href="https://web.archive.org/web/20231201205746/https://help.kagi.com/kagi/search-details/search-sources.html">previously listed the sources they used in their search</a>. It seems that they’ve now hidden that information on that page, seemingly contradictory to <a href="https://web.archive.org/web/20250915201759/https://blog.kagi.com/what-is-next-for-kagi">their “commitment to transparency.”</a></p>

<h2 id="inclusion-of-xais-grok">Inclusion of xAI’s Grok</h2>

<p>A portion of your fee goes towards Elon Musk’s xAI, of X/Twitter, to support Grok in their AI assistant. Indeed, you can simply not use this assistant, however all paying users are effectively subsidizing the cost of the Grok API calls. There is no way to opt-out of this.</p>

<p>Is it ethical to help finance a company which consistently promotes division among the population, the <a href="https://en.wikipedia.org/wiki/Criticism_of_Twitter#Handling_of_misinformation_and_disinformation">spread of disinformation</a>, and <a href="https://www.theverge.com/2022/12/2/23489808/elon-musk-twitter-hate-speech-increase-content-moderation">hate speech</a>? Is it ethical to give money to a company that continually platforms white supremacists?</p>

<p>Kagi currently <a href="https://web.archive.org/web/20250915200407/https://help.kagi.com/kagi/ai/assistant.html#which-model-to-choose">recommends</a> eight LLMs, including Grok. There are plenty of LLMs to choose from, yet Kagi continues to include Grok despite pushback from the community. Why is that?</p>

<p style="text-align: center">* * *</p>

<p>I don’t believe Kagi is upholding their mission of making the web more ethical. No matter the ethical framework here, the firm financial support of Russian services and X/Twitter is ultimately harming society as a whole and creating a more-fractured, divided world. It’s odd that Kagi dismisses these valid concerns, noting that they’re more focused on providing a better search, etc. But if they strive to be an ethical company, would that not take precedence over search result quality?</p>

<p>It goes without saying that none of us are perfect. No company is perfect. But, much like how Kagi can choose which heinous companies to partner with—you and I too can choose which companies to support. <em>The ethical choice is simply cancelling my Kagi membership.</em></p>

<p>As an aside, I do wonder when, not if, the larger, less obvious cracks will show in the company. When will Kagi change their mission and <a href="https://en.wikipedia.org/wiki/Don%27t_be_evil#History">remove their own “don’t be evil?”</a></p>

<hr />

<p> </p>

<p>PS: There are some other posts that I would recommend:</p>

<ul>
  <li><a href="https://d-shoot.net/kagi.html"><em>Why I Lost Faith in Kagi</em></a></li>
  <li><a href="https://ounapuu.ee/posts/2025/07/17/kagi/"><em>PSA: part of your Kagi subscription fee goes to a Russian company (Yandex)</em></a></li>
  <li><a href="https://web.archive.org/web/20250915200414/https://kagifeedback.org/d/2808-reconsider-your-partnership-with-brave"><em>Reconsider [Kagi’s] partnership with Brave</em></a></li>
</ul>]]></content><author><name>Kevin Evans</name></author><category term="thoughts" /><summary type="html"><![CDATA[Spoiler: Kagi doesn't seem very ethical]]></summary></entry><entry><title type="html">Zojirushi coffee maker audio mod</title><link href="/2025/08/01/coffee-maker-thing.html" rel="alternate" type="text/html" title="Zojirushi coffee maker audio mod" /><published>2025-08-01T00:00:00+00:00</published><updated>2025-08-01T00:00:00+00:00</updated><id>/2025/08/01/coffee-maker-thing</id><content type="html" xml:base="/2025/08/01/coffee-maker-thing.html"><![CDATA[<h1 id="background">Background</h1>

<p>I recently received a Zojirushi coffee maker as a gift, after doing pour-overs for years. While I love the ritual of doing a pour over, it can get fairly monotonous after a few years.</p>

<p>The internals of the Zojirushi coffee maker are very simple and I’m sure it’s the same design as most budget-friendly coffee makers. It consists of a single heater element for both the water and the hotplate, controlled by a single normally-on thermostat. During the brew cycle, the water cools the thermostat and boils up, then re-condenses over the coffee grounds. Once the heater runs dry, the specific heat decreases and the thermostat quickly warms up, tripping the thermostat. Once relatively cool, the thermostat trips on and resumes its hot plate heating cycle, indefinitely keeping the coffee warm. It’s a simple system that works great.</p>

<p>I wanted to create an alarm that can let me know if the coffee maker was left on for too long. Also given that this is a Zojirushi product, I wanted it to play a little jingle when turned on and when complete. I decided to modify the coffee maker by including a simple board to:</p>

<ul>
  <li>play a song during start-up</li>
  <li>detect the current of the heater and detect the heater’s falling edge
    <ul>
      <li>when the water heater is tripped off, it should play a song indicating the coffee’s done</li>
    </ul>
  </li>
  <li>when left on for too long, I want it to sound an alarm</li>
</ul>

<p>I also wanted to do this completely non-destructively and keep the coffee maker relatively stock and revertable to its original form.</p>

<video width="360" height="640" controls="">
  <source src="/assets/videos/coffee-maker-thing/startup.webm" type="video/webm" />
</video>

<video width="360" height="640" controls="">
  <source src="/assets/videos/coffee-maker-thing/done.webm" type="video/webm" />
</video>

<h1 id="prototyping">Prototyping</h1>
<p>Since I want this to be revertable, using a non-contact current sense would be the best choice. I have a couple current transformers (CTs) that I’ve salvaged and they will work for measuring whether the thermostat is switched on or off, since it’s non-invasive and relatively safe.</p>

<p>I initially just stuck the CT inline with a 10k resistor and probed it, just to see what the wave looked like (it was as-expected for a resistive heater, but skewed a little from stray effects).</p>

<p><img src="/assets/images/coffee-maker-thing/ct470.png" alt="ct + scope with 470 ohm resistor" /></p>

<h1 id="design">Design</h1>

<p>The system can be divided into a few parts:</p>

<ul>
  <li>power supply (120 VAC to 5VDC and 3V3)</li>
  <li>current sense (the CT) to DC-ish</li>
  <li>microcontroller with piezo buzzer</li>
</ul>

<h3 id="board-outline">Board outline</h3>

<p>I borrowed a flatbed scanner to get the board outline (surprisingly, it was 1:1), then used a set of dividers and a ruler to get the precise measurements. Then I imported the image into Inkscape and created an SVG of the outline using bezier curves, which I could then bring into KiCAD. Then, I printed out a 1:1 copy, cut it out, and confirmed it fit in there.</p>

<p><img src="/assets/images/coffee-maker-thing/outline.png" alt="board outline" /></p>

<p>This was by far the easiest solution of getting the outline. I initially tried to sketch it by transferring measurements between the coffee maker into CAD, but that was painstakingly slow and I couldn’t replicate the curves easily enough.</p>

<h3 id="power-supply">Power supply</h3>

<p>I’m just using a 120 VAC to 5VDC module from Meanwell with more than enough current to supply the microcontroller (max of like 100 mA). For protection, I added a fuse and MOV on the 120 VAC side, then added a thermal fuse on the 5V side. Then used a 5V to 3V3 LDO to supply the microcontroller.</p>

<p><img src="/assets/images/coffee-maker-thing/ps.png" alt="power supply schematic" /></p>

<h3 id="current-sense">Current sense</h3>
<p>The current sense is a current transformer, going across an AC coupling cap. This is fed into a full bridge rectifier, then dropped across a small resistor, then fed into the ADC with a diode in reverse for overvoltage protection. I originally had an RC filter here to read the DC voltage (i.e. rectifying to dc), but the ADC is fast enough to sample at 60/120Hz—so instead, I’m reading the half waves.</p>

<p><img src="/assets/images/coffee-maker-thing/sense.png" alt="current transformer ckt" /></p>

<p>I simulated the CT using an AC current source with a series resistor in SPICE, and while this worked, it wasn’t simulated very well. There were several major differences in simulation vs reality, probably attributed to the nonlinearlity of the CT and diodes. Although it wasn’t super accurate, it helped me get ballpark estimates of components without having to do some of the math.</p>

<h4 id="but-wtf">…but wtf?</h4>

<p>After rectifying, the frequency should’ve been 120 Hz, since the negative side should’ve been flipped upward. But for whatever reason (bad solder joint? did I solder the wrong component?), it’s still reading 60 Hz.</p>

<p><img src="/assets/images/coffee-maker-thing/rect.png" alt="probe of the ct" /></p>

<h3 id="mcu-and-buzzer">MCU and buzzer</h3>

<p>The MCU is laid out just like the application example from the TRM.</p>

<p><img src="/assets/images/coffee-maker-thing/mcu.png" alt="mcu schematic with audio ckt" /></p>

<p>The piezoelectric buzzer is fed by the 5V power divided by a variable resistor, controlled by a BJT on a PWM pin. While this works, I found it still to be too quiet. This was also simulated in SPICE at several audible frequencies.</p>

<h1 id="software">Software</h1>
<p>There are two main components of the software:</p>

<ul>
  <li>the firmware that runs on the esp32s2</li>
  <li>the user application, a webapp exposed by the esp32’s softAP</li>
</ul>

<h2 id="firmware">Firmware</h2>

<p>The firmware is pretty simple and consists of mostly timers:</p>

<ul>
  <li>on startup, the startup song is enqueued and played</li>
  <li>on the falling edge of the current sense (i.e. when the heater is first turned off), it plays the `done’ song and begins the alarm timer
    <ul>
      <li>there’s some debounce happening w/ timers</li>
      <li>this can only get trig’d after 5 seconds after boot</li>
    </ul>
  </li>
  <li>the alarm timer plays after 15 minutes of being started
    <ul>
      <li>this timer auto-reloads, so it plays every 15 minutes after the coffee is finished brewing</li>
    </ul>
  </li>
</ul>

<h3 id="protobuffers">Protobuffers</h3>

<p>I finally had a fun use for protobuffers! I’m using them to hold the track data:</p>

<div class="language-proto highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">syntax</span> <span class="o">=</span> <span class="s">"proto3"</span><span class="p">;</span>

<span class="kd">message</span> <span class="nc">AudioEvent</span> <span class="p">{</span>
  <span class="c1">// the frequency (Hz) or rest if 0</span>
  <span class="kt">int32</span> <span class="na">frequency</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

  <span class="c1">// the duration of the note/rest in ms</span>
  <span class="kt">int32</span> <span class="na">duration</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">AudioSequence</span> <span class="p">{</span>
  <span class="c1">// track name</span>
  <span class="kt">string</span> <span class="na">name</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

  <span class="k">repeated</span> <span class="n">AudioEvent</span> <span class="na">events</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It was honestly easier than expected to use protobuffers. I used protoc to generate both the C and TS code and it worked well for this purpose. It lets me easily transfer the tracks between the frontend and the fw code, as well as giving a convenient format for storing the tracks in NVS.</p>

<h3 id="miscesp-idf-bugs">Misc./ESP-IDF bugs</h3>

<p>I noticed there were several bugs in ESP-IDF, although these might just be HW limitations or problems on my part:</p>

<ul>
  <li>the digi adc calibration is off by a factor of 1/2
    <ul>
      <li>when in continuous mode, it’s using the digi adc (unlike oneshot which uses a different realm)</li>
      <li>the digi adc is limited to 12 bits, but the one shot uses 13 bits - but the calibration routine only seems to use 13?</li>
      <li><a href="https://github.com/espressif/esp-idf/issues/15876">bug report</a></li>
    </ul>
  </li>
  <li>the adc’s threshold interrupts do not work on the esp32s2
    <ul>
      <li>supposedly, you can set a min/max threshold on the adc to interrupt the cpu</li>
    </ul>
  </li>
  <li>when doing an ota update where the partitions are not properly defined, calling <code class="language-plaintext highlighter-rouge">esp_ota_write</code> on a nonnull handle provided by <code class="language-plaintext highlighter-rouge">esp_ota_begin</code> will hard crash and trigger the wdt
    <ul>
      <li>I don’t feel like diving into this since I don’t intend to use the ota functionality much</li>
      <li>I think this has to do with aligning the ota0 partition but I don’t feel like diving into this one</li>
      <li>I regret getting the 4MB version, since I like to use 2MB factory/ota sizes. Should’ve gotten the 8MB version <code class="language-plaintext highlighter-rouge">;_;</code></li>
    </ul>
  </li>
  <li>not a bug, but there’s no simd instructions on the s2 series, unfortunately. I was hoping to read the continuous adc via dma, then use the vector instructions to find the half-wave rms voltage. But alas, I had to just use <code class="language-plaintext highlighter-rouge">for</code>-loops</li>
</ul>

<h1 id="putting-it-all-together">Putting it all together</h1>

<p>Here’s what the board looks like in the enclosure:</p>

<p><img src="/assets/images/coffee-maker-thing/bottom.webp" alt="bottom of the coffee maker with a custom board in it" /></p>

<p>For the power pins, I’m using the same-sized spades as the original. The wire-to-board connectors I’m using have two spades on them, so none of the heater load is going across a trace.</p>

<h3 id="other-modifications">Other modifications</h3>

<p>I’m tempted to put in a ground but I don’t want to have to change out the cable or strain relief yet. Both the hotplate and enclosure cover are aluminum, and I can see a potential hazard if the spades disconnect, and either the live/neutral can short to these. I also would like to put a conformal coating on the board, but I might save that for the next revision (if ever).</p>]]></content><author><name>Kevin Evans</name></author><category term="programming" /><category term="electronics" /><summary type="html"><![CDATA[Adding audio to my Zojirushi coffee maker]]></summary></entry><entry><title type="html">My first Python package: pydantic-sqlalchemy-deco</title><link href="/2024/12/18/first-python-package.html" rel="alternate" type="text/html" title="My first Python package: pydantic-sqlalchemy-deco" /><published>2024-12-18T00:00:00+00:00</published><updated>2024-12-18T00:00:00+00:00</updated><id>/2024/12/18/first-python-package</id><content type="html" xml:base="/2024/12/18/first-python-package.html"><![CDATA[<p>Today, I released my first Python package <a href="https://pypi.org/project/pydantic-sqlalchemy-deco/">published on pypi</a>!</p>

<p>It doesn’t do much, but it’s something that has been bugging me for a while. I often include Pydantic models in SQLAlchemy-created databases. Normally, I just use <code class="language-plaintext highlighter-rouge">JSONB</code> and use the regular ol’ <code class="language-plaintext highlighter-rouge">model_validate</code> functions. While this works, I’d prefer it to be automatic. Here’s where <code class="language-plaintext highlighter-rouge">pydantic-sqlalchemy-deco</code> comes in.</p>

<p>It’s a simple <code class="language-plaintext highlighter-rouge">TypeDecorator</code> class that automatically converts between Pydantic and SQLAlchemy JSONB types.</p>

<h2 id="installation">Installation</h2>

<p>Since it’s pip installable, you can use <code class="language-plaintext highlighter-rouge">pip install pydantic_sqlalchemy_deco</code>.</p>

<h2 id="usage">Usage</h2>

<p>All you need to do is set the <code class="language-plaintext highlighter-rouge">Mapped[]</code> type and include <code class="language-plaintext highlighter-rouge">PydanticJSON</code> in the <code class="language-plaintext highlighter-rouge">mapped_column()</code>, like such:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pydantic_sqlalchemy_deco.decorator</span> <span class="kn">import</span> <span class="n">PydanticJSON</span>

<span class="p">...</span>

<span class="c1"># Define Pydantic model somewhere:
</span><span class="k">class</span> <span class="nc">MyCustomModel</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">custom_id</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span>


<span class="c1"># Define your SQLAlchemy model:
</span><span class="k">class</span> <span class="nc">MyEntry</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>

    <span class="n">custom_data</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="n">MyCustomModel</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">PydanticJSON</span><span class="p">(</span><span class="n">MyCustomModel</span><span class="p">))</span>

</code></pre></div></div>

<p>Then it’s easy as doing</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entry</span> <span class="o">=</span> <span class="n">MyEntry</span><span class="p">(</span>
    <span class="n">custom_data</span><span class="o">=</span><span class="n">MyCustomModel</span><span class="p">(</span><span class="n">custom_id</span><span class="o">=</span><span class="mi">1234</span><span class="p">)</span>
<span class="p">)</span>

<span class="p">...</span>

<span class="k">print</span><span class="p">(</span><span class="n">entry</span><span class="p">.</span><span class="n">custom_data</span><span class="p">.</span><span class="n">custom_id</span><span class="p">)</span>
</code></pre></div></div>

<p>Pretty simple, eh?</p>

<h2 id="further-remarks">Further remarks</h2>

<p>I never realized how easy it is to build Python packages. It’s basically:</p>

<ol>
  <li>write your code</li>
  <li>define your pyproject.toml file</li>
  <li>then setup the default Github action to push releases to pypi.</li>
</ol>

<p>That’s it! I was expecting a whole mess of generating and keeping track of tokens, writing and rewriting Github actions, and some CLI work to get it published.</p>

<p>I’ll eventually need to do tests, but I can hold off on those til later.</p>

<h2 id="links">Links</h2>

<ul>
  <li><a href="https://pypi.org/project/pydantic-sqlalchemy-deco/">pydantic_sqlalchemy_deco on pypi</a></li>
  <li><a href="https://github.com/kevinhikaruevans/pydantic-sqlalchemy-type-decorator">pydantic-sqlalchemy-type-decorator on Github</a></li>
</ul>]]></content><author><name>Kevin Evans</name></author><category term="programming" /><summary type="html"><![CDATA[My foray into Python]]></summary></entry><entry><title type="html">Dell D6000 dumb dock thing</title><link href="/2024/10/11/d6000-adapter.html" rel="alternate" type="text/html" title="Dell D6000 dumb dock thing" /><published>2024-10-11T00:00:00+00:00</published><updated>2024-10-11T00:00:00+00:00</updated><id>/2024/10/11/d6000-adapter</id><content type="html" xml:base="/2024/10/11/d6000-adapter.html"><![CDATA[<p>A few years back, I found a cheap USB-C dock on Craigslist. It had a partly broken USB-C plug but I decided to grab it because it was cheap.</p>

<p>When I got home, I cracked it open and noticed it seems to use a regular USB C-to-C cable to connect the laptop to the dock. When I tried using several cables, I noticed that none of them worked! It seems that the cable did not abide by the USB spec. I used some USB-C breakout boards to trace out the schematic. It seems that Dell (or Bizlink or whatever OE they use) chose to use a non-compliant, custom and propietary USB cable. What kind of monster does that? Is it for greater power delivery? Planned obsolescence? I cannot find a reasonable explanation for this.</p>

<h2 id="finding-the-pinout">Finding the pinout</h2>

<p>As stated, I just used some breakout boards to find the pinout: literally probing each pin on the breakout board and seeing where it goes. It turns out the entire cable is mangled as fuck and somebody, somewhere, chose to fuck the thing up. The RX pins go to RX again, instead of TX, the CC pins are weird, and the data pins are reversed. I’m assuming the dock’s PCB “corrects” the pinout, so that a proprietary pinout can be used.</p>

<p><img src="/assets/images/2024-10-11-pinout.jpg" alt="pinout of the dock's usb cable" /></p>

<h3 id="bringing-it-into-kicad">Bringing it into KiCAD</h3>

<p>I brought the design into KiCAD, for a much cleaner diagram. This schematic is slightly different than what’s above, since a USB cable is meant to be plugged into the receptacle on one side (so the RX/TX pins are somewhat “corrected”).</p>

<p><img src="/assets/images/2024-10-11-schematic.png" alt="schematic of the cable" /></p>

<p>Note that this adapter is unidirectional, at least on the dock side. This is discussed <a href="https://github.com/kevinhikaruevans/dell-d6000-usb-c-adapter/issues/3#issuecomment-2298873006">here</a>.</p>

<h3 id="testing">Testing</h3>

<p>Initially, I ignored the impedance requirement for the adapter and only matched differential lengths. I think it ended up being around 50 ohms (ish), but when I tested the maximum speed, I couldn’t achieve anywhere near GbE speeds despite a GbE connection. I’m guessing this has to do with the impedance matching.</p>

<p>I created a separate PCB design, hoping to get this printed on a long flex PCB strip. But unfortunately, when I estimated the impedance using openEMS, I was nowhere in the realm of 90 ohms. I might just be doing the simulation wrong, but I’m wondering if this is even remotely possible on a non-impedance controlled (read: cheap) flex PCB. Bleh.</p>

<h2 id="next-steps">Next steps</h2>

<p>I built an adapter that works but it’s pricey af and time-consuming to iterate on the design. I’m hoping that someone with more time and motivation can finish up the project.</p>

<p>The next steps are:</p>

<ul>
  <li>lay this out on a long flex pcb strip</li>
  <li>create a 3d printed part for the receptacle</li>
  <li>create an assembly containing the part above and the pcb</li>
  <li>get a handful manufactured</li>
  <li>????</li>
</ul>

<h2 id="links">Links</h2>

<ul>
  <li><a href="https://github.com/kevinhikaruevans/dell-d6000-usb-c-adapter">Github project</a></li>
  <li><a href="https://github.com/kevinhikaruevans/dell-d6000-usb-c-adapter/issues/3">Relevant issue on the Github project</a></li>
</ul>]]></content><author><name>Kevin Evans</name></author><category term="electronics" /><summary type="html"><![CDATA[wtf is this thing]]></summary></entry><entry><title type="html">Using Tek 571s at WSU</title><link href="/2024/07/22/my-wsu-experience.html" rel="alternate" type="text/html" title="Using Tek 571s at WSU" /><published>2024-07-22T00:00:00+00:00</published><updated>2024-07-22T00:00:00+00:00</updated><id>/2024/07/22/my-wsu-experience</id><content type="html" xml:base="/2024/07/22/my-wsu-experience.html"><![CDATA[<p>Here’s a story (read: my cringey rant) from my EECS undergrad experience at WSU. I didn’t fully enjoy my schooling here for a variety of reasons, but this is just one of the reasons why.</p>

<p>While getting my degree, I had several electrical engineering courses with labs. During one of the early labs, we were using curve tracers to characterize diodes. Many of y’all know the drill: plug in a diode or transitor into a curve tracer, it sweeps the voltage and displays the output. Our curve tracers are… <em>antiquated</em>. We’re using some dusty Tek 571s, which were due for calibration about thirty years ago. I am surprised that our lab equipment is so outdated, but this might be <a href="https://opengovwa.com/employee">related to the fact that our university’s presidents and coaches are some of the state’s top paid employees</a>.</p>

<p><img src="/assets/images/2024-07-22-tek.png" alt="CRT of a Tek 571" /></p>

<p>Anyway, a requirement of this specific lab was to take pictures of the curve tracer CRT display and embed them in our report. I think we were to put tangents over the curves (in post) and grab some fitment params, or something along those lines. This isn’t the most accurate method, considering the CRTs are convex and have a rolling shutter effect, so it’s actually quite hard to get a good image of the curves. But hey, it’s for an undergrad class, so it truly doesn’t matter.</p>

<p>Since I’m putting in the effort into <a href="https://github.com/kevinhikaruevans/old-homeworks/blob/main/EE362/lab5/lab5_report.pdf">typesetting my reports in TeX</a>, I wondered if there was a better way of grabbing the image. I noticed that there’s a print feature on the curve tracers with parallel ports in the rear. I drafted a quick email to the lab director asking if they would mind if I grabbed the print outputs using a microcontroller:</p>

<blockquote>
  <p>In the EE352 lab the other day, we were using the Tek 571 curve tracers and were taking snapshots of the CRTs on our phones. There were a couple of students that weren’t able to take photos because of the screen flicker and mismatched refresh rates between the camera/CRTs. [My instructor’s name] mentioned there were dot matrix printers setup previously, but [the department] had switched away from those because of their incredibly slow printing speed.</p>
</blockquote>

<blockquote>
  <p>I was wondering, since there are parallel ports available, would it be possible to setup a microcontroller to act as an printer client (basically bitbanging the IBM or Epson FX80 protocol) and dump the printer data as a file on a USB drive? I’m thinking it would only need a couple 5v to a 3.3v logic level converters (and maybe a parallel to serial register) and a microcontroller. Is there someone I could talk to about getting the permission to try implementing this?</p>
</blockquote>

<p>It seems the lab director thought it wasn’t a good idea, as he responded with telling me:</p>

<blockquote>
  <p>I am not in favor of adding additional custom-made hardware to the lab at this time.</p>
</blockquote>

<p>Then he CC’d other folks in the department, some of whom I’d never met prior. I can partly understand why he said this, given that our budget is limited enough to still use 30-plus-year-old equipment—there’s only a few of these in the department’s arsenal. But to me, this narrow-minded attitude stifles creativity and innovation in students. Students should accept the equipment we’re given without questioning if we could do this better. <em>Now, that’s the engineering mindset!</em></p>

<p>This could’ve been a cool learning and mentorship experience. Plus, it could’ve helped other engineering students who had trouble capturing the displays. But instead, we’re continuing to take these pictures of CRTs and doing fits with rulers. Instead, <em>we’re not engineering anything</em> but strictly following the course guidelines. Can’t go beyond those guidelines, eh. Well, hey, I thought it would’ve been a cool project.</p>]]></content><author><name>Kevin Evans</name></author><category term="computers" /><category term="wsu" /><category term="college" /><summary type="html"><![CDATA[Or how I lost some confidence in the EECS department at WSU]]></summary></entry><entry><title type="html">Concept2 Logbook Data Safety</title><link href="/2024/01/22/concept2-logbook-safety.html" rel="alternate" type="text/html" title="Concept2 Logbook Data Safety" /><published>2024-01-22T00:00:00+00:00</published><updated>2024-01-22T00:00:00+00:00</updated><id>/2024/01/22/concept2-logbook-safety</id><content type="html" xml:base="/2024/01/22/concept2-logbook-safety.html"><![CDATA[<p>Some time last month, I was wanting to sync my Concept2 erg logbook with my Google Fit data. Concept2’s API is actually quite pleasant to work with and had the data that I was wanting to retrieve. However, I noticed that their endpoint could be scraped to retrieve very sensitive info on others. This seems like a major whoopsy. Concept2 responded quickly and addressed the problems, but I question whether or not my data is safe in Concept2’s servers.</p>

<h3 id="their-api">Their API</h3>

<p>To retrieve your user data, it’s a simple <code class="language-plaintext highlighter-rouge">GET</code> call,</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://log.concept2.com/api/users/<span class="nv">$USER_ID</span> <span class="nt">--header</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$API_KEY</span><span class="s2">"</span> | jq
</code></pre></div></div>

<p>This retrieves a lovely JSON-formatted response,</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">123456789</span><span class="p">,</span><span class="w">
    </span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"myusername"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"first_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Kevin"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"last_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Evans"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"gender"</span><span class="p">:</span><span class="w"> </span><span class="s2">"M"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dob"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1800-01-01"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-email@some-domain.com"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"country"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USA"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"profile_image"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"age_restricted"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"email_permission"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"max_heart_rate"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"weight"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"roles"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"logbook_privacy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"partners"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>I was curious to see what would result if I weren’t authorized, so I ran</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://log.concept2.com/api/users/<span class="k">$((</span>USER_ID+1<span class="k">))</span> <span class="nt">--header</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$API_KEY</span><span class="s2">"</span> | jq
</code></pre></div></div>

<p>To my surprise, this gave me the same data fields for other users! I could retrieve the username, first and last names, gender, DOB, email, weight, max HR, and profile images of ANY user on Concept2, including children!</p>

<p><img src="/assets/images/2024-01-24-term-screenshot.png" alt="screenshot showing profile data with sensitive information redacted" /></p>

<h3 id="report-and-response">Report and response</h3>

<p>After seeing this, I promptly emailed the Concept2 Logbook team, noting that this is a pretty serious issue. They responsed quickly with,</p>

<blockquote>
  <p>Hi Keith,</p>
</blockquote>

<blockquote>
  <p>Thanks for bringing this to our attention. It should not be possible to return this level of information. It looks like a very recent change affected this by putting a bug into the permission logic. The API is set to return a small level of information about the user, no different from visiting their profile page. The bug has been fixed now and we will reconsider whether the information we return in still appropriate. I’ve quickly checked out logs, and it does not appear that anyone has taken advanatge of this bug, but I will check them in more detail.</p>
</blockquote>

<blockquote>
  <p>Thanks again for letting us know.</p>
</blockquote>

<blockquote>
  <p>Best,</p>
</blockquote>

<p>Well, first off, my name is Kevin and not Keith.</p>

<p>But I’m really wondering how the hell was this missed and how long was this vulnerability in place for? Were there no unit tests before deployments? I wonder how thoroughly they checked the logs to see if anybody has exploited this.</p>

<p>Can I trust Concept2 to store my data in the future? (No.)</p>]]></content><author><name>Kevin Evans</name></author><category term="rowing" /><category term="fun" /><category term="computers" /><category term="security" /><summary type="html"><![CDATA[Major whoopsie in Concept2's logbook]]></summary></entry><entry><title type="html">10-bit serial on the ESP32</title><link href="/2024/01/21/esp-ten-bit-serial.html" rel="alternate" type="text/html" title="10-bit serial on the ESP32" /><published>2024-01-21T00:00:00+00:00</published><updated>2024-01-21T00:00:00+00:00</updated><id>/2024/01/21/esp-ten-bit-serial</id><content type="html" xml:base="/2024/01/21/esp-ten-bit-serial.html"><![CDATA[<p>Recently, I was working on integrating with a device that appears to use a bidirectional 10-bit serial interface at exactly 100 kHz. The manufacturer does not give <em>any</em> information on their proprietary format, so I guess I’ll have to hack something together.</p>

<p>To begin, the master initiates the sequence by sending several 10-bit frames and slaves respond by driving the line low and responding with their own packets. On my logic analyzer, it looks something like this:</p>

<p><img src="/assets/images/2024-01-25-bits.png" alt="10 bit serial screenshot" /></p>

<p>Unfortunately, I don’t own anything that natively supports 10-bit serial. All of my chips only support up to 8-bit and one even supports 9-bit (addressed) serial.</p>

<p>Ideally, I would like to use an ESP32-S3, mostly because I’ve been working with ESP-IDF lately and find it good enough for a project like this.</p>

<h3 id="attempt-1-bitbanging-serial">Attempt 1: Bitbanging serial</h3>

<p>I attempted to just bitbang serial using several timers on the ESP32. I initially tried the high-resolution timer, but it is unable to reach a 10 us resolution and silently fails (d’oh). This is documented on their <a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html">docs</a>, but I must’ve missed it.</p>

<p>Then I tried the general purpose (GP) timer and while it’s able to reach 10 us periods, it sometime skewed more than a microsecond or two! This is not acceptable for this application since there is no external clock.</p>

<h3 id="attempt-2--rmt-peripheral-">Attempt 2: ✨ RMT peripheral ✨</h3>

<p>After consulting the ESP32 forum, I tried the <a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/rmt.html">remote control transceiver (RMT) peripheral</a>. To my surprise, this worked wonderfully. It is made for controlling remote control signals, but has numerous applications outside of that realm.</p>

<p>It’s a pain to set this up, as it requires you to manually encode and decode pulses at a fixed period. But, this peripheral is fairly precise and accurate and can hit 10 us right on the money.</p>

<p>The RMT also allows you to transmit bidirectionally on the same line, using <code class="language-plaintext highlighter-rouge">is_loop_back=true</code>.</p>

<p>Here’s my tester code that I used. It’s ugly and definitely not ready for production, but the bones are all there. It’s roughly based on the <a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/rmt.html#customize-rmt-encoder-for-nec-protocol">RMT NEC example</a> on the Espressif website.</p>

<p>It initializes the RMT with:</p>

<ul>
  <li>a transmitter</li>
  <li>an encoder to convert the lower 10-bits of ushorts into pulse structures for the underlying RMT</li>
  <li>a loopback receiver</li>
  <li>an ugly decoder that performs some rounding (e.g. rounding 8 us to 10 us)</li>
</ul>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"freertos/FreeRTOS.h"</span><span class="cp">
#include</span> <span class="cpf">"freertos/task.h"</span><span class="c1"> </span><span class="cp">
#include</span> <span class="cpf">"freertos/queue.h"</span><span class="cp">
#include</span> <span class="cpf">"driver/rmt_tx.h"</span><span class="cp">
#include</span> <span class="cpf">"driver/rmt_rx.h"</span><span class="cp">
#include</span> <span class="cpf">"driver/rmt_encoder.h"</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">"esp_check.h"</span><span class="cp">
#include</span> <span class="cpf">"esp_log.h"</span><span class="cp">
#include</span> <span class="cpf">"driver/gpio.h"</span><span class="cp">
#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
</span>
<span class="cp">#define SERIAL_FREQ_HZ 1000000
#define SERIAL_TX_GPIO_NUM 6
#define SERIAL_RX_GPIO_NUM 7
</span>
<span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">TAG</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"main"</span><span class="p">;</span>

<span class="k">static</span> <span class="n">rmt_channel_handle_t</span> <span class="n">tx_channel_handle</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="k">static</span> <span class="n">rmt_channel_handle_t</span> <span class="n">rx_channel_handle</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="k">static</span> <span class="k">const</span> <span class="n">rmt_transmit_config_t</span> <span class="n">tx_config</span> <span class="o">=</span> <span class="p">{</span>
  <span class="p">.</span><span class="n">loop_count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
  <span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">eot_level</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="c1">// stop bit: note inverted</span>
<span class="p">};</span>
<span class="k">static</span> <span class="n">rmt_encoder_handle_t</span> <span class="n">encoder</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>

<span class="k">const</span> <span class="k">static</span> <span class="n">rmt_symbol_word_t</span> <span class="n">bits</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
  <span class="c1">// inverted!</span>

  <span class="cm">/* 0 */</span> <span class="p">{</span>
    <span class="p">.</span><span class="n">level0</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="p">.</span><span class="n">duration0</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
    <span class="p">.</span><span class="n">level1</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="p">.</span><span class="n">duration1</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="cm">/* 1 */</span> <span class="p">{</span>
    <span class="p">.</span><span class="n">level0</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="p">.</span><span class="n">duration0</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
    <span class="p">.</span><span class="n">level1</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="p">.</span><span class="n">duration1</span> <span class="o">=</span> <span class="mi">5</span>
  <span class="p">},</span>
<span class="p">};</span>


<span class="c1">// #define RMT_ENCODING_RESET 0</span>

<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
  <span class="n">rmt_encoder_t</span> <span class="n">base</span><span class="p">;</span>
  <span class="n">rmt_encoder_t</span> <span class="o">*</span><span class="n">copy_encoder</span><span class="p">;</span>
  <span class="n">rmt_symbol_word_t</span> <span class="n">start_bit</span><span class="p">;</span>
  <span class="kt">int</span> <span class="n">state</span><span class="p">;</span>
  <span class="n">rmt_symbol_word_t</span> <span class="n">end_bit</span><span class="p">;</span>
<span class="p">}</span> <span class="n">rmt_curtis_encoder_t</span><span class="p">;</span>


<span class="k">static</span> <span class="kt">size_t</span> <span class="nf">rmt_encode_curtis</span><span class="p">(</span><span class="n">rmt_encoder_t</span> <span class="o">*</span><span class="n">encoder</span><span class="p">,</span> <span class="n">rmt_channel_handle_t</span> <span class="n">channel</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">primary_data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">data_size</span><span class="p">,</span> <span class="n">rmt_encode_state_t</span> <span class="o">*</span><span class="n">out_state</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">rmt_curtis_encoder_t</span> <span class="o">*</span><span class="n">curtis_encoder</span> <span class="o">=</span> <span class="n">__containerof</span><span class="p">(</span><span class="n">encoder</span><span class="p">,</span> <span class="n">rmt_curtis_encoder_t</span><span class="p">,</span> <span class="n">base</span><span class="p">);</span>
  <span class="n">rmt_encode_state_t</span> <span class="n">session_state</span> <span class="o">=</span> <span class="n">RMT_ENCODING_RESET</span><span class="p">;</span>
  <span class="n">rmt_encode_state_t</span> <span class="n">state</span> <span class="o">=</span> <span class="n">RMT_ENCODING_RESET</span><span class="p">;</span>
  <span class="kt">size_t</span> <span class="n">encoded_symbols</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

  <span class="kt">uint16_t</span> <span class="n">raw</span> <span class="o">=</span> <span class="o">*</span><span class="p">((</span><span class="kt">uint16_t</span> <span class="o">*</span><span class="p">)</span><span class="n">primary_data</span><span class="p">);</span>
  <span class="n">rmt_encoder_t</span> <span class="o">*</span><span class="n">copy_encoder</span> <span class="o">=</span> <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">copy_encoder</span><span class="p">;</span>

  <span class="k">switch</span><span class="p">(</span><span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">state</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="mi">0</span><span class="p">:</span> <span class="c1">// send start bit</span>
      <span class="n">encoded_symbols</span> <span class="o">+=</span> <span class="n">copy_encoder</span><span class="o">-&gt;</span><span class="n">encode</span><span class="p">(</span><span class="n">copy_encoder</span><span class="p">,</span> <span class="n">channel</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">start_bit</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">rmt_symbol_word_t</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">session_state</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">session_state</span> <span class="o">&amp;</span> <span class="n">RMT_ENCODING_COMPLETE</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">state</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="c1">// go to next state</span>
      <span class="p">}</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">session_state</span> <span class="o">&amp;</span> <span class="n">RMT_ENCODING_MEM_FULL</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">state</span> <span class="o">|=</span> <span class="n">RMT_ENCODING_MEM_FULL</span><span class="p">;</span>
        <span class="k">goto</span> <span class="n">out</span><span class="p">;</span>
      <span class="p">}</span>
    
    <span class="c1">// fall-through</span>
    <span class="k">case</span> <span class="mi">1</span><span class="p">:</span> <span class="c1">// send data symbols</span>
      <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// iterate through bits in data</span>
        <span class="n">bool</span> <span class="n">bit</span> <span class="o">=</span> <span class="p">(</span><span class="n">raw</span> <span class="o">&gt;&gt;</span> <span class="n">i</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mi">0</span><span class="n">b1</span><span class="p">;</span>
        <span class="n">rmt_symbol_word_t</span> <span class="n">symbol</span> <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="n">bit</span><span class="p">];</span>
        <span class="n">encoded_symbols</span> <span class="o">+=</span> <span class="n">copy_encoder</span><span class="o">-&gt;</span><span class="n">encode</span><span class="p">(</span><span class="n">copy_encoder</span><span class="p">,</span> <span class="n">channel</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">symbol</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">rmt_symbol_word_t</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">session_state</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">session_state</span> <span class="o">&amp;</span> <span class="n">RMT_ENCODING_MEM_FULL</span><span class="p">)</span> <span class="p">{</span>
          <span class="n">state</span> <span class="o">|=</span> <span class="n">RMT_ENCODING_MEM_FULL</span><span class="p">;</span>
          <span class="k">goto</span> <span class="n">out</span><span class="p">;</span>
        <span class="p">}</span>
      <span class="p">}</span>
      <span class="c1">// data is encoded :)</span>
      <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">state</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
      <span class="n">state</span> <span class="o">|=</span> <span class="n">RMT_ENCODING_COMPLETE</span><span class="p">;</span>

    <span class="c1">// fall through</span>
    <span class="k">case</span> <span class="mi">2</span><span class="p">:</span> <span class="c1">// end end bit</span>
      <span class="n">encoded_symbols</span> <span class="o">+=</span> <span class="n">copy_encoder</span><span class="o">-&gt;</span><span class="n">encode</span><span class="p">(</span><span class="n">copy_encoder</span><span class="p">,</span> <span class="n">channel</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">end_bit</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">rmt_symbol_word_t</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">session_state</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">session_state</span> <span class="o">&amp;</span> <span class="n">RMT_ENCODING_COMPLETE</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">state</span> <span class="o">=</span> <span class="n">RMT_ENCODING_RESET</span><span class="p">;</span> <span class="c1">// go to next state</span>
        <span class="n">state</span> <span class="o">|=</span> <span class="n">RMT_ENCODING_COMPLETE</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">session_state</span> <span class="o">&amp;</span> <span class="n">RMT_ENCODING_MEM_FULL</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">state</span> <span class="o">|=</span> <span class="n">RMT_ENCODING_MEM_FULL</span><span class="p">;</span>
        <span class="k">goto</span> <span class="n">out</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="c1">// all done!</span>
  <span class="p">}</span>

<span class="nl">out:</span>
  <span class="o">*</span><span class="n">out_state</span> <span class="o">=</span> <span class="n">state</span><span class="p">;</span>
  <span class="k">return</span> <span class="n">encoded_symbols</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="n">esp_err_t</span> <span class="nf">rmt_del_curtis_encoder</span><span class="p">(</span><span class="n">rmt_encoder_t</span> <span class="o">*</span><span class="n">encoder</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">rmt_curtis_encoder_t</span> <span class="o">*</span><span class="n">curtis_encoder</span> <span class="o">=</span> <span class="n">__containerof</span><span class="p">(</span><span class="n">encoder</span><span class="p">,</span> <span class="n">rmt_curtis_encoder_t</span><span class="p">,</span> <span class="n">base</span><span class="p">);</span>
  <span class="n">rmt_del_encoder</span><span class="p">(</span><span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">copy_encoder</span><span class="p">);</span>
  <span class="n">free</span><span class="p">(</span><span class="n">curtis_encoder</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">ESP_OK</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="n">esp_err_t</span> <span class="nf">rmt_curtis_encoder_reset</span><span class="p">(</span><span class="n">rmt_encoder_t</span> <span class="o">*</span><span class="n">encoder</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">rmt_curtis_encoder_t</span> <span class="o">*</span><span class="n">curtis_encoder</span> <span class="o">=</span> <span class="n">__containerof</span><span class="p">(</span><span class="n">encoder</span><span class="p">,</span> <span class="n">rmt_curtis_encoder_t</span><span class="p">,</span> <span class="n">base</span><span class="p">);</span>
  <span class="n">rmt_encoder_reset</span><span class="p">(</span><span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">copy_encoder</span><span class="p">);</span>
  <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">state</span> <span class="o">=</span> <span class="n">RMT_ENCODING_RESET</span><span class="p">;</span>
  <span class="k">return</span> <span class="n">ESP_OK</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">esp_err_t</span> <span class="nf">rmt_new_curtis_encoder</span><span class="p">(</span><span class="n">rmt_encoder_handle_t</span> <span class="o">*</span><span class="n">out_encoder</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">rmt_curtis_encoder_t</span> <span class="o">*</span><span class="n">curtis_encoder</span> <span class="o">=</span> <span class="n">calloc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">rmt_curtis_encoder_t</span><span class="p">));</span>
  <span class="c1">// TODO: error check</span>
  <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">state</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">base</span><span class="p">.</span><span class="n">encode</span> <span class="o">=</span> <span class="n">rmt_encode_curtis</span><span class="p">;</span>
  <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">base</span><span class="p">.</span><span class="n">del</span> <span class="o">=</span> <span class="n">rmt_del_curtis_encoder</span><span class="p">;</span>
  <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">base</span><span class="p">.</span><span class="n">reset</span> <span class="o">=</span> <span class="n">rmt_curtis_encoder_reset</span><span class="p">;</span>

  <span class="n">rmt_copy_encoder_config_t</span> <span class="n">copy_encoder_config</span> <span class="o">=</span> <span class="p">{};</span>
  <span class="n">rmt_new_copy_encoder</span><span class="p">(</span><span class="o">&amp;</span><span class="n">copy_encoder_config</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">copy_encoder</span><span class="p">);</span>

  <span class="c1">// construct start, end bit</span>
  <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">start_bit</span> <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
  <span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">end_bit</span> <span class="o">=</span> <span class="n">bits</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>

  <span class="o">*</span><span class="n">out_encoder</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">curtis_encoder</span><span class="o">-&gt;</span><span class="n">base</span><span class="p">;</span>

  <span class="k">return</span> <span class="n">ESP_OK</span><span class="p">;</span>

<span class="p">}</span>

<span class="k">static</span> <span class="n">bool</span> <span class="nf">rmt_rx_done_callback</span><span class="p">(</span><span class="n">rmt_channel_handle_t</span> <span class="n">channel</span><span class="p">,</span> <span class="k">const</span> <span class="n">rmt_rx_done_event_data_t</span> <span class="o">*</span><span class="n">edata</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">user_data</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">BaseType_t</span> <span class="n">high_task_wakeup</span> <span class="o">=</span> <span class="n">pdFALSE</span><span class="p">;</span>
    <span class="n">QueueHandle_t</span> <span class="n">receive_queue</span> <span class="o">=</span> <span class="p">(</span><span class="n">QueueHandle_t</span><span class="p">)</span><span class="n">user_data</span><span class="p">;</span>
    <span class="c1">// send the received RMT symbols to the parser task</span>
    <span class="n">xQueueSendFromISR</span><span class="p">(</span><span class="n">receive_queue</span><span class="p">,</span> <span class="n">edata</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">high_task_wakeup</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">high_task_wakeup</span> <span class="o">==</span> <span class="n">pdTRUE</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="nf">round_duration</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">duration</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">mod</span> <span class="o">=</span> <span class="n">duration</span> <span class="o">%</span> <span class="mi">10</span><span class="p">;</span>
  
  <span class="k">if</span> <span class="p">(</span><span class="n">mod</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">duration</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">mod</span> <span class="o">&gt;=</span> <span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">duration</span> <span class="o">+</span> <span class="p">(</span><span class="mi">10</span> <span class="o">-</span> <span class="n">mod</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">duration</span> <span class="o">-</span> <span class="n">mod</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">static</span> <span class="kt">int</span> <span class="nf">extract_level</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">int</span> <span class="n">index</span><span class="p">,</span> <span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">level</span><span class="p">,</span> <span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">duration</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">level</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// this is inverted, so this is a zero. we can just increase index</span>
    <span class="k">return</span> <span class="n">index</span> <span class="o">+</span> <span class="n">duration</span> <span class="o">/</span> <span class="mi">10</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">duration</span> <span class="o">/</span> <span class="mi">10</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="o">*</span><span class="n">data</span> <span class="o">|=</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">index</span><span class="p">;</span>
    <span class="n">index</span><span class="o">++</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">index</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">round_symbol_word</span><span class="p">(</span><span class="n">rmt_symbol_word_t</span> <span class="o">*</span><span class="n">word</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">word</span><span class="o">-&gt;</span><span class="n">duration0</span> <span class="o">=</span> <span class="n">round_duration</span><span class="p">(</span><span class="n">word</span><span class="o">-&gt;</span><span class="n">duration0</span><span class="p">);</span>
  <span class="n">word</span><span class="o">-&gt;</span><span class="n">duration1</span> <span class="o">=</span> <span class="n">round_duration</span><span class="p">(</span><span class="n">word</span><span class="o">-&gt;</span><span class="n">duration1</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">uint16_t</span> <span class="n">sequence</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
  <span class="mh">0x3BA</span><span class="p">,</span>
  <span class="mh">0x2</span><span class="p">,</span>
  <span class="mh">0x2</span><span class="p">,</span>
  <span class="mh">0x3E2</span><span class="p">,</span>
  <span class="mh">0x2</span><span class="p">,</span>
  <span class="mh">0x102</span><span class="p">,</span>
  <span class="mh">0x15A</span>
<span class="p">};</span>

<span class="kt">uint16_t</span> <span class="n">master_seq</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
  <span class="mh">0x2E8</span><span class="p">,</span>
  <span class="mh">0x202</span><span class="p">,</span>
  <span class="mh">0x002</span><span class="p">,</span>
  <span class="mh">0x3E2</span><span class="p">,</span>
  <span class="mh">0x002</span><span class="p">,</span>
  <span class="mh">0x002</span><span class="p">,</span>
  <span class="mh">0x05A</span>
<span class="p">};</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">parse_frame</span><span class="p">(</span><span class="n">rmt_symbol_word_t</span> <span class="o">*</span><span class="n">rmt_symbols</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">symbol_num</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// printf("frame start---\r\n");</span>
    <span class="kt">uint16_t</span> <span class="n">data</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">data_idx</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">symbol_num</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">rmt_symbol_word_t</span> <span class="n">symbol</span> <span class="o">=</span> <span class="n">rmt_symbols</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
      <span class="c1">// round_symbol_word(&amp;symbol);</span>

      <span class="n">data_idx</span> <span class="o">=</span> <span class="n">extract_level</span><span class="p">(</span><span class="o">&amp;</span><span class="n">data</span><span class="p">,</span> <span class="n">data_idx</span><span class="p">,</span> <span class="n">symbol</span><span class="p">.</span><span class="n">level0</span><span class="p">,</span> <span class="n">symbol</span><span class="p">.</span><span class="n">duration0</span><span class="p">);</span>
      <span class="n">data_idx</span> <span class="o">=</span> <span class="n">extract_level</span><span class="p">(</span><span class="o">&amp;</span><span class="n">data</span><span class="p">,</span> <span class="n">data_idx</span><span class="p">,</span> <span class="n">symbol</span><span class="p">.</span><span class="n">level1</span><span class="p">,</span> <span class="n">symbol</span><span class="p">.</span><span class="n">duration1</span><span class="p">);</span>

      
      <span class="c1">// printf("{%d:%d},{%d:%d}\r\n", symbol.level0, symbol.duration0,</span>
      <span class="c1">//         symbol.level1, symbol.duration1);</span>

    <span class="p">}</span>

    <span class="k">while</span><span class="p">(</span><span class="n">data_idx</span> <span class="o">&lt;</span> <span class="mi">14</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// stick in remaining bits</span>
      <span class="n">data</span> <span class="o">|=</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">data_idx</span><span class="p">;</span>
      <span class="n">data_idx</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">data</span> <span class="o">=</span> <span class="p">(</span><span class="n">data</span> <span class="o">&gt;&gt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mi">0</span><span class="n">b1111111111</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">data</span> <span class="o">==</span> <span class="mh">0x5A</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">for</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">7</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">rmt_transmit</span><span class="p">(</span><span class="n">tx_channel_handle</span><span class="p">,</span> <span class="n">encoder</span><span class="p">,</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">sequence</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">uint16_t</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">tx_config</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="c1">// rmt_tx_wait_all_done(tx_channel_handle, 5);</span>
      
    <span class="p">}</span>
    <span class="c1">// ESP_LOGI(TAG, "&gt;&gt; data = %x", (data &gt;&gt; 1) &amp; 0b1111111111);</span>
    <span class="c1">// // printf("---frame end: ");</span>
<span class="p">}</span>


<span class="k">static</span> <span class="kt">void</span> <span class="nf">parse_all_frames</span><span class="p">(</span><span class="n">rmt_symbol_word_t</span> <span class="o">*</span><span class="n">rmt_symbols</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">symbol_num</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// breaks up frames into smaller ones</span>

  <span class="kt">int</span> <span class="n">current_duration</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="kt">size_t</span> <span class="n">current_size</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

  <span class="n">rmt_symbol_word_t</span> <span class="o">*</span><span class="n">start</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>

  <span class="k">for</span><span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">symbol_num</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">rmt_symbol_word_t</span> <span class="o">*</span><span class="n">word</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">rmt_symbols</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">start</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">start</span> <span class="o">=</span> <span class="n">word</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">round_symbol_word</span><span class="p">(</span><span class="n">word</span><span class="p">);</span>
    <span class="n">current_duration</span> <span class="o">+=</span> <span class="n">word</span><span class="o">-&gt;</span><span class="n">duration0</span> <span class="o">+</span> <span class="n">word</span><span class="o">-&gt;</span><span class="n">duration1</span><span class="p">;</span>
    <span class="n">current_size</span><span class="o">++</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">current_duration</span> <span class="o">/</span> <span class="mi">10</span> <span class="o">&gt;=</span> <span class="mi">12</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// ship it</span>
      <span class="n">parse_frame</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="n">current_size</span><span class="p">);</span>

      <span class="n">current_duration</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
      <span class="n">current_size</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
      <span class="n">start</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">start</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">parse_frame</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="n">current_size</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">app_main</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">ESP_LOGI</span><span class="p">(</span><span class="n">TAG</span><span class="p">,</span> <span class="s">"create rmt tx channel"</span><span class="p">);</span>

  <span class="c1">// init rx channel</span>
  <span class="n">rmt_rx_channel_config_t</span> <span class="n">rx_channel_config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">.</span><span class="n">clk_src</span><span class="o">=</span><span class="n">RMT_CLK_SRC_DEFAULT</span><span class="p">,</span>
    <span class="p">.</span><span class="n">gpio_num</span><span class="o">=</span><span class="n">SERIAL_TX_GPIO_NUM</span><span class="p">,</span>
    <span class="p">.</span><span class="n">mem_block_symbols</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span>
    <span class="p">.</span><span class="n">resolution_hz</span><span class="o">=</span><span class="n">SERIAL_FREQ_HZ</span><span class="p">,</span>
    <span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">invert_in</span><span class="o">=</span><span class="nb">true</span><span class="p">,</span>
    <span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">with_dma</span><span class="o">=</span><span class="nb">false</span><span class="p">,</span>
    <span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">io_loop_back</span><span class="o">=</span><span class="nb">true</span><span class="p">,</span>
  <span class="p">};</span>
  <span class="n">ESP_ERROR_CHECK</span><span class="p">(</span><span class="n">rmt_new_rx_channel</span><span class="p">(</span><span class="o">&amp;</span><span class="n">rx_channel_config</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">rx_channel_handle</span><span class="p">));</span>

  <span class="c1">// init tx channel</span>
  <span class="n">rmt_tx_channel_config_t</span> <span class="n">tx_channel_config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">.</span><span class="n">clk_src</span><span class="o">=</span><span class="n">RMT_CLK_SRC_DEFAULT</span><span class="p">,</span>
    <span class="p">.</span><span class="n">gpio_num</span> <span class="o">=</span> <span class="n">SERIAL_TX_GPIO_NUM</span><span class="p">,</span>
    <span class="p">.</span><span class="n">mem_block_symbols</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span>
    <span class="p">.</span><span class="n">resolution_hz</span><span class="o">=</span><span class="n">SERIAL_FREQ_HZ</span><span class="p">,</span>
    <span class="p">.</span><span class="n">trans_queue_depth</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>
    <span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">invert_out</span><span class="o">=</span><span class="nb">true</span><span class="p">,</span>
    <span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">with_dma</span><span class="o">=</span><span class="nb">false</span><span class="p">,</span>
    <span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">io_loop_back</span><span class="o">=</span><span class="nb">true</span><span class="p">,</span>
    <span class="p">.</span><span class="n">flags</span><span class="p">.</span><span class="n">io_od_mode</span><span class="o">=</span><span class="nb">true</span>
  <span class="p">};</span>
  
  <span class="n">ESP_ERROR_CHECK</span><span class="p">(</span><span class="n">rmt_new_tx_channel</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tx_channel_config</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">tx_channel_handle</span><span class="p">));</span>
  
  <span class="c1">// set gpio modes</span>
  <span class="c1">// gpio_set_pull_mode(SERIAL_TX_GPIO_NUM, GPIO_FLOATING);</span>
  <span class="c1">// gpio_set_pull_mode(SERIAL_TX_GPIO_NUM, GPIO_MODE_INPUT);</span>
  <span class="c1">// gpio_set_drive_capability(SERIAL_TX_GPIO_NUM, GPIO_DRIVE_CAP_0);</span>

  <span class="c1">// init rx queue and callbacks</span>
  <span class="n">QueueHandle_t</span> <span class="n">receive_queue</span> <span class="o">=</span> <span class="n">xQueueCreate</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">rmt_rx_done_event_data_t</span><span class="p">));</span>

  <span class="n">rmt_rx_event_callbacks_t</span> <span class="n">cbs</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">.</span><span class="n">on_recv_done</span><span class="o">=</span><span class="n">rmt_rx_done_callback</span>
  <span class="p">};</span>
  <span class="n">ESP_ERROR_CHECK</span><span class="p">(</span><span class="n">rmt_rx_register_event_callbacks</span><span class="p">(</span><span class="n">rx_channel_handle</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">cbs</span><span class="p">,</span> <span class="n">receive_queue</span><span class="p">));</span>

  <span class="n">rmt_receive_config_t</span> <span class="n">receive_config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">.</span><span class="n">signal_range_min_ns</span><span class="o">=</span><span class="mi">7</span><span class="o">*</span><span class="mi">1000</span><span class="p">,</span> <span class="c1">// start bit only</span>
    <span class="p">.</span><span class="n">signal_range_max_ns</span><span class="o">=</span><span class="mi">12</span><span class="o">*</span><span class="mi">10</span><span class="o">*</span><span class="mi">1000</span> <span class="o">+</span> <span class="mi">7</span><span class="o">*</span><span class="mi">1000</span>
  <span class="p">};</span>

  <span class="c1">// create tx encoder</span>
  
  <span class="n">rmt_new_curtis_encoder</span><span class="p">(</span><span class="o">&amp;</span><span class="n">encoder</span><span class="p">);</span>
  
  <span class="n">ESP_ERROR_CHECK</span><span class="p">(</span><span class="n">rmt_enable</span><span class="p">(</span><span class="n">tx_channel_handle</span><span class="p">));</span>
  <span class="n">ESP_ERROR_CHECK</span><span class="p">(</span><span class="n">rmt_enable</span><span class="p">(</span><span class="n">rx_channel_handle</span><span class="p">));</span>
  
  <span class="n">rmt_symbol_word_t</span> <span class="n">raw_symbols</span><span class="p">[</span><span class="mi">64</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span> <span class="c1">// more than enough</span>
  <span class="n">rmt_rx_done_event_data_t</span> <span class="n">rx_data</span><span class="p">;</span>

  <span class="n">ESP_ERROR_CHECK</span><span class="p">(</span><span class="n">rmt_receive</span><span class="p">(</span><span class="n">rx_channel_handle</span><span class="p">,</span> <span class="n">raw_symbols</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">raw_symbols</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">receive_config</span> <span class="p">));</span>


  
  <span class="k">for</span><span class="p">(;;)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">xQueueReceive</span><span class="p">(</span><span class="n">receive_queue</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">rx_data</span><span class="p">,</span> <span class="n">pdMS_TO_TICKS</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span> <span class="o">==</span> <span class="n">pdPASS</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// ESP_LOGI(TAG, "received data");</span>
        <span class="n">parse_all_frames</span><span class="p">(</span><span class="n">rx_data</span><span class="p">.</span><span class="n">received_symbols</span><span class="p">,</span> <span class="n">rx_data</span><span class="p">.</span><span class="n">num_symbols</span><span class="p">);</span>
          <span class="c1">// start receive again</span>
        <span class="n">ESP_ERROR_CHECK</span><span class="p">(</span><span class="n">rmt_receive</span><span class="p">(</span><span class="n">rx_channel_handle</span><span class="p">,</span> <span class="n">raw_symbols</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">raw_symbols</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">receive_config</span><span class="p">));</span>
        

        <span class="c1">// vTaskDelay(5);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="c1">// for(uint16_t i = 0; i &lt; 7; i++) {</span>
      <span class="c1">//   rmt_transmit(tx_channel_handle, encoder, (void *)&amp;master_seq[i], sizeof(uint16_t), &amp;tx_config);</span>
      <span class="c1">// }</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Kevin Evans</name></author><category term="esp32" /><category term="computers" /><summary type="html"><![CDATA[Hacking together 10-bit serial using the RMT peripheral]]></summary></entry><entry><title type="html">Accessing the Skylight picture frame</title><link href="/2024/01/20/skylight.html" rel="alternate" type="text/html" title="Accessing the Skylight picture frame" /><published>2024-01-20T00:00:00+00:00</published><updated>2024-01-20T00:00:00+00:00</updated><id>/2024/01/20/skylight</id><content type="html" xml:base="/2024/01/20/skylight.html"><![CDATA[<p>Over the holiday, we received a <a href="https://www.skylightframe.com/">Skylight picture frame</a>, a wifi-connected picture frame with an accompanying app. As soon as it arrived, I noticed there was a micro USB plug in the back and got curious. Can we “hack” into the device and get a better peek?</p>

<p><img src="/assets/images/2024-01-20-skylight-front.jpg" alt="skylight front" />
<img src="/assets/images/2024-01-20-skylight-back.jpg" alt="skylight rear" /></p>

<p>Attaching it to my computer and using <code class="language-plaintext highlighter-rouge">lsusb</code> shows it has a Rockchip in it. I feel like Rockchips will either be running some slim Linux distro or something Android-based. I ran <code class="language-plaintext highlighter-rouge">adb devices</code> and sure enough, it’s just a slimmed-down Android-based tablet,</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>adb devices
List of devices attached
3431A0008409	device
</code></pre></div></div>

<p>Opening a shell, we can see it has a Rockchip RK3128, a fairly high-performance chip for something so innocuous. I’m presuming the designers chose this for its FPU/GPU, since this picture frame supports video playback.</p>

<h2 id="packages">Packages</h2>

<p>Running <code class="language-plaintext highlighter-rouge">pm list packages</code>, we can see there’s a handful of apps installed. Mainly, it looks like the company installs <code class="language-plaintext highlighter-rouge">com.skylight</code> and a watchdog <code class="language-plaintext highlighter-rouge">skylight.watchdog</code>. It looks like the apps listen for the <code class="language-plaintext highlighter-rouge">android.intent.action.BOOT_COMPLETED</code> to launch the app. Interestingly, looking at the dump, the MainActivity is <code class="language-plaintext highlighter-rouge">com.skylight/odesk.johnlife.skylight.activity.MainActivity</code>, maybe based on <a href="https://github.com/johnlife/Glimpse-Android/tree/master">this repo</a> or <a href="https://github.com/glimpseframe/Android/">the parent repo</a>. (As an aside, what’s with <a href="https://github.com/johnlife/Glimpse-Android/blob/master/!admin.txt">these plaintext creds?</a>).</p>

<p>Since the Android browser is still installed, we can launch it using <code class="language-plaintext highlighter-rouge">monkey -p com.android.browser 1</code>:</p>

<p><img src="/assets/images/2024-01-20-skylight-browser.jpg" alt="skylight browser" /></p>

<p>Launching the settings, <code class="language-plaintext highlighter-rouge">monkey -p com.android.settings -c android.intent.category.LAUNCHER 1</code>, we can see it’s running Android 5.1.1. It has a few GB of storage. It’s a bit outdated, so I think I’ll be keeping this on an isolated network.</p>

<p><img src="/assets/images/2024-01-20-skylight-about.jpg" alt="skylight settings" />
<img src="/assets/images/2024-01-20-skylight-storage.jpg" alt="skylight storage" /></p>

<h2 id="summary">Summary</h2>

<p>The Skylight is a cute little picture frame and I’m glad the ADB interface is still not locked down. It looks like somebody could easily create their own app to take place of the default Skylight app, which could be a fun little project.</p>]]></content><author><name>Kevin Evans</name></author><category term="computers" /><category term="security" /><summary type="html"><![CDATA[Taking a deeper look at the Skylight picture frame]]></summary></entry></feed>