From fcec028c7433dcd599f76356eeb4b13c72fb1628 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Thu, 28 May 2026 15:36:34 +0100 Subject: [PATCH] Update --- README.md | 6 ++---- src/content/posts/ad-astra-attiny85-game-engine.md | 12 ++---------- src/content/posts/avoid-early-web-game.md | 2 +- src/content/posts/backup-container-btrfs-borg.md | 7 +------ src/content/posts/city-simulation-unity-traffic.md | 10 +++------- src/content/posts/declared-shared-simulation-code.md | 6 +----- src/content/posts/fizika-erettsegi-practice-app.md | 2 +- src/content/posts/fleeting-garden-webgpu-drawing.md | 8 ++------ .../posts/foreign-exchange-prediction-experiment.md | 2 +- src/content/posts/frame-eink-photo-display.md | 12 +++--------- .../posts/graph-editor-javafx-simulation-input.md | 6 ++---- src/content/posts/greatai-ai-deployment-api.md | 6 +----- src/content/posts/life-towers-immutable-tries.md | 6 +----- src/content/posts/lights-synchronized-to-music.md | 8 +++----- src/content/posts/my-notes-android-markdown-app.md | 6 ++---- src/content/posts/nuclear-cooling-simulation.md | 8 ++------ .../posts/perfect-postcode-rust-property-server.md | 6 +----- src/content/posts/photo-colour-grader.md | 6 +++--- src/content/posts/photo-site-generator.md | 4 ++-- src/content/posts/platform-game-c-sdl.md | 8 ++++---- src/content/posts/reconcile-text-3-way-merge.md | 7 +------ src/content/posts/sdf-2d-ray-tracing.md | 8 ++------ src/content/posts/vault-link-obsidian-sync.md | 9 ++------- src/content/projects/declared.md | 2 +- src/content/projects/fizika.md | 2 +- src/content/projects/vault-link.md | 2 +- 26 files changed, 46 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index c4b04c0..64893ae 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # schmelczer.dev -A static personal blog for Andras Schmelczer, built with Astro. +Engineering writeups by Andras Schmelczer: finished projects with the design constraints left in. Built with Astro, no required client JavaScript. -The site is article-first: articles live in `src/content/posts`, project index entries -live in `src/content/projects`, and normal pages are rendered as static HTML with no -required client JavaScript. +Articles live in `src/content/posts`, project index entries in `src/content/projects`, and normal pages are rendered as static HTML. ## Setup diff --git a/src/content/posts/ad-astra-attiny85-game-engine.md b/src/content/posts/ad-astra-attiny85-game-engine.md index fed5bf8..6dbb016 100644 --- a/src/content/posts/ad-astra-attiny85-game-engine.md +++ b/src/content/posts/ad-astra-attiny85-game-engine.md @@ -26,17 +26,9 @@ media: transcript: No spoken dialogue. The handheld board runs its OLED game; the player moves through the small display while the IR input controls gameplay. --- -**The short version:** +I'd done microcontroller work on dev boards before and it always felt like I was renting the hardware. As soon as I had a real board with my own soldering on it, bugs stopped feeling like software inconveniences and started feeling like consequences of choices I'd made in KiCad. That shift was most of the value of doing it this way. Four years on from [my first hardware project](/articles/lights-synchronized-to-music/), the lesson was that owning the whole stack down to the copper changes how you debug. -- A handheld game built from the PCB up: ATtiny85V, 0.96" SPI OLED, TSOP4838 IR receiver, 3.3V regulator, battery. -- 8-bit ALU at 8 MHz. Every byte and every cycle has a price tag, and that's the whole point. -- 15–20 ms peak frame time, so gameplay never drops below 50 FPS. Power draw ~31 mW peak, ~1.5 mA standby. - -## Why the PCB changed the project - -I'd done microcontroller work on dev boards before and it always felt like I was renting the hardware. As soon as I had a real board with my soldering on it, bugs stopped feeling like software inconveniences and started feeling like consequences of choices I'd made in KiCad. That shift was most of the value of doing it this way. - -The constraint that mattered: an 8-bit ALU at 8 MHz, with no FPU, no SIMD instruction set, and 8 KB of flash. Anything I built had to fit inside that, or I'd be staring at a brick. +This one is a handheld game built from the PCB up around an ATtiny85V: 8-bit ALU at 8 MHz, no FPU, no SIMD, 8 KB of flash. Anything I built had to fit inside that, or I'd be staring at a brick. ## The bits worth showing diff --git a/src/content/posts/avoid-early-web-game.md b/src/content/posts/avoid-early-web-game.md index 01a608e..d298606 100644 --- a/src/content/posts/avoid-early-web-game.md +++ b/src/content/posts/avoid-early-web-game.md @@ -13,4 +13,4 @@ outcome: My first browser game; kept for the timeline audience: general --- -The first browser game I wrote. It's not good, but it was the moment a `` element stopped being mysterious. Keeping it here because pretending the older work didn't happen would be dishonest. +Keeping it here because pretending the older work didn't happen would be dishonest. The first browser game I wrote, January 2018. It isn't good, but it was the moment a `` element stopped being mysterious. diff --git a/src/content/posts/backup-container-btrfs-borg.md b/src/content/posts/backup-container-btrfs-borg.md index dd45273..26a4cbf 100644 --- a/src/content/posts/backup-container-btrfs-borg.md +++ b/src/content/posts/backup-container-btrfs-borg.md @@ -2,7 +2,6 @@ title: Backing Up Running Databases Without Stopping Them description: A Bash container around BorgBackup. BTRFS snapshots give atomic consistency, numeric env vars give multi-target 3-2-1, the loop is sleep not cron. date: 2026-05-29 -draft: true projectPeriod: '2024-2026' thumbnail: src: ./_assets/backup.png @@ -20,11 +19,7 @@ links: url: https://github.com/schmelczer/backup-container/pkgs/container/backup-container --- -**The short version:** - -- One Alpine container, ~75 lines of Bash, that snapshots a BTRFS volume and pushes the snapshot to one or more [Borg](https://borgbackup.readthedocs.io/) repositories on a fixed interval. The snapshot is the only thing standing between "consistent backup" and "corrupt database in the archive." -- Multi-target via numeric env vars (`BORG_REPO_0`, `BORG_REPO_1`, ...). The wrapper iterates until the next index isn't set. No config format, no DSL; the env file is the configuration. -- Two years of self-hosting, multiple restored incidents, zero data loss I noticed. +Once you self-host a few services with live databases, the backup question stops being theoretical. A Postgres or SQLite file half-written when `tar` reads it goes into the archive in a state nothing on Earth will replay; you just don't find out until the restore. Two years in, with multiple incidents I had to actually recover from (including the photos behind the [e-ink frame](/articles/frame-eink-photo-display/)), I trust this stack precisely because the correctness argument is short: BTRFS gives me an atomic snapshot, and everything above it can be a shell script. One Alpine container, ~75 lines of Bash, pushes that snapshot to one or more [Borg](https://borgbackup.readthedocs.io/) repositories on a fixed interval. Multi-target is numeric env vars (`BORG_REPO_0`, `BORG_REPO_1`, ...). No config format, no DSL; the env file is the configuration. ## The problem the snapshot solves diff --git a/src/content/posts/city-simulation-unity-traffic.md b/src/content/posts/city-simulation-unity-traffic.md index 1c7e9ba..4626d29 100644 --- a/src/content/posts/city-simulation-unity-traffic.md +++ b/src/content/posts/city-simulation-unity-traffic.md @@ -14,12 +14,8 @@ audience: technical links: [] --- -A small city in Unity where the traffic lights were driven by a REST API. Contestants in a PLC cybersecurity event would write control logic; bad logic made cars crash, and they'd watch it happen. +Most security challenges punish wrong answers with a red "incorrect." This one punished them with car wrecks, and people learned faster. A PLC cybersecurity event in the summer of 2018 needed something visceral; I built a small Unity city where the traffic lights were driven by a REST API and contestants wrote the control logic. -Three things are worth saying about it: +All decisions ran on the server and got broadcast to clients. The harder problem wasn't the simulation; it was making the broadcast fault-tolerant on conference Wi-Fi without flooding it. I built it solo, including the models and animations in Blender. Not a flex, just context for why everything's a little janky. -- **Visual feedback was the whole point.** Most security challenges punish wrong answers with a red "incorrect." This one punished them with car wrecks, and people learned faster. -- **Server-client, all decisions on the server.** Every agent's behaviour was computed centrally and broadcast. The harder problem wasn't simulation; it was getting the broadcast to be fault-tolerant on the conference Wi-Fi without flooding it. -- **Built it solo, including the models and animations in Blender.** Not a flex, just context for why everything's a little janky. - -There was also a HUD overlay for tweets, which felt clever at the time and dated horribly. Skip that part. +There was also a HUD overlay for tweets. It felt clever at the time and dated horribly. Skip that part. diff --git a/src/content/posts/declared-shared-simulation-code.md b/src/content/posts/declared-shared-simulation-code.md index 59fae5f..30e0aa8 100644 --- a/src/content/posts/declared-shared-simulation-code.md +++ b/src/content/posts/declared-shared-simulation-code.md @@ -25,11 +25,7 @@ media: caption: A real game loop is a worse audience than a tech demo. That's the point. --- -**The short version:** - -- Conquest-style space shooter. Two teams, small planets, points for control, ray-traced 2D rendering. -- Built on top of [SDF-2D](/articles/sdf-2d-ray-tracing/), partly to prove the renderer worked outside a thesis demo. -- The architecture decision worth remembering: one TypeScript module containing the game rules, linked by both the Node server and the browser client. Same code, both sides of the wire. +My thesis was a renderer; proving it in a real multiplayer loop was the point. A real game loop is a worse audience than a tech demo. That's the point. So through autumn 2020 I built decla.red on top of [SDF-2D](/articles/sdf-2d-ray-tracing/): a conquest-style space shooter, two teams, small planets, ray-traced 2D rendering, browser and mobile. The architecture decision worth remembering came out of needing the server and the client to stop lying to each other: one TypeScript module containing the game rules, linked by both sides of the wire. ## The split that usually goes wrong diff --git a/src/content/posts/fizika-erettsegi-practice-app.md b/src/content/posts/fizika-erettsegi-practice-app.md index 0ad2794..d6ae5e5 100644 --- a/src/content/posts/fizika-erettsegi-practice-app.md +++ b/src/content/posts/fizika-erettsegi-practice-app.md @@ -30,4 +30,4 @@ What I'd change if I were starting it now: - **Markdown source, not a hand-edited JSON file.** Editing questions in JSON is fine until you forget a comma at 1am and the site stops loading. - **A real licence note on the question text.** The papers are public exam material, but it's worth saying so somewhere on the page. -It's been online in some form for eight years. Every spring I get a few emails from students asking whether I'll add the latest year's paper. I usually do, eventually. That's a feedback loop I didn't design for and don't want to lose. +It's been online in some form for eight years. Every spring I get a few emails from students asking whether I'll add the latest year's paper. I usually do, eventually. The thing I made for myself in 2017 is still doing its job for someone else's last year of high school, and that's the only metric on it I actually care about. diff --git a/src/content/posts/fleeting-garden-webgpu-drawing.md b/src/content/posts/fleeting-garden-webgpu-drawing.md index ef9651c..3617a63 100644 --- a/src/content/posts/fleeting-garden-webgpu-drawing.md +++ b/src/content/posts/fleeting-garden-webgpu-drawing.md @@ -25,11 +25,7 @@ media: caption: A snapshot from one session. What you see is the trail texture; the agents that drew it are already gone. --- -**The three-bullet pitch:** - -- Draw a stroke; agents spawn along it, follow the trail, and slowly overwrite the patch you laid down. You shape the gesture, the garden owns the response. -- Six "vibes" share the same simulation. The personality of each one lives in a 3×3 matrix of nine numbers in `{-1, 0, 1}`. That's the entire behaviour rule. -- One static HTML file. `vite build`, `rsync`, done. Runs offline, can be emailed, no account, no save state. +Nine numbers in `{-1, 0, 1}` arranged in a 3×3 matrix decide an entire vibe's personality. That constraint is what kept me up: proving simplicity can be expressive, that you don't need a behaviour function per preset. A WebGPU drawing toy where you stroke a colour, agents spawn along it, and the garden slowly overwrites the patch you laid down. One static HTML file, six compute stages, none of them skippable. ## Why physarum needed a knob @@ -54,7 +50,7 @@ Adding a tenth number to the matrix would tax every existing vibe. Tuning the ni Six stages, ten WGSL files, each one short enough that I can hold it in my head when something breaks: 1. **Agent step:** sample the trail at a sensor offset, pick a turn, move, deposit colour. ~300 lines, the longest one. -2. **Diffusion:** blur and decay so old marks soften. +2. **Diffusion:** blur and decay so old marks soften. The boring one, and the one you can't skip: without it, strokes stay forever and the garden collapses into noise. 3. **Brush:** write user strokes into both the trail texture and a separate "source" texture the agents can read. 4. **Eraser:** two variants: one clears a region of the trail, the other kills agents in a radius. 5. **Agent generation:** spawn along strokes, resize the buffer when the cap changes, compact after erasure so dead slots don't waste GPU time. diff --git a/src/content/posts/foreign-exchange-prediction-experiment.md b/src/content/posts/foreign-exchange-prediction-experiment.md index 43e7b3f..832ad2b 100644 --- a/src/content/posts/foreign-exchange-prediction-experiment.md +++ b/src/content/posts/foreign-exchange-prediction-experiment.md @@ -14,7 +14,7 @@ audience: technical links: [] --- -A linear regression in the frequency domain, dressed up. Old portfolio screenshots showed the prediction (blue) tracking the actual rate (green) closely enough to be flattering. I did not trade real money with it. +In the autumn of 2019 I was an undergrad with a few weekends free and the quiet conviction that I could find a small edge on EUR/USD. The screenshots were flattering: the prediction (blue) hugged the actual rate (green) in a way that looked like skill. A linear regression in the frequency domain, dressed up. I did not trade real money with it, and that restraint is the only thing about the project that aged well. The pipeline: diff --git a/src/content/posts/frame-eink-photo-display.md b/src/content/posts/frame-eink-photo-display.md index e623e9f..9328550 100644 --- a/src/content/posts/frame-eink-photo-display.md +++ b/src/content/posts/frame-eink-photo-display.md @@ -31,11 +31,9 @@ media: caption: The bottom corners carry the photo's age and EXIF location. Painted as text on top, so the dither can't smear them. --- -**The short version:** +In 2024, researchers found family-blog photos of Brazilian children inside the LAION training set. Self-hosting your photos used to be a preference; it's a safeguarding decision now. Nixplay's cloud-tied frames have bricked. Funimation deleted libraries people had paid for. I wanted a photo frame on the hallway wall, and I wasn't going to hand the family album to a vendor who could close the doors on it. -- A Raspberry Pi Zero 2W drives Waveshare's [PhotoPainter](https://www.waveshare.com/wiki/PhotoPainter), a 7.3" 6-colour ACeP e-ink panel. Cron fires every 15 minutes; if [Home Assistant](https://www.home-assistant.io/) says the house is empty (or it's between midnight and 7am), the script quits. -- Photo source is my self-hosted [Immich](https://immich.app/) library. The pool is weighted toward "on this day," favourites, and recent uploads, with a 7-day rolling history to avoid repeats. -- Each accepted candidate is face-aware cropped, contrast and saturation boosted (e-ink lacks both), Atkinson-dithered to the 6-colour palette, then labelled with capture age and EXIF location before pushing. A few hundred lines of stdlib Python on top of Waveshare's reference driver. +So it's a Raspberry Pi Zero 2W driving Waveshare's [PhotoPainter](https://www.waveshare.com/wiki/PhotoPainter) panel, pulling from my self-hosted [Immich](https://immich.app/) library, part of the same [self-hosting setup I back up with btrfs and borg](/articles/backup-container-btrfs-borg/). A few hundred lines of stdlib Python on top of the reference driver. ## Why a stupid amount of engineering for a picture on a wall @@ -67,15 +65,11 @@ The dither is where the choice visibly matters. The panel can only show black, w Hundred Rabbits, a couple who live offshore on a sailboat doing permacomputing in practice, hold themselves to a rule: any system they depend on should be reimplementable in a weekend. The frame meets the bar. A few hundred lines of stdlib Python on a documented panel, reading from an HTTP endpoint that returns JPEGs. It came together over an afternoon with Claude Code plus a couple of weekends tuning the picker and the dither; the repo is public partly as a reference for anyone wanting to do something similar. If Immich disappears tomorrow the selection logic is eighty lines I can repoint at whatever replaces it. -This stopped being hobbyist territory around 2024, when researchers found family-blog photos of Brazilian children inside the LAION training set. Self-hosting your photos used to be a preference; it's becoming a safeguarding decision. Don't ask whether the hassle is worth it now. Ask what state you'd be in if any one of your platforms went dark, and notice that this isn't a hypothetical. Nixplay's cloud-tied frames have bricked. Funimation deleted libraries people had paid for. The parenthetical in _useless when the company closes its doors_ does the whole argument's work. - ## Smaller calls - **Capture age and EXIF location painted as text.** White on a black stroke, written _after_ dithering, so the labels stay sharp on the 6-colour palette. -- **CLI flags for the awkward photos.** `--album`, `--people`, `-o 90` (portrait), and `--saturation`/`--contrast`/`--gamma` are flags on the cron command. The defaults are tuned for the average photo; the flags exist for the few that aren't. -- **`flock` around the render.** A slow refresh can't overlap the next 15-minute tick. -- **Wifi power-save reconnect job.** The Pi Zero 2W's wifi drops if power-save kicks in. A separate `wifi-check.sh` every five minutes brings it back. - **Swap masked, journald volatile.** The SD card is the most likely thing to die on this build. Don't write to it unless you have to. +- **Wifi power-save reconnect job.** The Pi Zero 2W's wifi drops if power-save kicks in. A separate `wifi-check.sh` every five minutes brings it back. ## What I'd change diff --git a/src/content/posts/graph-editor-javafx-simulation-input.md b/src/content/posts/graph-editor-javafx-simulation-input.md index 38164d5..06a67c0 100644 --- a/src/content/posts/graph-editor-javafx-simulation-input.md +++ b/src/content/posts/graph-editor-javafx-simulation-input.md @@ -14,8 +14,6 @@ audience: technical links: [] --- -The input-side companion to the [cooling system sim](/articles/nuclear-cooling-simulation/). A JavaFX desktop tool where you'd lay out the plant as a graph, edit each element's parameters in a side panel, export JSON, or upload directly to the simulator backend. +Non-technical event organisers needed to rewire a cooling plant in real time without me hovering. That was the brief, and it ruled out every interface I'd have enjoyed writing. The [cooling system sim](/articles/nuclear-cooling-simulation/) was only as useful as the tool that fed it, so in late 2018 I built a JavaFX desktop editor: lay out the plant as a graph, edit each element's parameters in a side panel, export JSON, or upload straight to the backend. -It was small but it punched above its weight at the actual event. The simulator's value depended entirely on the organisers being able to change the plant without me sitting next to them. The editor was what made that possible. - -If I built it again I'd skip JavaFX and put the editor in the browser, the same place the monitoring clients lived. One install fewer for everyone. +Small tool, and the whole event hinged on it. If I built it again I'd skip JavaFX and put the editor in the browser next to the monitoring clients. One install fewer for everyone, and one fewer reason for someone to call me over. diff --git a/src/content/posts/greatai-ai-deployment-api.md b/src/content/posts/greatai-ai-deployment-api.md index b64560c..8f97d47 100644 --- a/src/content/posts/greatai-ai-deployment-api.md +++ b/src/content/posts/greatai-ai-deployment-api.md @@ -28,11 +28,7 @@ media: caption: A working GreatAI service is about ten lines on top of a plain prediction function. --- -**The short version:** - -- I surveyed working data scientists and engineers on a catalogue of 33 ML deployment habits. The honest finding: people skip habits not because they disagree, but because adopting one means writing five lines of glue, and they have a deadline. -- That's a design problem, not a discipline problem. The thesis built a Python framework around one bet: put the deployment behaviour next to the prediction function with a single decorator, and the right thing becomes the path of least resistance. -- Pip-installable as `great-ai`. Worth a look mainly for the survey methodology behind it. +By the end of 2021 I had stopped believing the people skipping ML deployment best practices were the problem. They knew the list. They agreed with the list. They had a deadline, and every item on the list cost five lines of glue. My MSc thesis turned that into the actual research question: not "what should engineers do" but "what API shape makes doing the right thing cheaper than not." The framework that fell out, `great-ai`, is a decorator on a plain Python function. The thesis behind it is the part worth reading. ## The thing nobody wants to admit diff --git a/src/content/posts/life-towers-immutable-tries.md b/src/content/posts/life-towers-immutable-tries.md index 59d3492..60c032a 100644 --- a/src/content/posts/life-towers-immutable-tries.md +++ b/src/content/posts/life-towers-immutable-tries.md @@ -23,11 +23,7 @@ media: caption: The interface was a 2019 weekend experiment. The trie underneath aged better. --- -**The short version:** - -- Built as a personal task tracker with a tower metaphor. The UI was the fun bit; the sync model is the part I'd still write the same way today. -- Clients and server hold the same data as an immutable trie. Comparing two versions is a walk over shared nodes. Sending the delta is sending only the nodes that aren't shared. -- No CRDT, no OT, no clever conflict resolution. The structure does the work, the protocol stays boring. +In August 2019 I wanted a goal tracker I'd actually open, on whichever device was nearest, without watching it disagree with itself. Nothing off the shelf fit, so I built one over a couple of weekends. The tower metaphor was the part friends saw; the part that aged well was the sync model that fell out of needing the same state in three places at once. ## The problem in one paragraph diff --git a/src/content/posts/lights-synchronized-to-music.md b/src/content/posts/lights-synchronized-to-music.md index 72c58fa..7894495 100644 --- a/src/content/posts/lights-synchronized-to-music.md +++ b/src/content/posts/lights-synchronized-to-music.md @@ -14,12 +14,10 @@ audience: technical links: [] --- -Spring 2016. I had a Raspberry Pi, a couple of 12V RGB LED strips someone had given me, a handful of MOSFETs from an electronics kit, and zero idea what I was doing. The plan was something like: play music, look at it, make the lights match. +Spring 2016. I had a Raspberry Pi, a couple of 12V RGB LED strips someone had given me, a handful of MOSFETs from an electronics kit, and zero idea what I was doing. I wired one of the MOSFETs backwards and it got hot enough to leave a small mark on the breadboard. I learned to read a datasheet, slowly, by needing one. This was the first thing I started and actually finished. -I got bands wrong first. I tried mapping raw audio amplitude to brightness, which made the lights pulse with anything: clipping, voice, fan noise. It produced a strobing mess that hurt to look at. Reading about Fourier transforms long enough to type `numpy.fft.fft(audio_chunk)` into a REPL was the moment the project started actually behaving like the thing I'd imagined. Bass-heavy frequency bins went to red; mids to green; highs to blue. Smoothing the output over a few frames stopped the seizure-inducing flicker. - -The MOSFETs took longer than they should have. I wired one backwards and it got hot enough to leave a small mark on the breadboard. I learned to read a datasheet, slowly, by needing one. +The plan was something like: play music, look at it, make the lights match. I got bands wrong first. Mapping raw audio amplitude to brightness made the lights pulse with anything (clipping, voice, fan noise), a strobing mess that hurt to look at. Reading about Fourier transforms long enough to type `numpy.fft.fft(audio_chunk)` into a REPL was the moment the project started actually behaving like the thing I'd imagined. Bass-heavy frequency bins went to red; mids to green; highs to blue. Smoothing the output over a few frames stopped the seizure-inducing flicker. The frontend was a vanilla web page on the same Pi: pick a track, tweak the band thresholds, see what changed. No framework. Just a `