<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thomas Simmer</title>
    <description>The latest articles on DEV Community by Thomas Simmer (@thomassimmer).</description>
    <link>https://dev.to/thomassimmer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3910457%2F1ac04df2-dcca-42d4-a6b3-6038dd6abab6.jpeg</url>
      <title>DEV Community: Thomas Simmer</title>
      <link>https://dev.to/thomassimmer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thomassimmer"/>
    <language>en</language>
    <item>
      <title>I Built a Cyberpunk Forensics Simulator to Teach Blue Team Thinking</title>
      <dc:creator>Thomas Simmer</dc:creator>
      <pubDate>Thu, 04 Jun 2026 20:00:12 +0000</pubDate>
      <link>https://dev.to/thomassimmer/i-built-a-cyberpunk-forensics-simulator-to-teach-blue-team-thinking-529</link>
      <guid>https://dev.to/thomassimmer/i-built-a-cyberpunk-forensics-simulator-to-teach-blue-team-thinking-529</guid>
      <description>&lt;p&gt;&lt;strong&gt;Most security tools teach you to attack. I wanted to build something that teaches you to investigate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There’s something strange in how cybersecurity is taught.&lt;/p&gt;

&lt;p&gt;CTFs, labs, HackTheBox, TryHackMe: they’re all great. But almost all of them focus on the offensive perspective. Find the vulnerability, exploit it, capture the flag. Which makes sense. Offensive security is concrete, gameable, and satisfying.&lt;/p&gt;

&lt;p&gt;But the reality of most security work is different. Most people working in security spend their time on the blue team: reading logs, correlating events, writing incident reports, deciding whether a suspicious request is a false positive or the beginning of a breach. That work is harder to gamify. It’s also harder to learn.&lt;/p&gt;

&lt;p&gt;I wanted to fix that. So I built &lt;a href="https://thomassimmer.github.io/nightcity-tracer/" rel="noopener noreferrer"&gt;NightCity Tracer&lt;/a&gt;: an open-source, browser-based forensics simulator set in a cyberpunk universe.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;You play a SOC analyst in Night City. A breach happened, or is happening right now.&lt;/p&gt;

&lt;p&gt;You get the evidence: access logs, vulnerable source code, network maps, config files, email chains, git histories, memory dumps. You investigate, reconstruct the attack, and file an incident report.&lt;/p&gt;

&lt;p&gt;No flags. No shell to pop. Just evidence and judgment.&lt;/p&gt;

&lt;p&gt;The game scores you on precision (did you identify the right vulnerability?), defense efficiency (did you take the right action?), and in live scenarios, speed (how much damage did you contain before it was too late?).&lt;/p&gt;

&lt;p&gt;At the end, you get a debrief with a replay of the attacker’s exact path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two radically different modes
&lt;/h2&gt;

&lt;p&gt;The most important design decision was splitting scenarios into two temporal modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Post-mortem:&lt;/strong&gt; the breach already happened. All evidence is present from the start. You’re reconstructing a past attack from whatever was left behind. This is closer to digital forensics: patient, methodical, no time pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; the attack is in progress. Logs arrive in real time. A countdown is ticking. A data exfiltration is on-going. You have minutes to read the code, identify the vulnerability, and submit your report before the attacker succeeds.&lt;/p&gt;

&lt;p&gt;These two modes feel like different games. The live mode creates genuine pressure: players report making mistakes under the time constraint that they wouldn’t make with unlimited time. Which is exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario system
&lt;/h2&gt;

&lt;p&gt;Every scenario in NightCity Tracer is a self-contained TypeScript config file. The engine reads it and assembles a completely different experience: different panels, different evidence, different scoring weights, different narrative identity.&lt;/p&gt;

&lt;p&gt;A scenario declares:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Which evidence panels to display:&lt;/strong&gt; log_stream, code_editor, network_map, db_viewer, terminal and more. The UI builds itself dynamically from this list.&lt;br&gt;
&lt;strong&gt;- The event timeline (live mode):&lt;/strong&gt; when each batch of logs appears, when alerts fire, when game over triggers. Payloads can be randomized to prevent memorization.&lt;br&gt;
&lt;strong&gt;- Scoring dimensions and weights:&lt;/strong&gt; a post-mortem forensics scenario weights precision heavily; a live incident scenario weights speed and defense efficiency. Each scenario defines what “a good answer” looks like.&lt;br&gt;
&lt;strong&gt;- The incident report fields:&lt;/strong&gt; a stored XSS scenario asks about the injection point and the sanitization fix. A social engineering scenario might ask for a decision (cut access now, or monitor?) rather than a technical finding.&lt;br&gt;
&lt;strong&gt;- Corporate identity:&lt;/strong&gt; each scenario belongs to a megacorp or faction with its own UI accent colors, briefing tone, and narrative voice.&lt;/p&gt;

&lt;p&gt;This means scenarios can be radically different: not just in attack technique, but in what the player is actually being asked to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four scenarios shipping in V1
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trauma Team Dispatch: Token Forgery&lt;/strong&gt; &lt;em&gt;(tutorial, post-mortem, beginner)&lt;/em&gt;&lt;br&gt;
A medical response API was compromised via JWT algorithm confusion. You have post-breach logs and the source code. Designed to teach the investigation loop before any time pressure starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operation Med-Assist Override&lt;/strong&gt; &lt;em&gt;(post-mortem, beginner)&lt;/em&gt;&lt;br&gt;
An AI triage dispatch system was manipulated via prompt injection. The question isn’t just “what happened” but “what did the model do that it shouldn’t have, and why.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watson District: Samurai on Air&lt;/strong&gt; &lt;em&gt;(post-mortem, intermediate)&lt;/em&gt;&lt;br&gt;
94 billboards. One attacker. Three seconds of footage that cost NeonGrid Systems their biggest advertiser and triggered a police investigation. Figure out how a single file upload brought down an entire district’s display network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NightOps Platform&lt;/strong&gt; &lt;em&gt;(live, intermediate)&lt;/em&gt;&lt;br&gt;
Active operator identities, drop locations, client names: everything on the platform is being exfiltrated right now. Every second you spend reading the code is a merc whose cover is blown. Stop it before the damage is irreversible.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why cyberpunk?
&lt;/h2&gt;

&lt;p&gt;The aesthetic isn’t decoration. It does real work.&lt;/p&gt;

&lt;p&gt;Framing a JWT misconfiguration as “a Trauma Team dispatch system was compromised mid-emergency” changes how players engage with it. The scenario isn’t an exercise anymore: it has stakes, a world, a narrative identity.&lt;/p&gt;

&lt;p&gt;Each faction creates a completely different feel. Arasaka scenarios feel corporate and precise, with cold system messages and red accents. A Netwatch scenario would feel more covert, like you’re working inside a surveillance apparatus. The aesthetic lets the same underlying mechanics feel like different experiences.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tech stack
&lt;/h2&gt;

&lt;p&gt;100% static. React 19 + TypeScript + Vite + Tailwind v4, hosted on GitHub Pages. No backend, no accounts, no analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attacker state machine
&lt;/h2&gt;

&lt;p&gt;Live scenarios have an attacker that isn’t just a timer: it’s an actual state machine. The attacker progresses through phases (recon, exploit, exfil), each phase unlocking new log batches and changing the threat level. Players can interact: blocking an IP delays the attacker, patching a code vulnerability can stop the exfil entirely.&lt;/p&gt;

&lt;p&gt;This creates a feedback loop that’s closer to real incident response: your actions have consequences, and the attacker progresses based on what you do or fail to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’m looking for
&lt;/h2&gt;

&lt;p&gt;The project is open source and the scenario library is the thing that makes or breaks it.&lt;/p&gt;

&lt;p&gt;Writing a scenario doesn’t require deep React knowledge: the config format is documented in the README with a schema walkthrough. If you know a real-world attack technique that would make a good investigation (a misconfigured S3 bucket, a malicious npm dependency, a phishing chain reconstruction, an insider threat), issues are open.&lt;/p&gt;

&lt;p&gt;UI work, new panel types, and engine improvements are equally welcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/thomassimmer/nightcity-tracer" rel="noopener noreferrer"&gt;thomassimmer/nightcity-tracer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Game:&lt;/strong&gt; &lt;a href="https://thomassimmer.github.io/nightcity-tracer/" rel="noopener noreferrer"&gt;thomassimmer.github.io/nightcity-tracer/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>gamedev</category>
      <category>cyberpunk</category>
      <category>blueteam</category>
    </item>
    <item>
      <title>CyberKey: What I learned building an embedded project coming from web development</title>
      <dc:creator>Thomas Simmer</dc:creator>
      <pubDate>Sun, 03 May 2026 14:44:40 +0000</pubDate>
      <link>https://dev.to/thomassimmer/cyberkey-what-i-learned-building-an-embedded-project-coming-from-web-development-1c62</link>
      <guid>https://dev.to/thomassimmer/cyberkey-what-i-learned-building-an-embedded-project-coming-from-web-development-1c62</guid>
      <description>&lt;p&gt;I'm a fullstack web developer with 6 years of experience. Python, Rust, JS, databases, and APIs. That's my day job. I had never touched electronics.&lt;/p&gt;

&lt;p&gt;A few weeks ago, I decided to build CyberKey. The itch came from something boring at work: my VPN disconnects when I lock my computer, and I have to type a TOTP code several times a day. Unlock my phone, open the authenticator app, read the code, type it before it expires. Every time. CyberKey is a small device that eliminates that friction. Place the right finger on it, and the code is typed automatically over Bluetooth. No app, no phone, no copy-paste. Just a finger, and the code appears in the input field.&lt;/p&gt;

&lt;p&gt;The project runs on an M5StickC Plus 2, an ESP32 microcontroller the size of a lighter. It's made by M5Stack, a company that produces ESP32-based modules with a screen, a battery, and built-in connectors, as well as a range of compatible sensors and peripherals. The fingerprint sensor in CyberKey is one of theirs. It's a reasonable entry point for software developers who don't want to deal with breadboards and soldering. The firmware (the program that runs directly on the chip, with no operating system underneath) is written in Rust. I was heavily assisted by Claude throughout the development, and most of the work happened during my baby's nap times, roughly two hours a day. Without that help, I couldn't have pulled this off in a reasonable amount of time.&lt;/p&gt;

&lt;p&gt;This is not a tutorial. It's a synthesis of the concepts I had never encountered in six years of web development, and a walkthrough of the firmware architecture, for web developers curious about what's on the other side.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hardware: when your code talks to physics
&lt;/h2&gt;

&lt;p&gt;The first thing that caught me off guard is how different a microcontroller is from anything I had worked with before. A server has an operating system under it: a scheduler, a filesystem, a networking stack, a memory allocator. You write code on top of all that. A microcontroller has none of it. You get a chip, some flash memory, and a few hundred kilobytes of RAM. Whatever your program needs to do, it has to set up itself.&lt;/p&gt;

&lt;p&gt;The most concrete expression of this is the &lt;strong&gt;GPIO&lt;/strong&gt; pins (General Purpose Input/Output). These are the physical legs of the chip. Each pin is connected to a wire on the board, and your code can set it high (3.3V) or low (0V), or read its current state. A boolean, but made of electricity. Turning on an LED is literally setting a pin to &lt;code&gt;true&lt;/code&gt;. Reading a button press is reading a pin's value in a loop.&lt;/p&gt;

&lt;p&gt;To make chips talk to each other, the embedded world uses a small set of standard protocols. The three I used in CyberKey are &lt;strong&gt;UART&lt;/strong&gt;, &lt;strong&gt;I2C&lt;/strong&gt;, and &lt;strong&gt;SPI&lt;/strong&gt;. The analogy that clicked for me is that they fill roughly the same role as different network protocols in web development: each is a tradeoff between simplicity, speed, and the number of devices you can connect.&lt;/p&gt;

&lt;p&gt;UART is the simplest: two wires, two devices, no shared clock. Both sides agree in advance on a speed (baud rate) and just send bits. It's what's behind the USB serial port you use to flash and debug a board. I2C uses only two wires but supports many devices on the same bus, each with an address, like an IP. It's slower, but perfect for sensors and clocks that don't need high throughput. SPI is the fastest: four wires, a dedicated clock, and it can push data at 80 MHz, which is why it's used for displays.&lt;/p&gt;

&lt;p&gt;What surprised me most was the hierarchy behind SPI. It has four wires: CLK (clock), MOSI (data out), MISO (data in), and CS (chip select). The first three form the &lt;strong&gt;bus&lt;/strong&gt;, physically shared between all components like a single cable soldered to multiple chips at once. When the ESP32 sends a clock signal, every connected chip sees it simultaneously. The fourth wire, CS, is what makes one chip respond and not the others: when CS is pulled low for a specific chip, that chip listens; the rest ignore it.&lt;/p&gt;

&lt;p&gt;In code, this maps to a clean hierarchy: a &lt;strong&gt;driver&lt;/strong&gt; manages the three shared wires, and a &lt;strong&gt;device&lt;/strong&gt; wraps that driver together with one specific CS pin to represent a single component. If you had a display and an SD card on the same bus, you'd have one driver and two devices, one per CS pin. The word "bus" is no accident. It's the same metaphor as a city bus, a shared route that multiple passengers can board, each getting off at their own stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  The firmware architecture
&lt;/h2&gt;

&lt;p&gt;A web server entry point does a lot before it starts serving requests: it connects to the database, registers middleware, sets up routes, starts a background job queue. But underneath all of that, there's an operating system managing memory and scheduling, a runtime handling I/O, a framework providing the event loop. You're building on top of layers that already exist.&lt;/p&gt;

&lt;p&gt;In embedded, those layers don't exist. &lt;code&gt;main()&lt;/code&gt; is not a starting point. It's the entire program. In CyberKey's firmware, &lt;code&gt;main()&lt;/code&gt; is a long sequential initialization function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Power pin&lt;/strong&gt;: a GPIO that must be held high immediately, or the board shuts off when you release the button&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battery ADC&lt;/strong&gt;: the analog-to-digital converter that reads the battery voltage; the chip only understands numbers, not voltages, so it needs hardware to translate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UART for the CLI&lt;/strong&gt;: the serial connection to a laptop, used to enroll fingerprints and sync the clock over USB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I2C for the real-time clock&lt;/strong&gt;: so the device knows what time it is to generate valid TOTP codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPI for the display&lt;/strong&gt;: the screen that shows the current status, the TOTP code, and the BLE pairing PIN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BLE&lt;/strong&gt;: Bluetooth Low Energy, the wireless protocol that makes the device appear as a keyboard to a computer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprint sensor&lt;/strong&gt;: connected via UART on the Grove port (a standardized connector from M5Stack that carries power and a communication protocol in a single plug, no soldering required)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each component needs its own protocol configuration, its own pins, its own driver. Only once everything is initialized does control pass to the main loop.&lt;/p&gt;

&lt;p&gt;The order matters. Initializing the display controller before completing its hardware reset sequence produces a black screen. Powering up BLE before the SPI bus is ready causes a crash. There's no framework catching your mistakes. If the sequence is wrong, the device just doesn't work, often without any error message.&lt;/p&gt;

&lt;p&gt;After initialization, the firmware runs a loop that never exits. It checks the buttons, handles BLE events, listens for fingerprint matches, updates the display, reads the battery level. This is the event loop you write yourself.&lt;/p&gt;

&lt;p&gt;One thing that has no equivalent in web development is power management. A device running on a 200 mAh battery drains fast, and every component you leave running costs you runtime. The ESP32 has sleep modes that should bring power down dramatically between uses. I spent a fair amount of time trying to make them work — and couldn't. For reasons I never fully pinned down, the chip never actually slept. The simplest solution that did work: pressing button C cuts power entirely by driving a GPIO low. The board shuts off instantly, draws nothing, and reconnects to bonded hosts automatically in a few seconds on next boot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rust in embedded: between C++ and nothing
&lt;/h2&gt;

&lt;p&gt;Rust is not the dominant language in embedded. C and C++ are. The official ESP32 SDK from Espressif (called ESP-IDF) is written in C. M5Stack's official drivers are in C++. Most of the community, the tutorials, the examples: C and C++.&lt;/p&gt;

&lt;p&gt;What makes Rust usable here is a layer of crates that wrap the ESP-IDF C code and expose it with a Rust API. When I call a function to read the battery voltage, it's Rust on the front but C in the back. You get the safety guarantees of Rust, but you're still standing on a foundation written in C.&lt;/p&gt;

&lt;p&gt;The practical consequence is that reading C++ code became part of the workflow. To understand how a component behaves, the most reliable source is often the official C++ driver: which pin to toggle, in what order, with what timing. For the fingerprint sensor, M5Stack publishes an Arduino driver in C++. I read it, understood the UART communication protocol it implements, and rewrote it from scratch in Rust. Not because Rust required it, but because no Rust driver existed for that sensor.&lt;/p&gt;

&lt;p&gt;This gave me a crate I called &lt;code&gt;fingerprint2-rs&lt;/code&gt;, compiled with &lt;code&gt;no_std&lt;/code&gt;. The &lt;code&gt;no_std&lt;/code&gt; annotation tells the compiler this code cannot rely on the standard library, which assumes an operating system underneath. No OS, no standard library, no runtime overhead.&lt;/p&gt;

&lt;p&gt;As for what Rust concretely brings: the compiler enforces error handling at every step, which matters when a failed I2C read can silently corrupt your state. Memory is managed explicitly, without a garbage collector. With a few hundred kilobytes to work with, that matters. I won't oversell it: Rust didn't make the hardware easier to understand. But it made the code easier to trust once it compiled.&lt;/p&gt;




&lt;h2&gt;
  
  
  What web development doesn't prepare you for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No hot reload.&lt;/strong&gt; Every change on the firmware follows the same cycle: edit the code, compile, flash the binary onto the device over USB, wait for it to boot, observe. A full iteration takes around a minute. You learn quickly to think before you type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A crash can brick the device.&lt;/strong&gt; In web development, an unhandled exception prints a stack trace and the process restarts. In embedded, bad firmware can leave the device in an infinite reboot loop with no output, or completely unresponsive. Bricking means the device becomes as useful as a brick: it won't boot, and recovering it requires a specific flashing procedure if it's even possible. It never happened to me on this project, but the possibility shapes how carefully you test each change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory is not elastic.&lt;/strong&gt; The ESP32 has 520 KB of internal RAM. No heap growth, no swap, no "just add more". Every allocation is a decision. This is where &lt;code&gt;no_std&lt;/code&gt; earns its place: memory usage becomes explicit and predictable by design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The battery is always on your mind.&lt;/strong&gt; In web development, energy consumption is invisible, it's the cloud provider's problem. On a device running on a 200 mAh battery, every component you leave running costs you autonomy. It forces a different way of thinking about every architectural decision, one I hadn't anticipated at all coming from web.&lt;/p&gt;




&lt;h2&gt;
  
  
  Developing with an AI as co-pilot
&lt;/h2&gt;

&lt;p&gt;I mentioned it in the intro, but it's worth being specific: I relied heavily on Claude throughout this project. Not as a code generator I blindly trusted, but as a way to stay unblocked. When you have two hours before your baby wakes up, you can't afford to spend forty-five minutes figuring out why your I2C bus is hanging. Having something that can explain the concept, point you to the relevant part, and sketch a direction changes the pace completely.&lt;/p&gt;

&lt;p&gt;What it doesn't replace is the decisions that require judgment. I drove the direction, the features, the architecture, the quality bar. When something didn't work, I tested on the device, read the terminal output, and went looking for answers in M5Stack's official documentation, their GitHub repositories, and a few open-source projects built on the same hardware. Several times that's what finally unblocked us, not the AI. And sometimes it was just intuition that turned out to be right.&lt;/p&gt;

&lt;p&gt;The risk I ran into: moving too fast. At several points I implemented something that worked, shipped it, and moved on without fully understanding what I had just written. It caught up with me later when something broke and I had to re-read my own code like it was someone else's.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results and takeaways
&lt;/h2&gt;

&lt;p&gt;The device works. Place an enrolled finger, the sensor matches it, the TOTP code appears on the screen and is typed over Bluetooth in under a second. BLE pairing, the display, the real-time clock, the USB CLI: all of it functions as intended. For a first embedded project, I'm happy with where it landed.&lt;/p&gt;

&lt;p&gt;The one disappointment is battery life. With a 200 mAh battery, the device lasts around 3–4 hours. I tried a lot of things to improve that — sleep modes, sensor standby, various combinations, and none of it moved the needle in any meaningful way. I never fully understood why. For now, the pragmatic solution is a hard power-off: one press of button C cuts all power at the hardware level, and the device reconnects to bonded hosts automatically on next boot. Not elegant, but honest. If you have an idea that doesn't require hardware changes, I'd genuinely love to see a pull request.&lt;/p&gt;

&lt;p&gt;If I did this again, I'd take more time upfront to understand the core concepts before writing any code. The moments where I got stuck hardest were always the moments where I had skipped the fundamentals. I'd also add logs from the very beginning, debugging embedded hardware without them means staring at a silent device and guessing.&lt;/p&gt;

&lt;p&gt;The broader takeaway: embedded development is accessible to a web developer. The concepts are unfamiliar, but they're learnable, and the instincts for clean architecture and readable code transfer well. It's just a different kind of disorienting than picking up a new framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  One more thing: Cyberpunk 2077
&lt;/h2&gt;

&lt;p&gt;Part of what drives this project is Cyberpunk 2077. I played it two years ago, before my kid was born and, it was the original inspiration. CyberKey only borrows the aesthetic: the UI typeface, the color palette. But the broader idea is to build things that exist in that world and don't exist yet in ours, with off-the-shelf hardware and a reasonable amount of work.&lt;/p&gt;

&lt;p&gt;If there's a piece of technology from the game you'd want to see built for real, I'd be curious to know. That's where I'm going next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thomassimmer/CyberKey" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;br&gt;
&lt;a href="https://youtu.be/Q93ilcUGO0s" rel="noopener noreferrer"&gt;Demo video&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>m5stack</category>
      <category>cyberpunk</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
