{
  "schemaVersion": 1,
  "generatedAt": "2026-10-19T19:48:56.339Z",
  "site": {
    "name": "Devan Metz",
    "url": "https://devanmetz.com",
    "description": "Projects by Devan Metz, including Goop Troops, OpenFloat Archery, a DIY electric bike, and hand-painted 3D prints.",
    "summary": "I build browser games, hardware, software tools, and 3D-printed props. This site brings together my projects and posts about building them.",
    "about": [
      "I’m Devan, a student and maker. I build software and physical projects, from browser games and bow sensors to an electric bike and printed instruments.",
      "I made Goop Troops, built a belt-drive electric bike, and printed and painted props from Rust. I also built OpenFloat Archery, which pairs a sensor on the bow with a browser dashboard to review its movement.",
      "My interests include Blender, Python, Arduino and Zephyr, CNC, RC, e-bike projects, YouTube, and 3D printing and painting.",
      "I’m open to internships, collaboration, and commissions. A 3D-printing store may come later."
    ],
    "profiles": {
      "github": "https://github.com/DevanMetz",
      "x": "https://x.com/devanmetz"
    },
    "contactUrl": "https://devanmetz.com/#contact",
    "rssUrl": "https://devanmetz.com/rss.xml"
  },
  "projects": [
    {
      "id": "openfloat-archery",
      "title": "OpenFloat Archery",
      "url": "https://devanmetz.com/portfolio/openfloat-archery/",
      "markdownUrl": "https://devanmetz.com/portfolio/openfloat-archery/index.md",
      "collection": "portfolio",
      "description": "A tiny bow-mounted sensor and a browser dashboard for seeing what happens before, during, and after a shot.",
      "summary": "I built OpenFloat to record how a bow moves before, during, and after an arrow is released. A small sensor sends that movement to a browser so I can replay shots and compare them. It tracks bow movement, not where the arrow lands.",
      "date": null,
      "updated": "2026-10-17",
      "tags": [
        "Bluetooth LE",
        "Zephyr",
        "JavaScript",
        "Signal processing"
      ],
      "projectDate": {
        "start": "2026-06"
      },
      "category": "Embedded hardware",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Firmware, browser app, and sensor integration"
        },
        {
          "label": "Hardware",
          "value": "XIAO nRF54L15 Sense + motion sensor"
        },
        {
          "label": "Data",
          "value": "Local shot history and optional sync"
        }
      ],
      "actions": [
        {
          "label": "Try the dashboard",
          "url": "https://openfloatarchery.com"
        },
        {
          "label": "Source & build guide",
          "url": "https://github.com/DevanMetz/Open-Float-Archery"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/openfloat.CyN-DkoE_Z1s64OT.webp",
        "alt": "OpenFloat dashboard displaying an archery target with colored hold, release, and follow-through traces.",
        "caption": "Shot review in OpenFloat, with a phase-colored trace and replay controls.",
        "width": 1200,
        "height": 737
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/openfloat-shot-trace.B8qEH6X-_wM3Ka.webp",
          "alt": "Devan at full draw with a compound bow in a grassy field, with a phone propped on a wooden post beside him, shown above the OpenFloat Trace Review screen where a green hold trace clusters near the center of the target.",
          "caption": "A real shot outdoors: video of the draw above, and the Pin Float trace building in Trace Review below. Still frame from the demo clip in the OpenFloat repository.",
          "width": 1200,
          "height": 2133
        },
        {
          "url": "https://devanmetz.com/_astro/openfloat-steady.ClxgCux6_ZeJlOb.webp",
          "alt": "OpenFloat Steady Aim demo showing a green movement trace, a countdown, and a bow tilt reading beside hold-duration controls.",
          "caption": "Steady Aim in demo mode, with synthetic motion rather than a recorded shot.",
          "width": 1200,
          "height": 737
        },
        {
          "url": "https://devanmetz.com/_astro/openfloat-dashboard-live.BZ4EyunA_Z187zng.webp",
          "alt": "OpenFloat Shot Trainer dashboard in demo mode, showing a 3D bow orientation view, a bow cant level reading of -1.8 degrees with a Zero Calibration button, and a shot sequence trace over an archery target.",
          "caption": "The main dashboard in demo mode, with bow orientation, a cant level for sideways tilt, calibration controls, and the live shot trace.",
          "width": 1200,
          "height": 737
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "nrf54l15-support",
          "title": "nRF54L15 Support",
          "url": "https://devanmetz.com/portfolio/nrf54l15-support/",
          "markdownUrl": "https://devanmetz.com/portfolio/nrf54l15-support/index.md"
        },
        {
          "id": "ws2812-matrix-xiao",
          "title": "6×10 WS2812 Matrix for XIAO nRF54L15",
          "url": "https://devanmetz.com/portfolio/ws2812-matrix-xiao/",
          "markdownUrl": "https://devanmetz.com/portfolio/ws2812-matrix-xiao/index.md"
        },
        {
          "id": "colmi-r12-zwift",
          "title": "A Smart Ring for Zwift",
          "url": "https://devanmetz.com/portfolio/colmi-r12-zwift/",
          "markdownUrl": "https://devanmetz.com/portfolio/colmi-r12-zwift/index.md"
        }
      ],
      "contentMarkdown": "## Problem \n\nThe target shows where an arrow landed. I wanted to see what the bow did on the way there. OpenFloat records motion before, during, and after release with a small bow-mounted sensor, then turns each shot into a trace I can replay and compare in a browser.\n\n### Try it without hardware \n\n[Open the dashboard](<https://openfloatarchery.com/>) and select a recorded demo shot. The live demo can also generate synthetic motion.\n\nWith the sensor connected, the same interface captures real shots and keeps them with a practice session. Float Score describes movement of the bow; it does not measure where the arrow hit.\n\n## Constraints \n\nI need the trace to describe bow movement without implying that it measures arrow impact. The sensor also needs to preserve shots when the browser disconnects, and the browser connection depends on Web Bluetooth support and permission.\n\n## Hardware and firmware \n\nThe sensor uses a Seeed XIAO nRF54L15 Sense running firmware (the software on the sensor) built with Zephyr, an embedded operating system, and the nRF Connect SDK (NCS), Nordic’s development toolkit.\n\nIts LSM6DS3TR-C motion sensor is read through an interrupt-driven FIFO: a first-in, first-out buffer that holds readings in order and signals when they are ready. The firmware samples at 3,332 Hz (readings per second) and averages those readings into a roughly 1,110 Hz Bluetooth stream.\n\nThe firmware estimates orientation with a Madgwick filter, which combines motion readings to estimate the bow’s angle. Live data frames carry acceleration, orientation, and a microphone-envelope measurement, a measure of sound level over time.\n\nShot detection looks for a sudden motion above an impulse threshold, checked against recoil (the bow’s kick at release), then captures the motion around the release.\n\nThe sensor keeps a lifetime shot count and the newest 100 shot records in nonvolatile memory, which keeps data when power is off. Buffered traces can be replayed when the browser reconnects.\n\n## Iterations \n\nThe current design combines live review with buffered shot recovery. I want to document the steps that led here without treating the current feature list as a test history.\n\n\\[TODO: Devan — describe the first mounting and firmware revisions, what failed during practice, and what changed.\\]\n\n## Shot review and training \n\nThe browser uses the sensor’s orientation estimate to drive a bubble level and a 3D bow view. Shot review separates hold, release, and follow-through with different colors.\n\n-   **Replay:** scrub through the motion leading into and following release.\n-   **Comparison:** inspect several traces together to look for repeatable movement.\n-   **Float Score:** summarize the trace with the project’s own 0–100 scoring system.\n-   **Steady Aim:** practice a hold with a live trace, countdown, and cant readout showing sideways bow tilt.\n-   **Sessions:** group shots into practice records and revisit them later.\n\nFloat Score summarizes bow movement. Arrow impact still needs to be checked at the target.\n\n## Browser connection and storage \n\nWeb Bluetooth needs a supported browser, a secure context (such as an HTTPS page), and a user action to start device selection. A connect button gives that permission request an explicit place in the workflow. Browser and operating-system support should be checked against the [Web Bluetooth compatibility notes](<https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API>).\n\nReceiving a shot and saving it are separate steps. OpenFloat acknowledges buffered shots (confirms receipt to the sensor) after the browser has saved them locally. Reconnection can then recover records that were captured while the browser was unavailable.\n\n### Keeping and backing up shots \n\nThe app uses IndexedDB, storage inside the browser, and can operate offline after loading as a progressive web app (a website that can work like an installed app). Backup and restore are available for local data. Keeping a session in one browser does not automatically back it up to another device; optional self-hosted sync is separate from the local workflow.\n\n## Getting started \n\n1.  Open the [hosted dashboard](<https://openfloatarchery.com/>) and review a demo capture.\n2.  Use the repository’s Quick Start for the supported sensor, firmware release, mounting, and first calibration.\n3.  Connect from the dashboard in a compatible browser.\n4.  Confirm the live orientation responds before recording a session.\n5.  Save a backup when the session is worth keeping.\n\nThe browser app uses native JavaScript modules, separate code files loaded directly by the browser. The repository also includes firmware build instructions, Bluetooth Low Energy (BLE) command documentation, host-side validation tools, and mechanical assets.\n\n## Build files and reference \n\nThe [OpenFloat repository](<https://github.com/DevanMetz/Open-Float-Archery>) has the hardware setup, firmware builds, BLE command reference, and mechanical files. OpenFloat is an experimental project; the feature-status list tracks what is implemented and what is still in progress.\n\n## Measured results and current limits \n\nThe implementation figures documented above are **3,332 Hz sensor sampling**, a **roughly 1,110 Hz BLE stream**, and storage for the **newest 100 shot records**. These describe the data pipeline and storage capacity; they do not establish end-to-end latency, battery runtime, or the number of field shots recorded.\n\n\\[TODO: Devan — verify sample rate and stream rate on hardware; include the measurement method.\\]\n\n\\[TODO: Devan — measure end-to-end latency and describe the test setup.\\]\n\n\\[TODO: Devan — measure battery life with the battery, firmware, and usage conditions identified.\\]\n\n\\[TODO: Devan — report total shots recorded in the field and distinguish those from the 100-record storage capacity.\\]\n\n## What’s next \n\nI want the next update to connect a real practice session to the trace and explain what I changed afterward. I’m keeping the field-testing story as a draft until I can supply that account.\n\n\\[TODO: Devan — choose the next hardware or firmware change based on field evidence.\\]"
    },
    {
      "id": "goop-troops",
      "title": "Goop Troops",
      "url": "https://devanmetz.com/portfolio/goop-troops/",
      "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md",
      "collection": "portfolio",
      "description": "A multiplayer strategy game where territory grows like an organism and one cut can break an opponent’s supply line.",
      "summary": "I built Goop Troops as a browser strategy game where every piece of territory needs a connected route. Cutting a narrow supply line can bring down a much larger army. You can learn against computer-controlled opponents, join public matches with up to eight players, or play in private rooms.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "TypeScript",
        "React",
        "Multiplayer",
        "Realtime simulation"
      ],
      "projectDate": {
        "start": "2026-02"
      },
      "category": "Game development",
      "group": "games",
      "status": "Browser game",
      "facts": [
        {
          "label": "My work",
          "value": "Game systems, web client, and multiplayer service"
        },
        {
          "label": "Platform",
          "value": "Browser · desktop and touch controls"
        },
        {
          "label": "Play",
          "value": "Up to 8 players · bots · ranked 1v1"
        }
      ],
      "actions": [
        {
          "label": "Play Goop Troops",
          "url": "https://gooptroops.org"
        },
        {
          "label": "Find it on itch.io",
          "url": "https://dgm12385.itch.io/goop-troops"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/goop-troops.sa_rb1Bh_D9Hx.webp",
        "alt": "Colorful branching armies competing across the Goop Troops grid.",
        "caption": "A gameplay capture with several bot-controlled armies sharing the battlefield.",
        "width": 1200,
        "height": 630
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/goop-tutorial.BZe1_zW2_1w5sFX.webp",
          "alt": "Goop Troops tutorial showing three connected green tiles and the instruction that every tile needs an unbroken path to its starting point.",
          "caption": "The tutorial introduces the connected-territory rule before a multiplayer match.",
          "width": 1200,
          "height": 499
        }
      ],
      "media": [],
      "project": null,
      "stories": [
        {
          "id": "goop-troops-supply-lines",
          "title": "A strategy game about the space between",
          "url": "https://devanmetz.com/blog/goop-troops-supply-lines/",
          "markdownUrl": "https://devanmetz.com/blog/goop-troops-supply-lines/index.md"
        }
      ],
      "related": [
        {
          "id": "poker-with-friends",
          "title": "Poker With Your Friends",
          "url": "https://devanmetz.com/portfolio/poker-with-friends/",
          "markdownUrl": "https://devanmetz.com/portfolio/poker-with-friends/index.md"
        },
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        },
        {
          "id": "market-atlas",
          "title": "Market Atlas",
          "url": "https://devanmetz.com/portfolio/market-atlas/",
          "markdownUrl": "https://devanmetz.com/portfolio/market-atlas/index.md"
        }
      ],
      "contentMarkdown": "## The hook \n\nIn [Goop Troops](<https://gooptroops.org/>), one small cut can undo a much larger army. Territory grows across a grid, but every part of it depends on a connected route. A narrow strip behind the front line can matter more than the size of the front itself.\n\nI built the game, its browser controls, and the multiplayer service around that rule. You can [play a bot match](<https://gooptroops.org/>) to learn it, then join a public game with up to eight players or set up a private room. The gameplay image above shows several armies pressing into the same board.\n\n## The supply-line rule \n\nA connected empire has both reach and dependencies. A large front may look strong while relying on a small piece of territory behind it. Cutting that connection can collapse the position more effectively than attacking the largest group directly.\n\nThat rule makes the empty space between armies useful information. A player has to read routes, chokepoints (narrow passages that are easy to block), and access to gold as well as the territory immediately in front of them. Expansion creates options, but it also creates more connections to defend.\n\nThe tutorial introduces this relationship before the player has to manage the full pace of a multiplayer match.\n\n## Ways to play \n\nThe browser release supports several entry points:\n\n| Mode | What it is for |\n| --- | --- |\n| Guest play | Enter a game without first creating an account |\n| AI opponents | Learn the rules and practice against computer-controlled armies |\n| Public matches | Share a battlefield with up to eight players |\n| Private rooms and teams | Arrange a game with specific people |\n| Ranked 1v1 | Play head-to-head through matchmaking |\n\nDesktop and touch controls make the same game accessible from different devices. The public version runs in a browser without a separate download.\n\n## Architecture \n\nI built the browser client (the part running on the player’s device) with React and TypeScript. It shares game-simulation code, which calculates how the battlefield changes, with an authoritative Node.js realtime service, which processes the match as it happens.\n\n“Authoritative” means the server has the final say on the match state, including the interaction between human commands and bot decisions.\n\nSharing simulation code keeps the rules close to the interface that explains them. Keeping the final match state on the server gives participants a common outcome when competing actions arrive.\n\n### Between matches \n\nPersistence, or saving data beyond the current session, supports the parts of the game that extend beyond a single match. Onboarding, room selection, matchmaking, and account-backed play are separate concerns from drawing and advancing the battlefield.\n\n## Project structure and editions \n\nThe main web project contains the client and shared simulation. A separate realtime service handles multiplayer and bots. The repository also holds database resources, production deployment tools, a Windows desktop prototype, and a native Roblox edition.\n\nThe browser game is the public release. The Windows and Roblox editions are separate development projects.\n\n## Play and development notes \n\nStart at [Goop Troops](<https://gooptroops.org/>), use the tutorial, and try a bot match to get a feel for supply lines. Private rooms are available when you want to play with friends. The [itch.io page](<https://dgm12385.itch.io/goop-troops>) provides another public home for the game.\n\nI built the simulation, web client, realtime service, interface, and release tools. The source code remains private."
    },
    {
      "id": "diy-electric-bike",
      "title": "DIY Electric Bike",
      "url": "https://devanmetz.com/portfolio/diy-electric-bike/",
      "markdownUrl": "https://devanmetz.com/portfolio/diy-electric-bike/index.md",
      "collection": "portfolio",
      "description": "A bicycle conversion combining a custom belt drive, CAD-designed mounts, and an ESP32 dashboard.",
      "summary": "I converted a bicycle with a custom belt drive, printed mounts, and a dashboard that shows data from the motor controller. The build brings together mechanical design and electronics so I can see battery and riding data on the bike. The bicycle is assembled, while the dashboard software remains a prototype.",
      "date": null,
      "updated": "2026-10-15",
      "tags": [
        "Fusion 360",
        "3D printing",
        "ESP32",
        "C++"
      ],
      "projectDate": {
        "start": "2022"
      },
      "category": "Mechanical & electrical",
      "group": "hardware",
      "status": "Physical build",
      "facts": [
        {
          "label": "My work",
          "value": "Mechanical integration, printed mounts, and dashboard"
        },
        {
          "label": "Electronics",
          "value": "ESP32 · VESC UART · TFT display"
        },
        {
          "label": "Files",
          "value": "Fusion 360 assemblies, STL parts, and C++"
        }
      ],
      "actions": [
        {
          "label": "Explore the build files",
          "url": "https://github.com/DevanMetz/DIY-Mid-Drive-EBike"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/ebike.2RbJRJf1_1fHioh.webp",
        "alt": "A blue bicycle fitted with a custom motor mount, large rear pulley, belt drive, and electronics.",
        "caption": "The assembled bicycle conversion, showing the motor, belt drive, and frame-mounted electronics.",
        "width": 1200,
        "height": 904
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/ebike-drive-closeup.zp_ATKIk_ZD05Ob.webp",
          "alt": "Close-up of the blue bicycle frame, where a motor on a tan mount plate drives a black toothed belt, with a frame bag, yellow connectors, and wiring running down to a black electronics case.",
          "caption": "A closer look at the motor mount, belt drive, and frame-mounted wiring.",
          "width": 1200,
          "height": 904
        },
        {
          "url": "https://devanmetz.com/_astro/ebike-rear-pulley.f-gFyXDK_8nqUn.webp",
          "alt": "Black printed pulley mounted beside the rear wheel of a blue bicycle, with a toothed belt running around it.",
          "caption": "The belt and large printed rear pulley on the assembled bicycle.",
          "width": 1200,
          "height": 903
        },
        {
          "url": "https://devanmetz.com/_astro/ebike-cad.C1bIeNdi_HToTv.webp",
          "alt": "CAD assembly of the bicycle conversion and drive components.",
          "caption": "The mechanical assembly in CAD, before everything has to fit on the real frame.",
          "width": 1200,
          "height": 828
        },
        {
          "url": "https://devanmetz.com/_astro/ebike-dashboard.C2wAOlim_1ojST2.webp",
          "alt": "ESP32-based display showing electric bicycle telemetry.",
          "caption": "The ESP32 dashboard prototype displays data from the motor controller.",
          "width": 1200,
          "height": 601
        }
      ],
      "media": [],
      "project": null,
      "stories": [
        {
          "id": "ebike-from-cad-to-dashboard",
          "title": "One bike, three kinds of making",
          "url": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/",
          "markdownUrl": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/index.md"
        }
      ],
      "related": [
        {
          "id": "printed-violin",
          "title": "3D-Printed Rainbow Violin",
          "url": "https://devanmetz.com/portfolio/printed-violin/",
          "markdownUrl": "https://devanmetz.com/portfolio/printed-violin/index.md"
        },
        {
          "id": "workshop-builds",
          "title": "From the Workbench",
          "url": "https://devanmetz.com/portfolio/workshop-builds/",
          "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md"
        },
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        }
      ],
      "contentMarkdown": "## What I built \n\nThe bicycle in the photo is the assembled result of three connected pieces of work: fitting a belt-driven motor to the frame, making the mounts and pulley, and giving the electronics a readable display. I designed the mechanical assembly in Fusion 360, a computer-aided design (CAD) tool, and printed parts to fit the bike.\n\nAn ESP32, a small programmable computer, runs the dashboard. It reads the VESC motor controller over UART, a serial data connection, and shows battery voltage, power, speed, and distance.\n\nThe bike is assembled; the dashboard is still prototype firmware (the software running on the ESP32). The [public build files](<https://github.com/DevanMetz/DIY-Mid-Drive-EBike>) include the editable CAD, printable parts, code, and build photos.\n\n## Mechanical assembly \n\nI used Fusion 360 to fit the motor, pulleys, and mounts around the bicycle frame and rear wheel. A belt connects the motor to a large rear pulley, with the electronics mounted on the frame.\n\n### Printable parts \n\nThe printable parts include:\n\n-   The motor mount.\n-   The rear pulley.\n-   Two seat-tube clamp halves.\n\nThe CAD files also include a 180-tooth pulley revision. The editable assembly is useful for checking clearances or adapting the mounts to another frame.\n\n## Electronics and live readings \n\nAn ESP32 communicates with the VESC motor controller over UART. The display sketch (the program running on the ESP32) uses three code libraries:\n\n-   **VescUart** reads controller data.\n-   **TFT\\_eSPI** drives the TFT, a thin-film-transistor display.\n-   **FastLED** controls addressable lighting, where each light can be set separately.\n\n### Measurements and lighting \n\nThe display presents battery voltage, estimated electrical power, speed, distance, and amp-hours (a measure of electric charge used).\n\nPower is calculated from input voltage and current. Speed and distance are calculated from controller RPM (revolutions per minute) and tachometer readings that track rotation using constants in the sketch.\n\nThe same firmware cycles colors on an eleven-pixel WS2812 LED strip while polling telemetry (regularly reading controller data) and updating the dashboard.\n\n## Dashboard implementation notes \n\nThe sketch communicates with the controller at 115,200 baud, the serial connection’s signaling rate. It updates individual regions of the TFT as the readings change.\n\n### Checking the conversions \n\nThe telemetry conversions are specific to this build. Motor pole count (the number of magnetic poles), wheel diameter, and pulley ratio (the relative sizes of the drive pulleys) affect the meaning of the reported RPM and distance.\n\nThe code includes hard-coded conversion factors (fixed numbers in the program), and some nearby comments describe different example values. Those constants need to be checked against the actual drivetrain before reusing the sketch.\n\n## Build files \n\n| Folder | Contents |\n| --- | --- |\n| `CAD/Fusion360` | Editable components and the full bicycle assembly |\n| `CAD/STL` | Motor mount, rear pulley, and seat-tube clamp exports in STL, a 3D-printable mesh format |\n| `Code/VESCOM` | Dashboard sketch and display assets |\n| `Pictures` | Assembled bike, CAD assembly, pulley, and dashboard photographs |\n\nThe CAD, printable parts, dashboard code, and build photos are in the [public repository](<https://github.com/DevanMetz/DIY-Mid-Drive-EBike>).\n\n## Status and reuse \n\nThe bicycle is assembled, and the dashboard remains prototype firmware. To adapt the build, start with the editable CAD and check the frame fit, belt alignment, mount strength, and telemetry constants for your components."
    },
    {
      "id": "printed-violin",
      "title": "3D-Printed Rainbow Violin",
      "url": "https://devanmetz.com/portfolio/printed-violin/",
      "markdownUrl": "https://devanmetz.com/portfolio/printed-violin/index.md",
      "collection": "portfolio",
      "description": "A rainbow Hovalin printed on a CR-10, assembled with guitar tuners, and put to the test—from plucked strings to the first days of bow practice.",
      "summary": "I printed and assembled a rainbow Hovalin violin, using a one-piece body, printed neck and bridge, and guitar tuners. I tested it with acoustic guitar strings while waiting for violin strings and a bow, then filmed my early bow practice. The videos show how the printed instrument sounds, both plucked and played with a bow.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "3D printing",
        "Hovalin",
        "Music",
        "Video"
      ],
      "projectDate": {
        "start": "2021-01"
      },
      "category": "Making & music",
      "group": "hardware",
      "status": "Printed & played",
      "facts": [
        {
          "label": "My work",
          "value": "Printing, assembly, sound tests, and video"
        },
        {
          "label": "Printer",
          "value": "Creality CR-10"
        },
        {
          "label": "Print time",
          "value": "About 20 hours"
        }
      ],
      "actions": [
        {
          "label": "Original Hovalin design",
          "url": "https://www.hovalabs.com/work/hovalin"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/printed-violin-playing.CmsxwXA4_1UJjwy.webp",
        "alt": "Playing a rainbow-colored printed violin fitted with a gray neck, four strings, and guitar tuning machines.",
        "caption": "The assembled violin during its first plucked-string sound test. Frame from my original video.",
        "width": 1200,
        "height": 675
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/printed-violin-printing.Cma7Jifg_1moy8H.webp",
          "alt": "A 3D printer working on the upright rainbow violin body, with its sound holes and raised tailpiece visible.",
          "caption": "The body being printed in one piece. Frame from the original short build video.",
          "width": 1200,
          "height": 2133
        },
        {
          "url": "https://devanmetz.com/_astro/printed-violin-body.CCEPpJXk_1nnLBm.webp",
          "alt": "The completed rainbow violin body held upright before fitting the neck, bridge, and strings.",
          "caption": "The unstrung body, showing the color bands, printed layers, sound holes, and neck opening.",
          "width": 1200,
          "height": 2133
        },
        {
          "url": "https://devanmetz.com/_astro/printed-violin-guitar-strings.DdswxFCZ_Z1Db7zc.webp",
          "alt": "The rainbow printed violin lying on a white table, fitted with a gray neck and bridge and acoustic guitar strings, under the title text “3D Printed Violin with Guitar Strings.”",
          "caption": "My earlier title thumbnail for the build, showing the violin strung with acoustic guitar strings before the violin strings and bow arrived.",
          "width": 1200,
          "height": 675
        }
      ],
      "media": [
        {
          "kind": "video",
          "title": "First sound test of the printed violin with guitar strings",
          "poster": "https://devanmetz.com/media/printed-violin/first-sound-test.webp",
          "sources": [
            {
              "url": "https://devanmetz.com/media/printed-violin/first-sound-test.webm",
              "type": "video/webm"
            },
            {
              "url": "https://devanmetz.com/media/printed-violin/first-sound-test.mp4",
              "type": "video/mp4"
            }
          ],
          "tracks": [
            {
              "url": "https://devanmetz.com/media/printed-violin/first-sound-test.vtt",
              "kind": "captions",
              "language": "en",
              "label": "English"
            }
          ]
        },
        {
          "kind": "video",
          "title": "Early bowed performance on the printed rainbow violin",
          "poster": "https://devanmetz.com/media/printed-violin/bowed-playing.webp",
          "sources": [
            {
              "url": "https://devanmetz.com/media/printed-violin/bowed-playing.webm",
              "type": "video/webm"
            },
            {
              "url": "https://devanmetz.com/media/printed-violin/bowed-playing.mp4",
              "type": "video/mp4"
            }
          ],
          "tracks": [
            {
              "url": "https://devanmetz.com/media/printed-violin/bowed-playing.vtt",
              "kind": "captions",
              "language": "en",
              "label": "English"
            }
          ]
        }
      ],
      "project": null,
      "stories": [
        {
          "id": "printed-violin-first-sound",
          "title": "The first sound from my printed violin",
          "url": "https://devanmetz.com/blog/printed-violin-first-sound/",
          "markdownUrl": "https://devanmetz.com/blog/printed-violin-first-sound/index.md"
        }
      ],
      "related": [
        {
          "id": "workshop-builds",
          "title": "From the Workbench",
          "url": "https://devanmetz.com/portfolio/workshop-builds/",
          "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md"
        },
        {
          "id": "diy-electric-bike",
          "title": "DIY Electric Bike",
          "url": "https://devanmetz.com/portfolio/diy-electric-bike/",
          "markdownUrl": "https://devanmetz.com/portfolio/diy-electric-bike/index.md"
        },
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI found out I could 3D print a violin, so I made one. This is my rainbow build of the **Hovalin**, a 3D-printable violin design by [Hova Labs](<https://www.hovalabs.com/work/hovalin>).\n\nI started with an improvised guitar-string sound test while waiting for violin strings and a bow (the tool drawn across strings to make them sound). The videos below show that first test and my early attempts at playing with a bow.\n\n## Printing and assembly \n\n### Printed parts \n\nI printed the entire body in one piece on a **Creality CR-10**, a 3D printer, with a print time of about **20 hours**. The neck and bridge (the piece that supports the strings above the body) were also printed. Guitar tuning machines, or tuners, adjust string tension to change pitch.\n\nThe body prints upright, standing vertically as the printer builds it layer by layer. Rainbow bands run across those layers.\n\n### Assembly and tuning \n\nAfter printing, I fitted the neck, bridge, tuners, and strings. The gray neck and bridge contrast with the colorful body. I tuned it E–A–D–G, the notes of its four strings.\n\n## First sound test \n\nBefore my violin strings and bow arrived, I fitted **acoustic guitar strings** and tried plucking it. The first recording starts with a look around the body and hardware, followed by the playing test. I change notes by pressing the strings against the fingerboard, the surface along the neck.\n\n   \n\n**First sound test of the printed violin with guitar strings**\n\n- [Video (video/webm)](<https://devanmetz.com/media/printed-violin/first-sound-test.webm>)\n- [Video (video/mp4)](<https://devanmetz.com/media/printed-violin/first-sound-test.mp4>)\n- [captions: English](<https://devanmetz.com/media/printed-violin/first-sound-test.vtt>)\n- [Preview image](<https://devanmetz.com/media/printed-violin/first-sound-test.webp>)\n\nThe original 1 minute 16 second sound-test video, with the build introduction and plucked-string playing. English captions are available.\n\n**Video transcript**\n\nThe body was completely 3D printed in one piece on my Creality CR-10. The neck and bridge were also printed. The violin uses guitar tuners and is tuned E–A–D–G. I am using acoustic guitar strings until my violin strings and bow arrive.\n\nThe remainder of the video shows me holding the violin across my lap and plucking the strings while changing notes on the fingerboard.\n\n## Learning with a bow \n\nOnce I had a bow, I started learning to play. This clip is from about **three days of practice**—an early attempt, but a chance to hear the printed violin played with a bow.\n\n   \n\n**Early bowed performance on the printed rainbow violin**\n\n- [Video (video/webm)](<https://devanmetz.com/media/printed-violin/bowed-playing.webm>)\n- [Video (video/mp4)](<https://devanmetz.com/media/printed-violin/bowed-playing.mp4>)\n- [captions: English](<https://devanmetz.com/media/printed-violin/bowed-playing.vtt>)\n- [Preview image](<https://devanmetz.com/media/printed-violin/bowed-playing.webp>)\n\nA 25-second excerpt from my original short video: early bow practice on the assembled violin.\n\nIn the clip, I hold the instrument at my shoulder, draw the bow across the strings, and change notes with my left hand. There is no spoken narration in this excerpt.\n\n## Design and build references \n\n**Hova Labs created the Hovalin design.** I printed and assembled this one, learned to play it, and filmed the tests. Their [project page](<https://www.hovalabs.com/work/hovalin>) has more about the instrument and its printable files.\n\nOther small physical builds, including the arcade controller and soldering fan, remain in [From the Workbench](<https://devanmetz.com/portfolio/workshop-builds/>)."
    },
    {
      "id": "solar-trail-camera",
      "title": "Solar Cellular Trail Camera",
      "url": "https://devanmetz.com/portfolio/solar-trail-camera/",
      "markdownUrl": "https://devanmetz.com/portfolio/solar-trail-camera/index.md",
      "collection": "portfolio",
      "description": "A wildlife-camera engineering prototype spanning low-power firmware, a private gallery, and a modular printable enclosure.",
      "summary": "I designed a solar cellular wildlife-camera prototype to save original photos locally and send smaller previews to a private gallery. The project includes device software, a gallery, power calculations, and a printable enclosure. The software and enclosure design have been checked, but a complete physical camera has not yet been assembled or field-tested.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "ESP32-S3",
        "LTE-M",
        "Python",
        "FastAPI",
        "CAD"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Embedded hardware",
      "group": "hardware",
      "status": "Engineering prototype",
      "facts": [
        {
          "label": "My work",
          "value": "Firmware, gallery, simulator, and enclosure CAD"
        },
        {
          "label": "Platform",
          "value": "ESP32-S3 + SIM7080G LTE-M"
        },
        {
          "label": "Validation",
          "value": "Software and CAD checked; physical build pending"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/trail-camera.B2Xii-at_Z1VtDQK.webp",
        "alt": "CAD render of the modular camera enclosure with separate front openings and mounting features.",
        "caption": "Revision A enclosure geometry. This is a CAD render, not a photograph of an assembled camera.",
        "width": 1200,
        "height": 800
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/trail-camera-exploded.p_06HPXP_1SJoLO.webp",
          "alt": "Exploded CAD assembly showing the enclosure, internal carrier, battery access, and mounts.",
          "caption": "The service layout separates the enclosure, electronics carrier, battery access, and mounting parts.",
          "width": 1200,
          "height": 800
        },
        {
          "url": "https://devanmetz.com/_astro/trail-camera-solar-mount.7tequbP5_2oHgPi.webp",
          "alt": "CAD render of a solar mount with a green slotted upper plate, two central supports, and a dark slotted base.",
          "caption": "Revision A solar-mount geometry, shown as a CAD render rather than an assembled or tested mount.",
          "width": 1200,
          "height": 900
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "workshop-builds",
          "title": "From the Workbench",
          "url": "https://devanmetz.com/portfolio/workshop-builds/",
          "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md"
        },
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        },
        {
          "id": "printed-led-sculptures",
          "title": "Printed LED Sculptures",
          "url": "https://devanmetz.com/portfolio/printed-led-sculptures/",
          "markdownUrl": "https://devanmetz.com/portfolio/printed-led-sculptures/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed this solar cellular wildlife-camera prototype around occasional motion events and limited energy. Motion should wake the camera, trigger a photograph, preserve the original locally, and send a smaller preview to a private gallery.\n\nThe system spans embedded firmware (software running on the camera’s electronics), a server, a browser interface, power calculations, and a printable enclosure.\n\nRevision A is an engineering prototype: the software and computer-aided design (CAD) have been exercised, but a complete physical camera has not yet been assembled or field-tested.\n\n## Capture and upload lifecycle \n\nThe firmware targets the LilyGO T-SIM7080G-S3 PMU (power-management unit) board, combining an ESP32-S3 processor with a SIM7080G cellular modem, which connects to the mobile network.\n\n1.  A motion event wakes the device.\n2.  The camera captures a JPEG image and writes the original and preview to an SD memory card.\n3.  The preview enters an upload queue, a list of images waiting to be sent.\n4.  The device attempts an HTTPS upload (an encrypted web transfer) and checks the server’s receipt.\n5.  Failed transfers remain queued for retry with backoff (waiting between attempts).\n6.  Battery-aware scheduling and shutdown of unused power domains (groups of powered components) reduce activity between events.\n\nThe local original is retained after upload. Cellular coverage should affect when the gallery receives an image, rather than whether the image survives at all.\n\n## Gallery and backend \n\nThe backend (server software) uses FastAPI, a web framework, and SQLite, a database. It stores:\n\n-   Per-camera image histories.\n-   Telemetry (device status readings).\n-   Configuration versions (revisions of the camera settings).\n-   Requests for full-resolution originals.\n\nPer-camera credentials separate devices.\n\nThe Field Station browser gallery is designed for phones as well as larger screens.\n\n### Testing coverage gaps \n\nA simulator exercises the event path, including periods without coverage and later reconnection. It makes queue and gallery behavior testable before taking hardware outside.\n\nBrowser notifications currently depend on leaving the gallery open. Closed-browser mobile push is outside the implemented scope.\n\n## Power estimates and cellular connection \n\nThe power model calculates how event count, upload cost, solar input, and battery capacity affect runtime.\n\nIts default scenario estimates 0.95 Wh (watt-hours, a measure of energy) per day at 100 events. It estimates about eight days of reserve without sun, adjusted for reduced battery capacity in cold conditions.\n\n### What still needs measurement \n\nThose figures are model outputs, not measured runtime. Weak cellular signal, frequent activity, shade, and charging being inhibited in cold conditions can change the balance substantially. Actual sleep, capture, and transmission energy still need to be measured on the assembled hardware.\n\nLTE-M service (a cellular connection intended for low-power devices), compatible camera operation, charge-temperature protection (preventing charging at unsuitable temperatures), and night illumination also need controlled hardware checks on the workbench.\n\n## Enclosure and service access \n\nThe mechanical design separates ten printable parts. Editable STEP geometry (a format for exchanging CAD models) and STL exports (3D-printable mesh files) are accompanied by assembly views and geometry reports.\n\nThe enclosure provides:\n\n-   Separate camera and infrared openings (infrared is light outside the visible range).\n-   A replaceable electronics carrier.\n-   A battery hatch.\n-   Independent tree and adjustable solar mounts.\n\n### Fitting parts and checking weather sealing \n\nPurchased components appear as dimensioned envelopes (shapes representing their size) in the CAD; module-specific supports still need to be matched to the actual parts.\n\nThe images show the Revision A CAD design. Weather sealing still needs to be tested on a physical enclosure.\n\n## Validation and next milestone \n\nThe local backend and simulator run, the embedded firmware compiles (builds successfully into device software), and the CAD exports have been validated.\n\nThe next physical milestone is a complete bench path from motion to photograph to local storage to cellular upload to viewing.\n\nAfter that, I need to measure power use, check battery and charging behavior, and test the optics and enclosure outdoors. Field reliability and actual runtime are still open questions.\n\nThe source repository is private."
    },
    {
      "id": "nrf54l15-support",
      "title": "nRF54L15 Support",
      "url": "https://devanmetz.com/portfolio/nrf54l15-support/",
      "markdownUrl": "https://devanmetz.com/portfolio/nrf54l15-support/index.md",
      "collection": "portfolio",
      "description": "Portable C libraries and Zephyr drivers for the Nordic nRF54L15 with examples for the Seeed XIAO nRF54L15 Sense.",
      "summary": "I built nRF54L15 Support as reusable code for reading sensors, estimating orientation, and sending data from a small Nordic chip. It provides components for my OpenFloat Archery project, along with firmware examples and tests for other applications. This first release is experimental, and its programming interfaces may change.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "C",
        "Zephyr",
        "nRF Connect SDK",
        "Bluetooth LE",
        "Embedded"
      ],
      "projectDate": {
        "start": "2026-10"
      },
      "category": "Embedded libraries",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Portable C libraries, Zephyr adapters, firmware examples, and telemetry tools"
        },
        {
          "label": "Hardware",
          "value": "Seeed XIAO nRF54L15 Sense"
        },
        {
          "label": "License",
          "value": "Apache-2.0"
        }
      ],
      "actions": [
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/nrf54l15-support"
        },
        {
          "label": "v0.1.0 release",
          "url": "https://github.com/DevanMetz/nrf54l15-support/releases/tag/v0.1.0"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/nrf54l15-support-cover.CEpOn4jA_d5Gk0.webp",
        "alt": "Illustration of a schematic MCU board, motion axes, and Bluetooth symbol beside the nRF54L15 Support name; not a photo or screenshot.",
        "caption": "Original illustration of the library's MCU, IMU, and Bluetooth LE themes, not a hardware photo or screenshot.",
        "width": 1200,
        "height": 800
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "ws2812-matrix-xiao",
          "title": "6×10 WS2812 Matrix for XIAO nRF54L15",
          "url": "https://devanmetz.com/portfolio/ws2812-matrix-xiao/",
          "markdownUrl": "https://devanmetz.com/portfolio/ws2812-matrix-xiao/index.md"
        },
        {
          "id": "openfloat-archery",
          "title": "OpenFloat Archery",
          "url": "https://devanmetz.com/portfolio/openfloat-archery/",
          "markdownUrl": "https://devanmetz.com/portfolio/openfloat-archery/index.md"
        },
        {
          "id": "tab5-os",
          "title": "Tab5 OS",
          "url": "https://devanmetz.com/portfolio/tab5-os/",
          "markdownUrl": "https://devanmetz.com/portfolio/tab5-os/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built nRF54L15 Support as portable C libraries for the Nordic nRF54L15 system-on-chip (SoC), which combines a processor and hardware features on one chip. I included examples for the **Seeed XIAO nRF54L15 Sense**.\n\nZephyr hardware adapters connect the libraries to an embedded operating system. The portable code can run on a host computer or sit inside an nRF Connect SDK application, using Nordic’s development toolkit.\n\n### Memory and hardware calls \n\nThe library code allocates no heap memory: it does not request extra memory dynamically while running. Callers own the state and buffers (the stored working data), while the Bluetooth service is a singleton, meaning there is one shared instance. Zephyr and the SDK manage their own resources.\n\nHardware calls run in thread context, within scheduled tasks rather than interrupt handlers. They must be serialized: callers need to keep those calls from running at the same time.\n\n## Motion and hardware support \n\n### Orientation and motion readings \n\nAn inertial measurement unit (IMU) measures motion. My orientation code includes:\n\n-   A **Madgwick IMU filter**, which combines motion readings to estimate orientation.\n-   Quaternion normalization, which keeps the mathematical orientation representation valid.\n-   Euler conversion, which expresses orientation as three angles.\n-   Wrapped angles, which keep angle values within a repeating range.\n\nMotion support includes an upstream Zephyr IMU adapter and direct LSM6DS3TR-C I2C access (a wired chip-to-chip connection). Readings can use polling (checking for data), a FIFO (first-in, first-out buffer), INT1 notifications (signals on the sensor’s interrupt pin), and suspend/resume.\n\nStationary gyro calibration adjusts the rotation sensor while it is still, rejecting motion and noise. Records are versioned and CRC-protected, using a cyclic redundancy check to detect corrupted data. Optional Settings/ZMS persistence saves them through Zephyr’s settings and storage systems.\n\nScheduling and event helpers cover fractional sample deadlines (timing samples at intervals that are not whole clock ticks), missed slots, debounce (ignoring repeated signals from one event), impact candidates (possible impacts), and GPIO wake notifications (signals from input/output pins).\n\n### Other hardware adapters \n\n-   **SAADC:** analog-to-digital conversion, with raw and millivolt reads and calibration.\n-   **Switched battery divider:** a circuit for measuring battery voltage.\n-   **PWM:** pulse-width modulation, with control of duty (the on-time proportion) and pulse length.\n-   **Audio:** PDM (pulse-density modulation) capture into mono PCM16 (16-bit sound samples), capture cleanup, and PCM signal levels.\n-   **Temperature:** die-temperature reads from the microcontroller (MCU) and IMU chips.\n\n## Telemetry and OpenFloat \n\nTelemetry means sending measurements to another device. My transport support includes:\n\n-   Fixed-configuration **SPI**, a wired interface between chips.\n-   **UART**, a serial connection, with receive buffering and recovery when data is lost.\n-   **COBS/CRC packets:** consistently delimited messages with corruption checks.\n-   An **OpenFloat Bluetooth Low Energy (BLE) service** for wireless communication.\n\nOn the host computer, Python reference codecs (message encoders and decoders), serial telemetry validation, and a UART packet echo probe (which sends packets back) support testing and integration.\n\nMy related [OpenFloat Archery](<https://devanmetz.com/portfolio/openfloat-archery/>) project uses bow-mounted motion sensing and a browser dashboard. This library provides reusable orientation, hardware, and telemetry components for that work.\n\n## Examples and verification \n\nThe repository includes **15 firmware examples**, native CTest suites (tests run on a host computer), and Python tests. Native tests build the production C code against simulated hardware and operating-system interfaces; the Python packet suite compares 1,028 independent vectors with the built C encoder and decoder.\n\nThe reference firmware environment is **nRF Connect SDK v3.3.0**, targeting `xiao_nrf54l15/nrf54l15/cpuapp`. The [README](<https://github.com/DevanMetz/nrf54l15-support#readme>) links the example index, API contracts (the rules applications must follow), integration instructions, and hardware testing setup.\n\n### Hardware testing limits \n\nHardware validation results and remaining fixture requirements are recorded in the [verification record](<https://github.com/DevanMetz/nrf54l15-support/blob/main/docs/verification.md>). Those checks document the tested behavior; they do not establish absolute measurement accuracy.\n\n## Current release \n\n**v0.1.0 is an experimental first release**, published October 1, 2026. Public APIs (the programming interfaces applications call) may change before v1.0. The code is Apache-2.0 licensed; Nordic SDK dependencies retain their own licenses and are installed separately."
    },
    {
      "id": "ws2812-matrix-xiao",
      "title": "6×10 WS2812 Matrix for XIAO nRF54L15",
      "url": "https://devanmetz.com/portfolio/ws2812-matrix-xiao/",
      "markdownUrl": "https://devanmetz.com/portfolio/ws2812-matrix-xiao/index.md",
      "collection": "portfolio",
      "description": "Zephyr examples and docs for driving Seeed's 6×10 RGB WS2812 matrix from a XIAO nRF54L15 with a GPIO bit-bang driver.",
      "summary": "I documented how to drive Seeed's 6×10 RGB WS2812 matrix from a XIAO nRF54L15 in Zephyr. The repo includes a GPIO bit-bang driver, a demo gallery, pinout and pixel-mapping notes, and a wiring diagram, because the stock WS2812 SPI driver did not light this panel on that board.",
      "date": null,
      "updated": "2026-10-15",
      "tags": [
        "C",
        "Zephyr",
        "nRF Connect SDK",
        "WS2812",
        "Embedded"
      ],
      "projectDate": {
        "start": "2026-05"
      },
      "category": "Embedded libraries",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Zephyr bit-bang driver, demo gallery, hardware docs, and pixel-mapping notes"
        },
        {
          "label": "Hardware",
          "value": "Seeed 6×10 RGB matrix · XIAO nRF54L15"
        },
        {
          "label": "License",
          "value": "MIT"
        }
      ],
      "actions": [
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/6x10-RGB-WS2812-Matrix-for-XIAO-nRF54L15"
        },
        {
          "label": "Hardware notes",
          "url": "https://github.com/DevanMetz/6x10-RGB-WS2812-Matrix-for-XIAO-nRF54L15/blob/main/docs/hardware.md"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/ws2812-matrix-rainbow-demo.DhI3tARG_Z28ssl2.webp",
        "alt": "Close-up photo of a compact 6-by-10 RGB LED matrix on a dark PCB running a left-to-right rainbow gradient, with XIAO-style header pads along the top and bottom edges.",
        "caption": "The Seeed 6×10 RGB matrix running the rainbow demo from the Zephyr example on a XIAO nRF54L15.",
        "width": 1200,
        "height": 995
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/ws2812-matrix-wiring.BmCpjMLu_Z1rFF1W.webp",
          "alt": "Wiring diagram connecting a XIAO nRF54L15 5V, GND, and D0/A0 pins to a 6×10 RGB matrix VCC, GND, and DIN, noting that D0/A0 is P1.04.",
          "caption": "Wiring diagram from the repository: 5 V to VCC, common ground, and data on D0/A0 (P1.04).",
          "width": 1200,
          "height": 600
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "nrf54l15-support",
          "title": "nRF54L15 Support",
          "url": "https://devanmetz.com/portfolio/nrf54l15-support/",
          "markdownUrl": "https://devanmetz.com/portfolio/nrf54l15-support/index.md"
        },
        {
          "id": "openfloat-archery",
          "title": "OpenFloat Archery",
          "url": "https://devanmetz.com/portfolio/openfloat-archery/",
          "markdownUrl": "https://devanmetz.com/portfolio/openfloat-archery/index.md"
        },
        {
          "id": "tab5-os",
          "title": "Tab5 OS",
          "url": "https://devanmetz.com/portfolio/tab5-os/",
          "markdownUrl": "https://devanmetz.com/portfolio/tab5-os/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built community examples and documentation for driving Seeed’s 6×10 RGB (red, green, and blue) WS2812 LED matrix from a XIAO nRF54L15. The project fills a gap for Zephyr (an embedded operating system) and nRF Connect SDK (Nordic’s software development kit) users, with C code, wiring notes, and patterns for the 60 individually controlled lights.\n\n## Getting the panel to light \n\nThe stock `worldsemi,ws2812-spi` driver, which uses SPI (Serial Peripheral Interface) to generate the LED data signal, did not light this panel on this board. I built a GPIO (general-purpose input/output) bit-bang driver, which generates the signal by toggling a pin in software.\n\nThe data connection uses **D0/A0**, the XIAO header pin connected to **P1.04** on the nRF54L15. My hardware notes and wiring diagram record that connection alongside the panel’s power and ground pins.\n\n## Finding the pixel order \n\nI reverse-engineered the pixel mapping by lighting pixels and observing their positions. The 6×10 panel has **10 runs of 6 pixels** in plain raster order: each run follows the same direction. It is not serpentine (alternating direction on successive runs).\n\nDocumenting that observed order gives the demos a consistent way to translate positions on the panel into LED indices (numbers identifying individual lights).\n\n## Demo patterns \n\nI included a demo gallery with a scrolling green dot, solid fills, a wipe, a rainbow, a bouncing dot, a breathing brightness pattern, and a 0–10 counter. These patterns exercise individual pixels, color changes, movement, and number display. The cover photo shows the panel running the rainbow demo.\n\n## Power and wiring \n\nI prefer **5 V** for the panel’s VCC (power input), with a common ground between the panel and XIAO and D0/A0 connected to DIN (data input). The panel also works at **3.3 V**, with dimmer output. The [hardware notes](<https://github.com/DevanMetz/6x10-RGB-WS2812-Matrix-for-XIAO-nRF54L15/blob/main/docs/hardware.md>) collect the pinout and wiring details.\n\n## Related work \n\nMy [nRF54L15 Support](<https://devanmetz.com/portfolio/nrf54l15-support/>) project provides reusable libraries and Zephyr drivers for the same microcontroller family. This matrix repository focuses on the Seeed panel’s driver, examples, and hardware documentation.\n\nMy [Printed LED Sculptures](<https://devanmetz.com/portfolio/printed-led-sculptures/>) are separate projects using printed structures and lighting; they are not builds of this matrix example."
    },
    {
      "id": "kickr-core-hub",
      "title": "KICKR CORE 2 Hub",
      "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
      "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md",
      "collection": "portfolio",
      "description": "An indoor cycling cockpit with virtual shifting, a 3D riding world, live trainer telemetry, and game-style progression.",
      "summary": "I built KICKR CORE 2 Hub as an indoor cycling interface with virtual gears, a moving 3D world, ride statistics, and game progression. Its browser simulator lets anyone explore those features without a trainer. Controlling a physical Wahoo trainer requires a local connection service, and the project remains an interactive prototype.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "React",
        "TypeScript",
        "Three.js",
        "Smart trainers"
      ],
      "projectDate": {
        "start": "2026-08"
      },
      "category": "Cycling software",
      "group": "tools",
      "status": "Interactive prototype",
      "facts": [
        {
          "label": "My work",
          "value": "Trainer integration, 3D interface, and ride analytics"
        },
        {
          "label": "Stack",
          "value": "React, TypeScript, Three.js, and Node.js"
        },
        {
          "label": "Hardware target",
          "value": "Wahoo KICKR CORE 2 via Direct Connect"
        }
      ],
      "actions": [
        {
          "label": "Try the simulator",
          "url": "https://kickr-core-hub.pages.dev/"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/kickr-core-hub.D8EbYecQ_ZOmNAa.webp",
        "alt": "A cyclist in a colorful 3D landscape surrounded by power, cadence, virtual gearing, and ride timeline panels.",
        "caption": "The browser simulator running an endurance ride. All telemetry shown here is synthetic.",
        "width": 1200,
        "height": 750
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/kickr-stats.CqL7K2dG_FJnRX.webp",
          "alt": "The Performance and Stat Review panel showing energy, ride duration, peak power, and a power-duration chart.",
          "caption": "The ride review presents energy and power-duration data from a short simulated session.",
          "width": 1200,
          "height": 750
        },
        {
          "url": "https://devanmetz.com/_astro/kickr-garage.DEtZ3cY3_Z1UqbzY.webp",
          "alt": "The Rider Guild and Garage with Strength, Endurance, Agility, and Climbing skill cards.",
          "caption": "Game progression earned during the simulator session, with tabs for equipment, upgrades, quests, and segments.",
          "width": 1200,
          "height": 750
        }
      ],
      "media": [],
      "project": null,
      "stories": [
        {
          "id": "kickr-core-hub-simulator",
          "title": "One simulated ride, the whole cycling cockpit",
          "url": "https://devanmetz.com/blog/kickr-core-hub-simulator/",
          "markdownUrl": "https://devanmetz.com/blog/kickr-core-hub-simulator/index.md"
        }
      ],
      "related": [
        {
          "id": "market-atlas",
          "title": "Market Atlas",
          "url": "https://devanmetz.com/portfolio/market-atlas/",
          "markdownUrl": "https://devanmetz.com/portfolio/market-atlas/index.md"
        },
        {
          "id": "agent-message-board",
          "title": "Agent Message Board",
          "url": "https://devanmetz.com/portfolio/agent-message-board/",
          "markdownUrl": "https://devanmetz.com/portfolio/agent-message-board/index.md"
        },
        {
          "id": "goop-troops",
          "title": "Goop Troops",
          "url": "https://devanmetz.com/portfolio/goop-troops/",
          "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built KICKR CORE 2 Hub to turn indoor cycling into an interactive riding cockpit.\n\nIt combines an interface for a smart trainer (an indoor cycling trainer that reports ride data and accepts control commands), virtual gears, a Three.js 3D world, and saved game progression in one browser user interface.\n\nThe project spans both sides of the connection: a Node.js bridge (a local connection service) for communicating with a Wahoo trainer over the local network, and a React interface for riding, reviewing sessions, and experimenting with different setups.\n\nThe [public demo](<https://kickr-core-hub.pages.dev/>) includes a simulator that runs without trainer hardware.\n\n## Riding interface \n\nThe main view puts power, cadence (pedaling rate), speed, distance, and accumulated work (energy over the ride) around a moving 3D scene. Short power averages and a scrolling timeline make changes in effort visible without opening another panel.\n\n### The riding world \n\nPedaling animation follows cadence, while the landscape, visual effects, and speed streaks respond to the ride. The world includes rolling terrain, pine trees, mountain ridges, and streetlights.\n\n-   **Camera views:** chase, first-person, drone, and free-orbit.\n-   **Lighting:** dawn, daytime, sunset, and night change the atmosphere.\n\nThe aim is to make the workout readable while giving the rider something more engaging than a stationary dashboard.\n\n## Virtual shifting and trainer controls \n\nVirtual gearing changes the simulated gear without a physical shift. It is designed for a single-cog trainer setup, with one rear sprocket. Presets include:\n\n-   A sequential 24-speed drivetrain, stepping through gears in order.\n-   Shimano 2×12 and SRAM AXS 2×12 setups, each with two front gears and twelve rear gears.\n-   A 1×12 gravel setup, with one front gear and twelve rear gears.\n\nRiders can shift with the on-screen controls, keyboard, or a gamepad. Mechanical shift sounds provide another cue that a gear changed.\n\n### Trainer control modes \n\nThe trainer-control interface supports four modes:\n\n-   **ERG (target-power mode):** hold a target power.\n-   **Simulation:** configure grade (slope), wind, rolling resistance (drag from tire contact), and aerodynamic conditions (air drag).\n-   **Resistance:** set a resistance percentage directly.\n-   **Level:** select a progressive resistance level from 0–9.\n\n### Connecting to a trainer \n\nThe bridge discovers compatible trainers on the local network or accepts a direct IP connection using the trainer’s network address.\n\nIt performs service discovery (finding the trainer’s available services), subscribes to telemetry (live trainer readings), and waits for the trainer’s control acknowledgement (confirmation) before enabling control commands.\n\n## Progression and the garage \n\nThe game layer borrows the 1–99 skill structure of RuneScape. Strength, Endurance, Agility, and Climbing gain experience from different aspects of a ride, including power, time, cadence, and simulated gradient (slope).\n\n### Equipment and saved progress \n\nEarned Drops, the game’s currency, fund equipment in the garage, including frames, wheels, jerseys, and headwear.\n\nWorkshop upgrades, bounties (ride objectives), segments (sections of a route), and temporary bonuses give sessions additional objectives. These levels describe progress inside the game.\n\nProgress is saved in the browser so the rider can return to the same character and equipment.\n\n## Ride history and analytics \n\nThe Stats panel brings together a power-duration curve (power sustained over different lengths of time), time in power zones (effort ranges), and ride history.\n\nSession summaries include duration, distance, average and peak power, and accumulated energy. The power-duration view compares efforts from five seconds through twenty minutes when enough data is available.\n\n### Storage and estimates \n\nThe app keeps up to 50 archived rides in local browser storage and maintains lifetime totals. Virtual speed, calories, and FTP-related calculations (based on functional threshold power, an estimate of sustained cycling power) are estimates. Storage is local to that browser, with no automatic cloud backup.\n\n## Trying the simulator \n\nOpen the demo and select **Sim** to generate a ride. Choose Recovery, Endurance, Intervals, Sprint, or Climb to change its pattern. The same data feeds the dashboard, 3D scene, skill progression, and stats panels.\n\nUse **Garage & Skills** to inspect progression and **Stats** to review the session. Keyboard shortcuts include **W/S** for shifting, **C** for camera views, and **L** for lighting. Select **Sim** again to stop.\n\nThe screenshots on this page come from that simulator. They show synthetic telemetry, rather than recorded personal workout results.\n\n## Architecture and current state \n\n### How the pieces connect \n\nReact and TypeScript handle the interface, Three.js renders the riding world, and shared modules define gearing and telemetry.\n\nA Node.js service handles trainer discovery and the Direct Connect protocol (the trainer’s network communication rules), then relays state to the browser over WebSocket, a connection that lets the service and browser exchange updates.\n\n### What runs in the public demo \n\nThe public Cloudflare Pages deployment hosts the frontend (the browser interface) and simulator. Controlling a physical trainer requires the local bridge on the trainer’s network. The current release is an interactive prototype, with the simulator available for anyone to explore.\n\nRelated projects include my [COLMI smart-ring firmware for Zwift](<https://devanmetz.com/portfolio/colmi-r12-zwift/>) and [DIY electric bike](<https://devanmetz.com/portfolio/diy-electric-bike/>), which explore other sides of cycling hardware and telemetry."
    },
    {
      "id": "painted-rust-props",
      "title": "Hand-Painted Rust Props",
      "url": "https://devanmetz.com/portfolio/painted-rust-props/",
      "markdownUrl": "https://devanmetz.com/portfolio/painted-rust-props/index.md",
      "collection": "portfolio",
      "description": "Game objects made physical—printed satchels, tools, a storage chest, and display replicas finished by hand.",
      "summary": "I printed, assembled, and hand-painted a collection of display props based on items from the game Rust. Paint separates straps, handles, metal details, and bone-colored surfaces on the printed pieces. The collection includes a satchel, tools, a storage chest, and nonfunctional display replicas.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "3D printing",
        "Hand painting",
        "Prop finishing",
        "Fan art"
      ],
      "projectDate": {
        "start": "2021",
        "end": "2022"
      },
      "category": "Prop making",
      "group": "props",
      "status": "Printed & painted",
      "facts": [
        {
          "label": "My work",
          "value": "3D printing, assembly, and hand painting"
        },
        {
          "label": "Collection",
          "value": "Satchel, chest, tools, and display replicas"
        },
        {
          "label": "Finish",
          "value": "Color-separated surfaces and weathered details"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/rust-props.CqWVp3Pl_ZGd5in.webp",
        "alt": "Hand-painted Rust-inspired satchel, pickaxe, storage chest, and glowing miniature furnace arranged together.",
        "caption": "A finished set of Rust-inspired prints, assembled and painted by hand.",
        "width": 1200,
        "height": 675
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/rust-satchel-unpainted.CbkWTTgG_Z17JQ9b.webp",
          "alt": "An unpainted white satchel display prop with raised straps, loops, and cylindrical details.",
          "caption": "The satchel before paint, with the printed surface details visible.",
          "width": 1200,
          "height": 1673
        },
        {
          "url": "https://devanmetz.com/_astro/rust-satchel.TaEq6F5L_9aU77.webp",
          "alt": "A completed satchel prop with a tan body, dark brown straps, and black details.",
          "caption": "Contrasting paint colors separate the satchel body, straps, and top details.",
          "width": 1200,
          "height": 675
        },
        {
          "url": "https://devanmetz.com/_astro/rust-eoka.Di4tuFQl_Z2kr4K6.webp",
          "alt": "A small Eoka display replica with a tan curved handle, dark barrel, and wrapped detail.",
          "caption": "The Eoka as a finished, nonfunctional display prop.",
          "width": 1200,
          "height": 675
        },
        {
          "url": "https://devanmetz.com/_astro/rust-bone-knife.D9Wv9ndB_zc4Dj.webp",
          "alt": "A printed bone-knife display prop with a white blade and brown wrapped handle.",
          "caption": "A bone-knife prop with contrasting blade and handle finishes.",
          "width": 1200,
          "height": 675
        },
        {
          "url": "https://devanmetz.com/_astro/rust-rocket.DX2XIpNw_Z1EAcBh.webp",
          "alt": "A painted miniature rocket display prop with a dark body, red tip, and open tail details.",
          "caption": "A miniature rocket replica from the same printed prop collection.",
          "width": 1200,
          "height": 675
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "runescape-prints",
          "title": "RuneScape Prints & Props",
          "url": "https://devanmetz.com/portfolio/runescape-prints/",
          "markdownUrl": "https://devanmetz.com/portfolio/runescape-prints/index.md"
        },
        {
          "id": "rust-furnace-lamps",
          "title": "Rust Furnace Lamps",
          "url": "https://devanmetz.com/portfolio/rust-furnace-lamps/",
          "markdownUrl": "https://devanmetz.com/portfolio/rust-furnace-lamps/index.md"
        },
        {
          "id": "diy-electric-bike",
          "title": "DIY Electric Bike",
          "url": "https://devanmetz.com/portfolio/diy-electric-bike/",
          "markdownUrl": "https://devanmetz.com/portfolio/diy-electric-bike/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI printed, assembled, and hand-painted this collection of display props inspired by the game Rust.\n\nThe set includes a satchel, pickaxe, storage chest, Eoka (a gun-shaped game item), bone knife, and miniature rocket.\n\nPaint gives the printed pieces their different surfaces: brown straps and handles, dark metal details, and lighter bone-colored areas. These are nonfunctional display replicas.\n\n## Pieces in the collection \n\n| Piece | Details |\n| --- | --- |\n| Satchel | Raised straps, loops, and cylindrical details; tan body with dark brown and black accents |\n| Storage chest | Printed chest with a hand-painted finish |\n| Pickaxe | Contrasting colors for the handle and head |\n| Eoka | Tan curved handle, dark barrel-shaped detail, and wrapped section |\n| Bone knife | Light blade with a brown wrapped handle |\n| Miniature rocket | Dark body, red tip, and open tail details |\n\nThe separate [furnace lamps](<https://devanmetz.com/portfolio/rust-furnace-lamps/>) add warm lighting to the same family of props.\n\n## Satchel: before and after \n\nThe satchel started as a white print, with the folds, raised straps, loops, and top details already built into the shape.\n\n### Separating the surfaces with paint \n\nI painted the body tan, the straps dark brown, and the smaller details black. Those color changes make the straps and fittings stand out from the body. The gallery shows the same prop before and after painting.\n\n## Painting and surface details \n\nI used contrasting colors across the smaller pieces:\n\n-   **Bone knife:** a light blade and a brown wrapped handle.\n-   **Eoka:** a tan grip with a dark barrel and wrapped detail.\n-   **Miniature rocket:** a red tip contrasting with the dark body and open tail.\n\nThe print texture remains visible beneath the paint, especially around edges and recessed details (areas set below the surrounding surface).\n\n## Game inspiration \n\nThese are fan props based on items from Rust. My work is the printing, assembly, and hand painting; the game-item designs come from Rust."
    },
    {
      "id": "rust-furnace-lamps",
      "title": "Rust Furnace Lamps",
      "url": "https://devanmetz.com/portfolio/rust-furnace-lamps/",
      "markdownUrl": "https://devanmetz.com/portfolio/rust-furnace-lamps/index.md",
      "collection": "portfolio",
      "description": "Hand-painted furnace replicas with glowing interiors, from small shelf pieces to a larger USB-powered build.",
      "summary": "I printed and hand-painted furnace lamps inspired by the game Rust, with lights inside to give the openings a warm glow. I made small shelf pieces with electric candle lights and a larger USB-powered version with a separate internal light support.",
      "date": null,
      "updated": "2026-10-15",
      "tags": [
        "3D printing",
        "Hand painting",
        "LEDs",
        "Assembly"
      ],
      "projectDate": {
        "start": "2021"
      },
      "category": "Props & lighting",
      "group": "props",
      "status": "Printed & painted",
      "facts": [
        {
          "label": "My work",
          "value": "Printed assemblies, hand painting, and lighting"
        },
        {
          "label": "Versions",
          "value": "Miniature candle-lit and larger USB-powered builds"
        },
        {
          "label": "Large build",
          "value": "Furnace shell, bottom, and internal light holder"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/rust-furnace-lamps.CAwPPetV_Z1urPbq.webp",
        "alt": "Six hand-painted miniature furnaces with tan tops, gray stone bases, and glowing yellow interiors.",
        "caption": "A finished batch of illuminated Rust-inspired furnace replicas.",
        "width": 1200,
        "height": 675
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/rust-furnaces-unpainted.CiF-kuuk_ZN6F8v.webp",
          "alt": "Several white printed furnace shells before painting and final assembly.",
          "caption": "Printed furnace shells before the stone, clay, and soot colors are added.",
          "width": 1200,
          "height": 900
        },
        {
          "url": "https://devanmetz.com/_astro/rust-furnace-lit.CfQ1-FHe_ZDr2Jq.webp",
          "alt": "A finished miniature furnace with a weathered tan chimney, gray base, and warm light inside.",
          "caption": "The painted surface and warm interior light together on a finished miniature.",
          "width": 1200,
          "height": 675
        },
        {
          "url": "https://devanmetz.com/_astro/rust-furnace-large.BtRWFfpQ_ZXGM2E.webp",
          "alt": "A larger printed furnace beside a gallon jug for scale.",
          "caption": "The larger furnace version, photographed beside a gallon jug for scale.",
          "width": 1200,
          "height": 675
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "painted-rust-props",
          "title": "Hand-Painted Rust Props",
          "url": "https://devanmetz.com/portfolio/painted-rust-props/",
          "markdownUrl": "https://devanmetz.com/portfolio/painted-rust-props/index.md"
        },
        {
          "id": "runescape-prints",
          "title": "RuneScape Prints & Props",
          "url": "https://devanmetz.com/portfolio/runescape-prints/",
          "markdownUrl": "https://devanmetz.com/portfolio/runescape-prints/index.md"
        },
        {
          "id": "tabletop-dice-boxes",
          "title": "Dice Boxes & Life Counters",
          "url": "https://devanmetz.com/portfolio/tabletop-dice-boxes/",
          "markdownUrl": "https://devanmetz.com/portfolio/tabletop-dice-boxes/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI printed the shells, painted them by hand, and added lights inside to make these furnace lamps inspired by the game Rust.\n\nThe gray stone bases, tan chimneys, and darkened openings recreate the look of the in-game furnace.\n\nI built a batch of small lamps with electric candle lights and a larger version powered by USB.\n\n## Small candle-lit lamps \n\nThe smaller pieces use an electric candle-style light inside the printed shell.\n\nThe gallery includes the white shells before painting and a finished miniature; the cover shows a batch of six with their lights on.\n\nPainting separates the gray lower stones from the tan upper surface.\n\nDark marks around the opening and chimney give the form a used appearance, while the interior light remains visible through the front.\n\n## Larger USB-powered version \n\nThe larger assembly has three printed parts:\n\n-   Main shell\n-   Bottom closure\n-   Interior light support\n\n### Power and lighting \n\nThe electronics consist of:\n\n-   A USB cable.\n-   A rocker switch, which toggles power on and off.\n-   A boost converter, which raises the input voltage.\n-   A light strip.\n\nThe switch controls input power to the converter, which supplies the strip inside the light support.\n\nFor a similar build, match the converter’s output voltage to the light strip and check the power supply’s current rating (how much electrical current it can supply).\n\n## Assembly sequence \n\nThe larger lamp comes together in four steps:\n\n1.  Print the shell, bottom, and light support.\n2.  Paint the shell and glue the light strip into its support.\n3.  Wire the switch, converter, and strip.\n4.  Fix the support inside the furnace and attach the bottom.\n\nThe separate support makes the lighting easier to assemble before closing the shell. The large furnace is pictured beside a gallon jug for scale.\n\n## Finish and photographs \n\nThe printed shell defines the stones and chimney. Paint separates the surfaces and adds soot marks, while the light gives the opening a warm glow.\n\nThese are fan props based on Rust’s furnace. My work is the printing, painting, assembly, and lighting. See my [other Rust props](<https://devanmetz.com/portfolio/painted-rust-props/>) for the satchel, chest, and smaller replicas."
    },
    {
      "id": "runescape-prints",
      "title": "RuneScape Prints & Props",
      "url": "https://devanmetz.com/portfolio/runescape-prints/",
      "markdownUrl": "https://devanmetz.com/portfolio/runescape-prints/index.md",
      "collection": "portfolio",
      "description": "A printed Jad figure and a hand-painted dagger that bring angular game designs onto the workbench.",
      "summary": "I printed a red Jad figure and made a hand-painted dagger inspired by the game RuneScape. The figure shows the angular shape and print layers in one color, while paint separates the dagger’s blade, guard, and handle. The dagger is a decorative display prop.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "3D printing",
        "Miniatures",
        "Hand painting",
        "Fan art"
      ],
      "projectDate": {
        "start": "2021",
        "end": "2022"
      },
      "category": "Props & miniatures",
      "group": "props",
      "status": "Physical builds",
      "facts": [
        {
          "label": "My work",
          "value": "Physical printing and prop finishing"
        },
        {
          "label": "Pieces",
          "value": "Jad figure and painted dagger"
        },
        {
          "label": "Style",
          "value": "Faceted game shapes with visible print texture"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/runescape-jad.DDM3lvIW_Z2klVvA.webp",
        "alt": "A red 3D-printed Jad figure with angular legs, horns, and an open mouth on a wooden surface.",
        "caption": "A red Jad print, with its faceted shape and print layers visible.",
        "width": 1200,
        "height": 675
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/runescape-dagger.BuR5ZhDj_Z25NRIK.webp",
          "alt": "Close view of a hand-painted dagger prop with a red blade, green tip, gray guard, and brown handle.",
          "caption": "The painted dagger's blade, guard, and handle use distinct colors.",
          "width": 1200,
          "height": 1600
        },
        {
          "url": "https://devanmetz.com/_astro/runescape-workbench.CMYmM9ae_ZVipzs.webp",
          "alt": "A painted dagger and red Jad figure beside a miniature furnace on a white photo backdrop.",
          "caption": "The RuneScape-inspired pieces on the photo bench alongside a Rust furnace replica.",
          "width": 1200,
          "height": 900
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "painted-rust-props",
          "title": "Hand-Painted Rust Props",
          "url": "https://devanmetz.com/portfolio/painted-rust-props/",
          "markdownUrl": "https://devanmetz.com/portfolio/painted-rust-props/index.md"
        },
        {
          "id": "rust-furnace-lamps",
          "title": "Rust Furnace Lamps",
          "url": "https://devanmetz.com/portfolio/rust-furnace-lamps/",
          "markdownUrl": "https://devanmetz.com/portfolio/rust-furnace-lamps/index.md"
        },
        {
          "id": "diy-electric-bike",
          "title": "DIY Electric Bike",
          "url": "https://devanmetz.com/portfolio/diy-electric-bike/",
          "markdownUrl": "https://devanmetz.com/portfolio/diy-electric-bike/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI printed a red Jad figure and made a hand-painted dagger based on the game RuneScape.\n\nThe figure keeps a single red color, while the dagger uses separate colors for the blade, guard, and grip.\n\n## Jad figure \n\nJad is the game character represented by the figure. The print has a faceted body (made up of flat, angular surfaces), spread legs, horns, and an open mouth.\n\nIts angular shape works well as a single-color print, with the red material and visible print layers giving it texture.\n\n## Painted dagger \n\nI painted the dagger with a red blade, green tip, gray guard, and brown handle.\n\nThe colors follow the edges of each part, so the blade and handle stand apart even though they share the same printed texture.\n\nIt is a decorative display prop.\n\n## Shape and paint \n\nI used two different finishing approaches:\n\n-   **Jad:** most of its appearance comes from the shape itself.\n-   **Dagger:** paint does more of the work to separate its surfaces.\n\nBoth approaches also show up in my [Rust prop collection](<https://devanmetz.com/portfolio/painted-rust-props/>).\n\n## Game inspiration \n\nThese are RuneScape fan pieces. My work is the printing and finishing; the character and item designs come from the game."
    },
    {
      "id": "openrp",
      "title": "OpenRP",
      "url": "https://devanmetz.com/portfolio/openrp/",
      "markdownUrl": "https://devanmetz.com/portfolio/openrp/index.md",
      "collection": "portfolio",
      "description": "A multiplayer city sandbox in the browser, with jobs, property, businesses, and server-owned physics.",
      "summary": "I’m building OpenRP, a multiplayer city game that runs in a browser, where players can take jobs, own property, and shape city life. Its shared systems connect businesses, construction, and government without requiring a Garry’s Mod installation. It is a playable alpha, and I’m still developing and tuning its systems.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "Three.js",
        "TypeScript",
        "Node.js",
        "WebSockets"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Game development",
      "group": "games",
      "status": "Public alpha",
      "facts": [
        {
          "label": "My work",
          "value": "Game systems, networked simulation, and original assets"
        },
        {
          "label": "Stack",
          "value": "Three.js · TypeScript · Node.js"
        },
        {
          "label": "Release",
          "value": "Playable 0.5 alpha"
        }
      ],
      "actions": [
        {
          "label": "Visit OpenRP",
          "url": "https://openrp.dev"
        },
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/openrp"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/openrp.I2SRLusV_AL0g2.webp",
        "alt": "OpenRP project artwork showing the low-poly city and its jobs, property, and physics systems.",
        "caption": "OpenRP’s project artwork, showing the browser-built city.",
        "width": 1200,
        "height": 630
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "poker-with-friends",
          "title": "Poker With Your Friends",
          "url": "https://devanmetz.com/portfolio/poker-with-friends/",
          "markdownUrl": "https://devanmetz.com/portfolio/poker-with-friends/index.md"
        },
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        },
        {
          "id": "goop-troops",
          "title": "Goop Troops",
          "url": "https://devanmetz.com/portfolio/goop-troops/",
          "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI’m building OpenRP as a multiplayer city sandbox—a place where players can choose their own activities—that runs in a browser. Players can take jobs, buy property, operate businesses, build with physics props (objects that can move and collide), and participate in elections and city laws.\n\nI took inspiration from the social sandbox of DarkRP, a roleplay game mode for Garry’s Mod. OpenRP has its own implementation and locally generated map geometry (the shapes that make up the map), characters, equipment, textures, and sound effects.\n\nA Garry’s Mod installation or extracted game assets are not required.\n\n## The city and its systems \n\nThe city connects several activities through shared resources and ownership:\n\n-   **Jobs:** roles define salaries, equipment, and access to businesses.\n-   **Property:** doors can be purchased, named, locked, and shared through keys.\n-   **Businesses:** stock, prices, and purchases connect a shop to its owner.\n-   **Construction:** players manipulate props using physics and tool controls.\n-   **Government:** elections, laws, licenses, warrants, and arrests change city life.\n-   **Social play:** nearby chat, roleplay actions, group channels, and proximity voice (voice chat limited by how close players are) support interaction.\n\nThe Union District includes neighborhoods, multi-room apartments, and businesses. Shared lobbies remain public while individual units have their own ownership.\n\n## Multiplayer architecture \n\n### Rendering and server decisions \n\nI use Three.js, a JavaScript 3D graphics library for the browser, to render the world. A Node.js server (Node.js runs JavaScript on a server) owns movement checks, rigid-body physics (simulating how solid objects move and collide), combat, and economic transactions.\n\nWebSockets—a connection that stays open so the browser and server can send messages both ways—carry commands and state updates.\n\n### Keeping movement responsive \n\nThe simulation runs at 30 Hz (30 updates per second), with 15 Hz snapshots (shared copies of the game state sent 15 times per second). Client prediction estimates movement in the browser before the server responds; interpolation fills in movement between received states.\n\nThese help movement look responsive between updates from the authoritative server, which has the final say on game state. Purchases and combat results still depend on server decisions.\n\n### Voice connections \n\nVoice has a separate connection path with server-enforced proximity, explicit microphone controls, and independent muting. The hosting setup has to support long-lived connections for both gameplay and voice.\n\n## First session and controls \n\nOpen [OpenRP](<https://openrp.dev/>), create an account or enter as a guest, and choose to return to the streets. The field guide explains the controls inside the game.\n\n| Control | Use |\n| --- | --- |\n| WASD and mouse | Move and look |\n| E | Use the targeted object |\n| F4 | Jobs, shop, laws, and voting |\n| Q | Building tools and props |\n| C | Context actions |\n| F1 | Field guide |\n| V | Push-to-talk after enabling the microphone |\n\n### Guests and accounts \n\nGuest identities rely on a stored browser credential, a saved identifier that lets the server recognize the guest. An account can claim the guest character’s belongings. Accounts belong to the server being played on; independently hosted servers do not share an account database.\n\n## Running a local copy \n\nUse Node.js 22.12 or newer. From a checkout:\n\n```\nnpm ci\nnpm run dev\n```\n\nThe client and server run together at localhost port 3000, an address on your own computer. A production build uses `npm run build` followed by `npm start`.\n\n### Hosting on the internet \n\nInternet hosting requires a Node server with WebSocket support. Static file hosting alone cannot run the multiplayer simulation. The repository’s hosting guide covers the gameplay and voice upgrade routes.\n\n## Status and reference \n\nOpenRP is a playable alpha. I’m still developing its systems and tuning how they interact.\n\nThe [public repository](<https://github.com/DevanMetz/openrp>) contains the source, control reference, configuration, and self-hosting documentation (instructions for running your own server). The cover is project artwork showing the city."
    },
    {
      "id": "openmic",
      "title": "OpenMic",
      "url": "https://devanmetz.com/portfolio/openmic/",
      "markdownUrl": "https://devanmetz.com/portfolio/openmic/index.md",
      "collection": "portfolio",
      "description": "A native Windows microphone cleaner and soundboard that keeps the audio processing on your computer.",
      "summary": "I built OpenMic, a Windows app that cleans microphone audio and mixes in local sound clips before sending the result to another application. The audio processing stays on the computer and does not need a remote service. Separate microphone mute and soundboard controls let clips keep playing when I mute my voice.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "Rust",
        "Audio DSP",
        "RNNoise",
        "egui"
      ],
      "projectDate": {
        "start": "2026-08"
      },
      "category": "Desktop software",
      "group": "tools",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Rust application, audio routing, mixing, and UI"
        },
        {
          "label": "Platform",
          "value": "Windows 10 and 11"
        },
        {
          "label": "Processing",
          "value": "Local 48 kHz RNNoise core"
        }
      ],
      "actions": [
        {
          "label": "Source & downloads",
          "url": "https://github.com/DevanMetz/openmic"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/openmic.DRxTHtRj_tPcsw.webp",
        "alt": "OpenMic’s native interface with microphone routing, noise reduction, gain, noise gate, and live audio meters.",
        "caption": "The microphone workspace, with routing, processing controls, and live input and output meters.",
        "width": 1200,
        "height": 1420
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "featherclick",
          "title": "FeatherClick",
          "url": "https://devanmetz.com/portfolio/featherclick/",
          "markdownUrl": "https://devanmetz.com/portfolio/featherclick/index.md"
        },
        {
          "id": "opensnip",
          "title": "OpenSnip",
          "url": "https://devanmetz.com/portfolio/opensnip/",
          "markdownUrl": "https://devanmetz.com/portfolio/opensnip/index.md"
        },
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built OpenMic as a native Windows app (software that runs directly on Windows) for microphone cleaning and a soundboard, which plays selected audio clips. It processes live microphone audio, mixes optional local clips, and sends the combined result to a selected output device.\n\nA virtual audio cable, a software connection that passes audio between applications, lets another application use that processed result as its microphone input. Audio processing—the digital signal processing (DSP) that changes or cleans the sound—stays on the computer; the application does not need a hosted inference service (a remote service running the noise-cleaning model).\n\n## Audio pipeline \n\n### Formats and noise suppression \n\nDevices open in their native formats. OpenMic resamples audio (changes its sample rate) into a 48 kHz RNNoise processing core. That rate means 48,000 audio samples per second; RNNoise is an open-source noise-suppression library from Xiph.\n\nOpenMic converts the audio as needed for the chosen output.\n\n### Processing controls \n\nThe controls expose different stages of the signal path (the route audio takes through processing):\n\n| Control | Purpose |\n| --- | --- |\n| Input gain (level adjustment) | Adjust the microphone level before processing |\n| Noise reduction | Mix between the original and cleaned signal |\n| Noise gate (a cutoff for quiet audio) | Suppress audio below the selected threshold |\n| Output gain (level adjustment) | Set the level leaving the processor |\n| Bypass (skip processing) | Compare with processing disabled |\n| Microphone mute | Silence voice while leaving the soundboard available |\n\nInput and output meters, together with a voice-probability readout (the processor’s estimate that the sound is speech), make it possible to see what the processor is receiving and producing.\n\n## Routing into a voice application \n\n1.  Select the physical microphone in OpenMic.\n2.  Choose **CABLE Input** as the processed output when using VB-Audio Virtual Cable.\n3.  Select **CABLE Output** as the microphone in the receiving application.\n4.  Check the meters and adjust gain before adding more processing.\n5.  Use headphones when enabling the separate headphone monitor (listening to the output through headphones).\n\nThe Input and Output names refer to opposite ends of the cable. A common routing mistake is choosing the same end in both applications.\n\nWhen trying OpenMic, disable any second noise suppressor, such as Discord’s Krisp noise-suppression feature, to hear what OpenMic is doing on its own.\n\n## Soundboard behavior \n\nThe Soundboard tab accepts local WAV, FLAC, OGG, MP3, and AIFF clips through Symphonia, an audio-decoding library that reads the clip files. Symphonia is written in Rust, the programming language OpenMic uses. Clips have their own volume and mix into the processed output.\n\n### Device changes and saved settings \n\nDevice changes hand the stream to a new engine while preserving the clip’s playhead (its current playback position). Microphone mute and soundboard playback are independent, so muting a conversation does not unexpectedly stop a playing clip.\n\nSettings and the soundboard list persist between runs. Optional Windows startup and automatic processing make the app usable as an everyday routing tool.\n\n## Building and testing \n\n### Build requirements \n\nI wrote the application in Rust with an egui interface (egui is a Rust user-interface library). RNNoise is built from the vendored Xiph source, a copy of that source included in the repository.\n\nThe Windows build requires Rust, MSVC build tools, and a compatible Clang C compiler for RNNoise. MSVC and Clang are compilers that turn source code into runnable software.\n\n### Running and testing \n\nCargo is Rust’s build and package tool. From the repository, `cargo run --release` builds and starts the app. The test suite is run with `cargo test`.\n\nTests cover gain, gating, bypass, mute, soundboard isolation, decoding, resampling, stream continuity, settings persistence, and startup handling.\n\n## Source and attribution \n\nThe [OpenMic repository](<https://github.com/DevanMetz/openmic>) contains setup instructions, source, and the native interface capture shown above. My work covers the application and its routing, mixing, controls, and integration. RNNoise retains the upstream project’s source and license notices."
    },
    {
      "id": "workshop-builds",
      "title": "From the Workbench",
      "url": "https://devanmetz.com/portfolio/workshop-builds/",
      "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md",
      "collection": "portfolio",
      "description": "Printed controls, a soldering fan, and other small workshop builds where CAD becomes something physical.",
      "summary": "I built an arcade-style controller, a soldering fan, and other small workshop projects using printed parts. Custom housings and mounts bring buttons, motors, and other components together into physical builds.",
      "date": null,
      "updated": "2026-10-16",
      "tags": [
        "3D printing",
        "CAD",
        "Electronics",
        "Video"
      ],
      "projectDate": {
        "start": "2020",
        "end": "2021"
      },
      "category": "Making & fabrication",
      "group": "hardware",
      "status": "Physical builds",
      "facts": [
        {
          "label": "My work",
          "value": "Printing, assembly, electronics integration, and videos"
        },
        {
          "label": "Builds",
          "value": "Arcade controller · workshop fan · small printed parts"
        },
        {
          "label": "Parts",
          "value": "Printed housings, controls, mounts, and grilles"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/fightstick.BJ0NTx2N_G7jXF.webp",
        "alt": "A custom black 3D-printed arcade-style controller with large gray buttons.",
        "caption": "An arcade-style controller build, with a printed enclosure and button caps.",
        "width": 1200,
        "height": 675
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/fume-extractor.DDQOWRo7_VHnNL.webp",
          "alt": "A workshop fan with a white printed housing, front grille, and wide feet on a cutting mat.",
          "caption": "The soldering-fan build combines a printed housing, motor mount, grille, and feet.",
          "width": 1200,
          "height": 1600
        },
        {
          "url": "https://devanmetz.com/_astro/workbench-disc-sander.xKcc7u1B_1dOGGK.webp",
          "alt": "A desktop disc sander with an orange printed housing, a sanding disc on top, and wires leading out.",
          "caption": "A small disc sander I assembled with a printed housing.",
          "width": 1200,
          "height": 900
        },
        {
          "url": "https://devanmetz.com/_astro/workbench-pegboard-hooks.2ymku14c_ZFVXtM.webp",
          "alt": "Blue printed hooks and green printed spool holders holding electrical tape and solder on a brown pegboard, with pliers below.",
          "caption": "Printed hooks and spool holders keep supplies on my pegboard.",
          "width": 1200,
          "height": 1117
        },
        {
          "url": "https://devanmetz.com/_astro/workbench-monitor-mount.BigM81iQ_24bTCh.webp",
          "alt": "A blue printed hinged bracket clamped to the back edge of a monitor.",
          "caption": "A printed monitor bracket I assembled for my desk.",
          "width": 1200,
          "height": 1600
        },
        {
          "url": "https://devanmetz.com/_astro/workbench-speaker-plate.JOaAazSi_1A6pCr.webp",
          "alt": "A speaker cabinet with a green printed front plate holding two drivers.",
          "caption": "A printed front plate I assembled with two speaker drivers.",
          "width": 1200,
          "height": 2133
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "printed-led-sculptures",
          "title": "Printed LED Sculptures",
          "url": "https://devanmetz.com/portfolio/printed-led-sculptures/",
          "markdownUrl": "https://devanmetz.com/portfolio/printed-led-sculptures/index.md"
        },
        {
          "id": "printed-violin",
          "title": "3D-Printed Rainbow Violin",
          "url": "https://devanmetz.com/portfolio/printed-violin/",
          "markdownUrl": "https://devanmetz.com/portfolio/printed-violin/index.md"
        },
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        }
      ],
      "contentMarkdown": "## Small workshop builds \n\nThese are some of my smaller workshop builds: an arcade-style controller, a soldering fan, and printed parts for other projects.\n\nMost combine a custom printed part with buttons, a motor, or another component that needs a housing or mount. CAD (computer-aided design) means using software to work out a part’s shape before printing it.\n\n## Arcade-style controller \n\nI built the controller with a black printed enclosure, large buttons, and gray printed caps. The enclosure holds the controls together in an arcade-style layout.\n\n## Soldering fan \n\nThe soldering fan has a printed casing, front grille, motor mount, and wide feet. The mount holds the fan inside the body, while the feet support it at an angle on the workbench.\n\n## Shop tools and fixtures \n\nI printed and assembled a small disc sander with an orange housing for my workshop. I also printed pegboard hooks and spool holders to hold electrical tape and solder. For my desk, I printed and assembled a hinged monitor bracket and a speaker front plate that holds two drivers.\n\n## Printed violin experiment \n\nI also printed and assembled a rainbow violin using Hova Labs’ Hovalin design. Its [project page](<https://devanmetz.com/portfolio/printed-violin/>) has the CR-10 (3D printer) build details, my first sound test with guitar strings, and a clip of early bow practice.\n\n## Other printed parts \n\nOther experiments include:\n\n-   A printed mallet.\n-   Bike-light mounts.\n-   Organizers.\n-   Parts for RC (radio-controlled) cars and robot builds.\n\nThe [Catan organizers](<https://devanmetz.com/portfolio/catan-organizers/>), [dice boxes and counters](<https://devanmetz.com/portfolio/tabletop-dice-boxes/>), and [LED sculptures](<https://devanmetz.com/portfolio/printed-led-sculptures/>) each have a page with more photos and build details."
    },
    {
      "id": "catan-organizers",
      "title": "Catan Organizers",
      "url": "https://devanmetz.com/portfolio/catan-organizers/",
      "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md",
      "collection": "portfolio",
      "description": "Printed card trays and compact piece cases that keep resource decks, roads, and buildings organized on the table.",
      "summary": "I designed and printed card trays and covered player-piece cases for the board game Catan. The trays keep shared decks visible and accessible during play, while the cases store each player’s roads, settlements, and cities afterward. I revised the holders and covers to fit together, so matching versions and piece dimensions matter when printing a set.",
      "date": null,
      "updated": "2026-10-13",
      "tags": [
        "3D printing",
        "CAD",
        "Product design",
        "Tabletop games"
      ],
      "projectDate": {
        "start": "2020",
        "end": "2021"
      },
      "category": "Tabletop accessories",
      "group": "hardware",
      "status": "Physical builds",
      "facts": [
        {
          "label": "My work",
          "value": "Organizer design, printing, and fit revisions"
        },
        {
          "label": "Objects",
          "value": "Card trays and covered player-piece cases"
        },
        {
          "label": "Construction",
          "value": "Printed trays, fitted channels, and separate covers"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/catan-organizers.BjzwxP_I_1zlUX0.webp",
        "alt": "Catan resource and development cards arranged in connected black and white printed card trays.",
        "caption": "Printed trays holding the game's resource and development decks.",
        "width": 1200,
        "height": 900
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/catan-piece-case.BrhoJW8N_16tvrg.webp",
          "alt": "A compact white printed case with fitted spaces for gray roads, settlements, and cities.",
          "caption": "A player-piece case with separate positions for roads and buildings.",
          "width": 1200,
          "height": 675
        },
        {
          "url": "https://devanmetz.com/_astro/catan-case-cad.EsUeHcDA_Z6JujV.webp",
          "alt": "CAD detail of the piece holder showing a red road piece seated in its channel.",
          "caption": "A design view of the road storage channel during development.",
          "width": 1200,
          "height": 593
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "tabletop-dice-boxes",
          "title": "Dice Boxes & Life Counters",
          "url": "https://devanmetz.com/portfolio/tabletop-dice-boxes/",
          "markdownUrl": "https://devanmetz.com/portfolio/tabletop-dice-boxes/index.md"
        },
        {
          "id": "rigid-heddle-loom-accessories",
          "title": "Rigid Heddle Loom Accessories",
          "url": "https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/",
          "markdownUrl": "https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/index.md"
        },
        {
          "id": "ring-organizer",
          "title": "Ring and Jewelry Organizer",
          "url": "https://devanmetz.com/portfolio/ring-organizer/",
          "markdownUrl": "https://devanmetz.com/portfolio/ring-organizer/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed and printed these organizers for the board game Catan’s cards and player pieces.\n\nThe card trays keep the shared decks together during a game, and the covered cases store each player’s roads, settlements, and cities.\n\n## Card trays \n\nThe trays give each resource and development deck (the game’s shared card decks) its own slot. The cards stay visible and accessible during play, with black and white printed sections arranged together on the table.\n\n## Player-piece case \n\nThe compact case separates the pieces into:\n\n-   Parallel channels for roads.\n-   Spaces for settlements and cities, the game’s buildings.\n\nOpening it gives a quick view of the whole set. A separate cover closes the case for storage.\n\n### Matching holders and covers \n\nI made several holder and cover revisions, including V2 and V3 (version 2 and version 3). Each cover needs to fit its matching holder.\n\n## Design and fit \n\nI used the game pieces to check the fit of the printed holders.\n\nThe computer-aided design (CAD) close-up shows a road sitting in its channel, and the assembled case shows the roads and buildings loaded together.\n\nFor a different edition of Catan, check the piece dimensions before printing a full set of cases.\n\n## At the table \n\nThe trays hold the shared decks during play; the cases collect each player’s pieces afterward. I also made [dice boxes and rotating life counters](<https://devanmetz.com/portfolio/tabletop-dice-boxes/>) for other tabletop games.\n\nMy work here is the organizer design and printing. The game pieces and artwork are from Catan."
    },
    {
      "id": "tabletop-dice-boxes",
      "title": "Dice Boxes & Life Counters",
      "url": "https://devanmetz.com/portfolio/tabletop-dice-boxes/",
      "markdownUrl": "https://devanmetz.com/portfolio/tabletop-dice-boxes/index.md",
      "collection": "portfolio",
      "description": "Printed dice storage with lined rolling trays, plus numbered counters built from separate rotating rings.",
      "summary": "I printed and assembled dice boxes that open into storage and a lined rolling tray, along with rotating life counters for tabletop games. Separate numbered rings let me change one digit at a time to track a value during play. The builds need matching parts and enough room for the lids to close and the rings to turn.",
      "date": null,
      "updated": "2026-10-15",
      "tags": [
        "3D printing",
        "Assembly",
        "Product design",
        "Tabletop games"
      ],
      "projectDate": {
        "start": "2022"
      },
      "category": "Tabletop accessories",
      "group": "hardware",
      "status": "Physical builds",
      "facts": [
        {
          "label": "My work",
          "value": "Printing, fitting, assembly, and finishing"
        },
        {
          "label": "Objects",
          "value": "Dice storage/rolling box and rotating life counters"
        },
        {
          "label": "Mechanism",
          "value": "Separate numbered rings with pointers and end pieces"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/dice-box.DO8Vx7VX_Z1ki8yd.webp",
        "alt": "An open brown dice box with a set of colorful dice in one half and a green die in the lined rolling tray.",
        "caption": "The dice box opened into storage and a lined rolling tray.",
        "width": 1200,
        "height": 675
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/dice-box-closed.Jea-6V94_2w5xjy.webp",
          "alt": "The brown dice box closed, showing its textured lid and rounded rectangular shape.",
          "caption": "The same style of box closed for storage.",
          "width": 1200,
          "height": 675
        },
        {
          "url": "https://devanmetz.com/_astro/tabletop-counter-parts.B-tkxviv_1L8OnC.webp",
          "alt": "Separate gray printed numbered rings and end pieces for a tabletop counter.",
          "caption": "Counter parts before assembly, including the numbered rings.",
          "width": 1200,
          "height": 675
        },
        {
          "url": "https://devanmetz.com/_astro/tabletop-counter.ChUM7jId_Z25jGc2.webp",
          "alt": "An assembled multicolored tabletop life counter with raised numbers on its rotating rings.",
          "caption": "A finished counter with the digits built into the printed rings.",
          "width": 1200,
          "height": 904
        },
        {
          "url": "https://devanmetz.com/_astro/dice-box-mint-open.BTdIndpO_1tDx3d.webp",
          "alt": "An open mint-green dice box on wood with black felt lining, four silver rim magnets (two per long side), and a detached lid with a textured triangular pattern behind the base.",
          "caption": "A mint-green box variant with black felt lining and four inset magnets, with its textured lid behind the base.",
          "width": 1200,
          "height": 904
        },
        {
          "url": "https://devanmetz.com/_astro/life-counter-marble.BvFaZzGq_Z1le2xW.webp",
          "alt": "An assembled light blue-grey marble-speckled rotary life counter on a cutting mat, with an embossed arrow on the left end, a 2 visible on the middle ring, and 20 selected on the right ring among teen and twenty-range values.",
          "caption": "A marble-speckled life counter with an embossed arrow pointer and 20 selected on the right ring.",
          "width": 1200,
          "height": 675
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        },
        {
          "id": "rigid-heddle-loom-accessories",
          "title": "Rigid Heddle Loom Accessories",
          "url": "https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/",
          "markdownUrl": "https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/index.md"
        },
        {
          "id": "ring-organizer",
          "title": "Ring and Jewelry Organizer",
          "url": "https://devanmetz.com/portfolio/ring-organizer/",
          "markdownUrl": "https://devanmetz.com/portfolio/ring-organizer/index.md"
        }
      ],
      "contentMarkdown": "## Dice storage and game counters \n\nI printed and assembled these dice boxes and life counters for tabletop games.\n\nThe box opens into dice storage and a lined rolling tray. The life counter, a counter for tracking a value during play, uses rotating numbered rings.\n\n## Dice box layout \n\nOne half of the box holds the dice; the other has a dark lining for rolling. The brown printed body has a textured lid, rounded corners, and inset closure pieces (parts set into the body to hold it closed).\n\nThe cover shows the brown box open during use. The gallery shows the brown box closed for storage and a mint-green variant open on wood, with black felt lining, four inset magnets, and its detached textured lid behind the base.\n\n## Box files and variants \n\nThe print files include:\n\n-   Separate tops and bottoms.\n-   A card-size box.\n-   Decorative variants.\n\nThe lining and closure pieces are added to the printed shell during assembly.\n\n## Rotating life counter \n\nEach digit is built into a separate printed ring. Turning a ring changes that digit while the others stay in place.\n\nEnd pieces hold the assembly together, and a pointer identifies the selected number.\n\nThe gallery shows loose gray rings and end pieces alongside an assembled multicolored counter and a light blue-grey marble-speckled counter. The marble-speckled build has an embossed arrow pointer on its left end and 20 selected on the right ring. The part designs also include a roller, indicator, and stand.\n\n## Fit and assembly notes \n\n### Box lid \n\nThe box needs a lid that closes cleanly around its base and lining.\n\n### Counter rings \n\nThe counter needs enough clearance (space between parts) for the rings to turn independently while staying held by the end pieces.\n\nCheck those fits before finishing the box or fully assembling a counter. Alternate part versions may need different matching pieces.\n\n## Photos and related work \n\nI also made [Catan organizers](<https://devanmetz.com/portfolio/catan-organizers/>) with card trays and covered cases for roads, settlements, and cities."
    },
    {
      "id": "printed-led-sculptures",
      "title": "Printed LED Sculptures",
      "url": "https://devanmetz.com/portfolio/printed-led-sculptures/",
      "markdownUrl": "https://devanmetz.com/portfolio/printed-led-sculptures/index.md",
      "collection": "portfolio",
      "description": "Printed frames turn light strips into geometric desk lights, wall pieces, and an open-frame color-changing sculpture.",
      "summary": "I made printed frames for a geometric desk light, a wall piece, and a larger open-frame sculpture. The frames hold the lighting along edges, corners, and diagonals, making the structure and light path part of the appearance.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "3D printing",
        "LEDs",
        "CAD",
        "Electronics"
      ],
      "projectDate": {
        "start": "2022"
      },
      "category": "Lighting & fabrication",
      "group": "hardware",
      "status": "Physical builds",
      "facts": [
        {
          "label": "My work",
          "value": "Printed frames, lighting integration, and assembly"
        },
        {
          "label": "Forms",
          "value": "Desk light, wall frame, and open geometric sculpture"
        },
        {
          "label": "Lighting",
          "value": "LED tubing and exposed light strips"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/led-sculpture.5a9-pjSJ_6AOQT.webp",
        "alt": "A glowing blue and white Ethereum-shaped desk light in a printed frame beside a clock and plant.",
        "caption": "A freestanding geometric light with LED tubing held by a printed frame.",
        "width": 1200,
        "height": 904
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/led-frame.C6wgeoRl_XQIRR.webp",
          "alt": "The unlit purple geometric light frame mounted on a wall, showing its channels and support structure.",
          "caption": "The light's frame with the LEDs off, making the printed structure easier to see.",
          "width": 1200,
          "height": 904
        },
        {
          "url": "https://devanmetz.com/_astro/led-polyhedron-build.Vc3COks7_Zrcs3r.webp",
          "alt": "An open geometric frame on a desk with exposed LED strips following its edges and diagonals.",
          "caption": "The larger geometric light during assembly, with the strips and wiring visible.",
          "width": 1200,
          "height": 904
        },
        {
          "url": "https://devanmetz.com/_astro/led-polyhedron.DW8j6Nhb_2nz8pz.webp",
          "alt": "An open-frame geometric sculpture glowing blue at its edges on a workbench.",
          "caption": "The assembled sculpture glowing blue. It also lights up red and green.",
          "width": 1200,
          "height": 904
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "reflector-lantern",
          "title": "Reflector Lantern",
          "url": "https://devanmetz.com/portfolio/reflector-lantern/",
          "markdownUrl": "https://devanmetz.com/portfolio/reflector-lantern/index.md"
        },
        {
          "id": "workshop-builds",
          "title": "From the Workbench",
          "url": "https://devanmetz.com/portfolio/workshop-builds/",
          "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md"
        },
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI made printed frames to hold LEDs in geometric shapes: a desk light shaped like the Ethereum emblem (the geometric symbol of the Ethereum blockchain network), a wall piece, and a larger open-frame sculpture.\n\nThe frames position the lights along edges, corners, and diagonals.\n\n## Desk and wall lights \n\n### Freestanding desk light \n\nThe desk light uses LED tubing, lighting enclosed in a tube, held in a freestanding printed frame. Its continuous lines follow the outline of the Ethereum emblem.\n\n### Wall-mounted frame \n\nThe wall version uses channels in the frame to position the lighting. With the LEDs off, the purple printed structure and its supports are visible.\n\n## Open-frame sculpture \n\nThe larger sculpture has LED strips, flexible bands carrying individual LEDs, running along the edges and diagonals of a three-dimensional frame. The strips and wiring remain exposed during assembly, with individual points of light tracing the structure when switched on.\n\nThe finished sculpture can light up blue, red, and green. The gallery shows it during assembly and illuminated in blue.\n\n## Structure and light placement \n\nThe smaller lights create a continuous outline, while the larger sculpture makes the separate LEDs part of the appearance. In both, the printed frame determines the path of the light and holds it in place through corners and intersections.\n\n## Related builds \n\nMy [Rust furnace lamps](<https://devanmetz.com/portfolio/rust-furnace-lamps/>) hide the light inside painted shells. These sculptures keep the structure and light path visible.\n\nI printed the frames, assembled the lights, and integrated the wiring. The small lights use the existing Ethereum emblem as their shape."
    },
    {
      "id": "reflector-lantern",
      "title": "Reflector Lantern",
      "url": "https://devanmetz.com/portfolio/reflector-lantern/",
      "markdownUrl": "https://devanmetz.com/portfolio/reflector-lantern/index.md",
      "collection": "portfolio",
      "description": "A 3D-printable RGBW lantern with a curved mirror that spreads light around it.",
      "summary": "I designed a small printable lantern with a ring of colored and white LEDs pointing into a curved mirror. The mirror spreads light around roughly a full circle, and the adjustable model lets me export individual parts and rerun the mirror simulation when I change its shape.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "3D printing",
        "OpenSCAD",
        "LEDs",
        "WLED",
        "Electronics"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Lighting & fabrication",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Parametric OpenSCAD design, STL exports, and mirror ray trace"
        },
        {
          "label": "Size",
          "value": "About 118 mm wide × 112 mm tall"
        },
        {
          "label": "Power",
          "value": "Two 18650 cells in parallel (1S2P), with a 5 V boost"
        }
      ],
      "actions": [
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/reflector-lantern"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/reflector-lantern-assembly.BTG68K3U_pao0i.webp",
        "alt": "Repository assembly render of Reflector Lantern showing the LED ring, translucent tube, top reflector, and base.",
        "caption": "Assembly render from the project repository, showing the lantern's modeled parts.",
        "width": 1200,
        "height": 1200
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/reflector-lantern-section.DwA_8NdL_Z2l0I02.webp",
          "alt": "Repository cross-section diagram showing the curved reflector above the LED ring and the battery space inside the base.",
          "caption": "Cross-section diagram from the project repository showing the reflector profile and internal layout.",
          "width": 1200,
          "height": 1000
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "printed-led-sculptures",
          "title": "Printed LED Sculptures",
          "url": "https://devanmetz.com/portfolio/printed-led-sculptures/",
          "markdownUrl": "https://devanmetz.com/portfolio/printed-led-sculptures/index.md"
        },
        {
          "id": "workshop-builds",
          "title": "From the Workbench",
          "url": "https://devanmetz.com/portfolio/workshop-builds/",
          "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md"
        },
        {
          "id": "rust-furnace-lamps",
          "title": "Rust Furnace Lamps",
          "url": "https://devanmetz.com/portfolio/rust-furnace-lamps/",
          "markdownUrl": "https://devanmetz.com/portfolio/rust-furnace-lamps/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed a small printable lantern around a 24-LED RGBW (red, green, blue, and white) ring pointing up into a curved mirror.\n\nThe mirror redirects light around roughly 360°, from about 30° below to 50° above the horizon. The model is about 118 mm wide and 112 mm tall; the images show the repository’s assembly render and cross-section diagram.\n\n## Model and printing \n\n### Adjustable model and exports \n\nI made the design parametric (adjustable through named dimensions and settings) in `lantern.scad`. Open it in OpenSCAD, a code-based 3D modeling program, using its Customizer settings panel or set `part` to export individual pieces.\n\nThe repository includes print-oriented STL exports (3D-printable mesh files) and `raysim.py`, a 2D mirror ray trace (a simulation of light paths) to rerun when changing the reflector geometry.\n\n### Print settings \n\nPLA and PETG are plastics commonly used in 3D printing. The parts use these settings:\n\n-   **Base:** PLA or PETG, top face down, without supports.\n-   **Reflector:** PLA, flange down, without supports, using 10–15% infill (the internal printed structure).\n-   **Diffuser:** clear or natural PETG, either the 1.6 mm wall tube or the vase-mode version with about a 0.8 mm line width. Vase mode prints a continuous single-wall shell.\n\nMeasure the battery holders and adjust their dimensions in the model before printing.\n\n### Mirror finish options \n\nThe mirror finish starts with sanding the curved surface from 240 to 800 grit (abrasive grades, with higher numbers giving a finer finish). The README lists these finishing options:\n\n-   Molotow Liquid Chrome over glossy black primer.\n-   Aluminized mylar or chrome vinyl cut into gores (tapered sections that fit a curved surface).\n-   Silver spray paint.\n\n## Electronics and assembly \n\n### Parts list \n\nThe parts list uses:\n\n-   An Adafruit 24-pixel NeoPixel RGBW ring or a compatible 66 mm SK6812/WS2812 ring. NeoPixel is Adafruit’s name for individually controllable LEDs; SK6812 and WS2812 are addressable LED families. RGBW adds a separate white channel to the red, green, and blue channels.\n-   An ESP32-C3 SuperMini (a small microcontroller board) running WLED (software for controlling LED lighting).\n-   Two 18650 cells (cylindrical rechargeable batteries) wired in parallel, called 1S2P: one series group containing two parallel cells.\n-   A USB-C charger with a 5 V boost converter, which raises voltage, rated for at least 2 A.\n-   A 12 mm latching button, which stays in its selected state after being pressed.\n-   A 300–470 Ω data resistor.\n-   A 470–1000 µF capacitor across the LED supply.\n\n### Assembly sequence \n\n1.  Flash WLED (install its software) over USB first.\n2.  Fit and hot-glue the ring into its pocket, then feed the wires through the slot.\n3.  Fit the button and wire the charger, controller, and holders.\n4.  Tape the holders onto the lid and close it with four M3×10 self-tapping screws.\n5.  Press the tube into the base groove and the reflector onto the tube; a dab of glue secures them.\n\nAn optional M4 eye bolt allows hanging.\n\n### Current and estimated runtime \n\nThe README gives:\n\n-   Up to about 1.4 A at full white.\n-   A recommended 1200 mA WLED current limit.\n-   Roughly 6–10 hours of estimated runtime at a cozy brightness with two approximately 3000 mAh cells. Milliamp-hours (mAh) express battery charge capacity.\n\n## Related builds \n\nMy [Printed LED Sculptures](<https://devanmetz.com/portfolio/printed-led-sculptures/>) use printed frames to position light along geometric shapes."
    },
    {
      "id": "rigid-heddle-loom-accessories",
      "title": "Rigid Heddle Loom Accessories",
      "url": "https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/",
      "markdownUrl": "https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/index.md",
      "collection": "portfolio",
      "description": "Printed heddle holders and reeds, from stepped support panels to modular sections assembled between wooden rails.",
      "summary": "I designed and printed holders and reeds for rigid heddle looms as product designs for my planned 3D-printing shop. The holders support one or more heddles, the slotted frames that guide the loom's threads. The reeds range from a small nine-slot piece to modular sections assembled between wooden rails. I revised each design through several versions; measurements should be checked against the loom before printing.",
      "date": null,
      "updated": "2026-10-15",
      "tags": [
        "3D printing",
        "CAD",
        "Product design",
        "Weaving"
      ],
      "projectDate": {
        "start": "2022-07",
        "end": "2022-10"
      },
      "category": "Weaving accessories",
      "group": "hardware",
      "status": "Physical builds",
      "facts": [
        {
          "label": "My work",
          "value": "CAD design, printing, and revisions"
        },
        {
          "label": "Objects",
          "value": "Panel holders, holder blocks, clip-on arms, and reeds"
        },
        {
          "label": "Print options",
          "value": "PLA or PETG; parts fit a 256 mm cube print volume"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/heddle-three-panel-pair.CDqXhDV__ZdokTF.webp",
        "alt": "A mirrored pair of flat tan printed panels, each with three stepped rows of hook-shaped slots and mounting holes along the inner edge.",
        "caption": "The three-row holder panels form a mirrored pair, with mounting holes along their inner edges.",
        "width": 1200,
        "height": 904
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/heddle-three-panel-stacked.2KZ3be_D_Z27ooOg.webp",
          "alt": "Two tan three-row holder panels laid one above the other, showing stepped hook-shaped slots and a line of mounting holes on each panel.",
          "caption": "The same panel pair laid out to show the slots and mounting holes.",
          "width": 1200,
          "height": 1594
        },
        {
          "url": "https://devanmetz.com/_astro/heddle-four-slot-pair.DePd4_5Y_2hmSzC.webp",
          "alt": "Two cream printed four-slot holder blocks lying side by side on a dark surface.",
          "caption": "A pair of four-slot holder blocks laid flat.",
          "width": 1200,
          "height": 904
        },
        {
          "url": "https://devanmetz.com/_astro/heddle-four-slot-standing.uohpiqf6_Z22lW6o.webp",
          "alt": "Two cream four-slot holder blocks standing upright facing each other on a dark surface, with screw holes visible.",
          "caption": "The four-slot blocks upright, showing their stepped supports and screw holes.",
          "width": 1200,
          "height": 571
        },
        {
          "url": "https://devanmetz.com/_astro/heddle-clip-on-hooks.CM_bgPH1_Z2oiXNz.webp",
          "alt": "Two tan printed holder arms with long top hooks for hanging over a loom frame and stepped hooks down their sides.",
          "caption": "Clip-on arms use a top hook to hang over the loom frame and side hooks to rest heddles.",
          "width": 1200,
          "height": 1240
        },
        {
          "url": "https://devanmetz.com/_astro/heddle-mini-reed.oKkf9LLu_ZuR1xe.webp",
          "alt": "A small narrow tan printed reed with nine slots and eyelets between them, lying on a light wood surface.",
          "caption": "The nine-slot mini reed measures about 54 by 104 by 4 mm.",
          "width": 1200,
          "height": 1594
        },
        {
          "url": "https://devanmetz.com/_astro/heddle-modular-reed.BvWTIoWB_KHcWu.webp",
          "alt": "A full-width rigid heddle made from several printed slotted modules held between two light wooden rails on a dark surface.",
          "caption": "Printed reed modules line up between wooden rails to make a full-width heddle.",
          "width": 1200,
          "height": 904
        },
        {
          "url": "https://devanmetz.com/_astro/heddle-reed-closeup.C5nL5Fzb_1ohslC.webp",
          "alt": "A low-angle close-up looking along the repeated slots and holes of a long assembled printed reed.",
          "caption": "A close view along the assembled reed's alternating slots and holes.",
          "width": 1200,
          "height": 904
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        },
        {
          "id": "ring-organizer",
          "title": "Ring and Jewelry Organizer",
          "url": "https://devanmetz.com/portfolio/ring-organizer/",
          "markdownUrl": "https://devanmetz.com/portfolio/ring-organizer/index.md"
        },
        {
          "id": "workshop-builds",
          "title": "From the Workbench",
          "url": "https://devanmetz.com/portfolio/workshop-builds/",
          "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed and printed these rigid heddle loom accessories as product designs for my planned 3D-printing shop.\n\nA rigid heddle, also called a reed, is a frame with alternating slots and holes for the warp threads, the threads running lengthwise through the loom. Lifting or lowering it opens a gap called the shed. The weft, the thread woven across the warp, passes through that gap.\n\nI worked on both the reeds and the holders that support them.\n\n## Stepped heddle holders \n\nHeddle holders, sometimes called heddle blocks, hold the heddle in its up, neutral, and down positions. Extra rows let a weaver use two or more heddles at once for more complex cloth.\n\nThe mirrored three-row panels are each about 145 by 115 by 14 mm, with a row of mounting holes along the inner edge. I also made four-slot holder blocks, about 100 by 115 by 14 mm, with screw holes.\n\nThe clip-on arms have a long top hook that hangs over the loom frame and stepped hooks down the side for resting heddles. These designs are about 83–91 mm wide, with hook and no-hook versions and two- and three-step variants.\n\n## Small and modular reeds \n\nThe mini reed is a narrow piece with nine slots and eyelets between them, about 54 by 104 by 4 mm.\n\nFor a full-width heddle, I made modular reed sections that line up between two wooden rails, with small printed end caps. Each section is about 112 by 100 by 13 mm.\n\nI made sections in more than one dent spacing. Dent means the number of slots plus holes per inch. My reed designs include 5, 8, and 10 dent variants, including a six-inch reed.\n\n## Revisions and loom fit \n\nI went through several computer-aided design (CAD) versions of each design. The files are printable in PLA or PETG on a Bambu Lab A1-sized bed, with a print volume of 256 mm in each direction.\n\nFit to a particular loom brand is not confirmed. Check the holder dimensions, mounting holes, and reed measurements against the loom before printing.\n\nThese sit alongside my [Catan organizers](<https://devanmetz.com/portfolio/catan-organizers/>) as printed product designs built around the objects they hold."
    },
    {
      "id": "ring-organizer",
      "title": "Ring and Jewelry Organizer",
      "url": "https://devanmetz.com/portfolio/ring-organizer/",
      "markdownUrl": "https://devanmetz.com/portfolio/ring-organizer/index.md",
      "collection": "portfolio",
      "description": "A 3D-printed jewelry organizer with three ring cones and a shallow tray for loose jewelry.",
      "summary": "I designed and printed this jewelry organizer with three ring cones at the back and a shallow tray at the front. My December 2020 photos show the physical print in use with rings and loose jewelry. The charcoal sample has KYLA recessed into its front face as a historical customization.",
      "date": null,
      "updated": "2026-10-15",
      "tags": [
        "3D printing",
        "CAD",
        "Product design",
        "Jewelry"
      ],
      "projectDate": {
        "start": "2020-12"
      },
      "category": "Jewelry organizers",
      "group": "hardware",
      "status": "Physical builds",
      "facts": [
        {
          "label": "My work",
          "value": "CAD design and printing"
        },
        {
          "label": "Objects",
          "value": "Three ring cones and a shallow jewelry tray"
        },
        {
          "label": "Print options",
          "value": "PLA or PETG; approximately 140 × 80 × 84 mm"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/ring-organizer.CI4xjVQ0_Z13xjla.webp",
        "alt": "A charcoal printed jewelry organizer with three ring cones at the back, a shallow front tray holding jewelry, and KYLA recessed into the front face.",
        "caption": "My December 2020 physical print in use, with KYLA lettering as a historical customization sample.",
        "width": 1200,
        "height": 900
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/ring-organizer-alt.DxcgWU0Y_1rA0BW.webp",
          "alt": "Another view of the charcoal jewelry organizer in use with rings and loose jewelry.",
          "caption": "A second December 2020 phone photo of the printed organizer in use.",
          "width": 1200,
          "height": 900
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        },
        {
          "id": "rigid-heddle-loom-accessories",
          "title": "Rigid Heddle Loom Accessories",
          "url": "https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/",
          "markdownUrl": "https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/index.md"
        },
        {
          "id": "workshop-builds",
          "title": "From the Workbench",
          "url": "https://devanmetz.com/portfolio/workshop-builds/",
          "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed this ring and jewelry organizer in CAD (computer-aided design) and made a physical print. Three conical holders sit in a row at the back, with a shallow tray at the front for loose jewelry.\n\nThe cones act as mandrels, or tapered spires that rings slip over. The tray gives other small jewelry a place to rest alongside the rings.\n\n## Printed sample and personalization \n\nMy December 2020 phone photos show the organizer in use with rings and jewelry. The sample is dark gray or charcoal, with visible layer lines from FDM (fused deposition modeling), a printing process that builds an object in layers of melted filament.\n\nThe front face has debossed lettering, meaning letters recessed into the surface, spelling KYLA. I show this as a historical customization sample; the photos document that personalized print rather than establish which versions are available.\n\n## Size and print options \n\nMy STL print file has an overall envelope of approximately 140 × 80 × 84 mm. It fits within a 256 mm cube print volume, such as the Bambu Lab A1’s, and can be printed in PLA or PETG.\n\nThis is another of my printed product designs built around the objects it holds, alongside my [Catan organizers](<https://devanmetz.com/portfolio/catan-organizers/>), [dice boxes and counters](<https://devanmetz.com/portfolio/tabletop-dice-boxes/>), and [rigid heddle loom accessories](<https://devanmetz.com/portfolio/rigid-heddle-loom-accessories/>)."
    },
    {
      "id": "dean-android-agent",
      "title": "Dean Android Agent",
      "url": "https://devanmetz.com/portfolio/dean-android-agent/",
      "markdownUrl": "https://devanmetz.com/portfolio/dean-android-agent/index.md",
      "collection": "portfolio",
      "description": "Dean turns an unrooted Android tablet into an always-on voice assistant for the living room.",
      "summary": "I built Dean to turn an Android tablet into an always-on living-room voice assistant that answers aloud and works with household devices, information, lists, and reminders. I can also text it from my phone, and follow-up voice questions share conversation context for three minutes. Wake-word detection and speech-to-text run on the tablet, while transcribed requests go to a model provider through OpenRouter.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Python",
        "Android",
        "Termux",
        "Voice assistant",
        "OpenRouter"
      ],
      "projectDate": {
        "start": "2026-10"
      },
      "category": "Voice assistants",
      "group": "tools",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Python voice assistant, LLM tool loop, wall dashboard, and Telegram texting"
        },
        {
          "label": "Platform",
          "value": "Unrooted Android tablet via Termux; tested on onn. 12-inch Tablet Pro (2024)"
        },
        {
          "label": "License",
          "value": "MIT"
        }
      ],
      "actions": [
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/dean-android-agent"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/dean-android-agent-cover.DZ-i8en-_Z10HjlB.webp",
        "alt": "Illustration of a tablet and microphone beside the Dean voice assistant name; not a screenshot.",
        "caption": "Illustration of Dean's Android tablet voice pipeline, not a screenshot.",
        "width": 1200,
        "height": 800
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "devbot",
          "title": "DevBot",
          "url": "https://devanmetz.com/portfolio/devbot/",
          "markdownUrl": "https://devanmetz.com/portfolio/devbot/index.md"
        },
        {
          "id": "derp",
          "title": "DERP",
          "url": "https://devanmetz.com/portfolio/derp/",
          "markdownUrl": "https://devanmetz.com/portfolio/derp/index.md"
        },
        {
          "id": "solar-trail-camera",
          "title": "Solar Cellular Trail Camera",
          "url": "https://devanmetz.com/portfolio/solar-trail-camera/",
          "markdownUrl": "https://devanmetz.com/portfolio/solar-trail-camera/index.md"
        }
      ],
      "contentMarkdown": "## What it does \n\nI built Dean as an always-on voice assistant that runs on an unrooted Android tablet, without unlocking administrator-level access to Android.\n\nSay **“hey dean”**, ask a question, and it can answer aloud or use tools (functions that perform actions or retrieve information) to work with household devices, information, lists, and reminders. Conversations carry over for three minutes so follow-up questions can use the same context.\n\nI built and tested it on an **onn. 12” Tablet Pro (2024, model 100146663)** with a MediaTek Helio G99, 6 GB RAM, and Android 14.\n\nThe Python assistant runs through Termux, an Android terminal and Linux tool environment, and a Debian environment, which provides tools from the Debian Linux distribution.\n\n## Voice pipeline \n\nThe voice pipeline has four stages:\n\n1.  The microphone feeds **Vosk**, a speech-recognition engine, for the wake word—the phrase that activates listening for a request.\n2.  **Moonshine**, a speech-recognition model, handles speech-to-text, converting speech into a transcript.\n3.  The transcript goes to an LLM (large language model) with tools through **OpenRouter**, a service that routes requests to model providers. The tool loop lets it use their results while answering. The response is streamed back, arriving in pieces as it is generated.\n4.  **Piper**, a text-to-speech engine, speaks it sentence by sentence as the answer arrives.\n\nWake-word detection and speech-to-text run on the tablet.\n\n### Measured response times \n\nThe README’s latency table (the delay before a response) reports measurements on that onn. tablet, counted from when speaking stops:\n\n-   **End-of-speech detection:** 0.7 seconds.\n-   **Moonshine transcription:** 0.3 seconds for a four-second command.\n-   **LLM’s first words:** about 1.0 second on a simple question.\n-   **Weather or sensor question:** about 2.5–3.2 seconds with one tool round.\n-   **Piper’s first audio:** 0.2–0.7 seconds after a sentence arrived.\n\nThese are the README’s measured numbers on that tablet, not response-time guarantees.\n\n## Tools it can use \n\nThe model decides when to call tools.\n\n### Information, lists, and memory \n\nThe information tools cover:\n\n-   Web search.\n-   Open-Meteo weather and US National Weather Service alerts.\n-   NPR or topic-based news headlines.\n-   Private iCal calendars (a format for sharing calendar events).\n-   OpenRouter spending.\n\nNamed lists are shared between voice and text.\n\nTimers, reminders, alarms, and plain-English routines are saved so they survive restarts. Long-term memory is stored on the tablet.\n\n### Household devices and phone finding \n\nFor the home, Dean can:\n\n-   Control Govee lights over LAN (the local area network) or the cloud API (an interface for communicating with a remote service).\n-   Operate a Roku TV over the local network.\n-   Read Govee Bluetooth thermometers through a companion app.\n-   Check household phone presence on Wi-Fi.\n-   Deliver reminders when someone gets home.\n-   Ring a Pixel through the Dean Finder app or an iPhone through an email-triggered Shortcut, including when the phones are on silent.\n\n### Tablet controls and camera \n\nTermux:API, an add-on that exposes Android device features to Termux, provides location, tablet sensors, battery and device status, volume, brightness, and flashlight controls.\n\nThe camera tool takes a photo to answer a question, chimes when used, and deletes the photo right away.\n\n## Wall dashboard and texting \n\n### Wall display \n\nThe full-screen Termux dashboard shows a clock, date, weather and forecast, light status, the next three reminders or alarms, and recent conversation. A status bar shows what Dean is doing.\n\n### Text conversations \n\nA Telegram bot (an automated account on the Telegram messaging service) lets me text Dean from a phone.\n\nOnly allowed chats can use it, and each chat keeps its own conversation.\n\nReminders set by text are sent back by text, while the `announce` tool speaks at home. Camera access over text is disabled by default.\n\n## Limits and setup \n\n### Foreground audio and reboot recovery \n\n**Termux must stay on screen:** Android silences the microphone for background apps. Dean can recover its audio once Termux is back in front.\n\nTermux:Boot, an add-on for running scripts after Android starts, also runs only after the first unlock following a reboot; unattended recovery from a power cut needs no lock or swipe-to-unlock.\n\n### Battery care \n\nA tablet plugged in around the clock can develop a swollen battery over time. The README recommends a charge limit where available or a smart plug on a schedule.\n\n### Requests sent to model providers \n\nWake-word detection and speech-to-text stay on the device, but transcribed requests go to the chosen model provider through OpenRouter. Photos also go through OpenRouter when I ask Dean to look at something.\n\n### Setup and source \n\nThe public [GitHub README](<https://github.com/DevanMetz/dean-android-agent#readme>) documents Termux setup, API configuration, companion apps, and integration requirements. The code is MIT licensed. There are no GitHub releases yet, so the project action links to source."
    },
    {
      "id": "opensnip",
      "title": "OpenSnip",
      "url": "https://devanmetz.com/portfolio/opensnip/",
      "markdownUrl": "https://devanmetz.com/portfolio/opensnip/index.md",
      "collection": "portfolio",
      "description": "A fast Windows screenshot and screen recording tool with region capture, annotations, pinned images, and MP4 recording.",
      "summary": "I built OpenSnip to capture screenshots and screen recordings on Windows with keyboard shortcuts. I can annotate captures, keep pinned images above other windows, and record part of the screen with optional system sound and microphone audio.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "C#",
        ".NET",
        "Windows",
        "Screen capture"
      ],
      "projectDate": {
        "start": "2026-10"
      },
      "category": "Desktop software",
      "group": "tools",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "C#/.NET application, capture workflow, annotation editor, and screen recording"
        },
        {
          "label": "Platform",
          "value": "Windows 10 and 11"
        },
        {
          "label": "License",
          "value": "MIT"
        }
      ],
      "actions": [
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/OpenSnip"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/opensnip-cover.D7z1p2IX_267J0W.webp",
        "alt": "Illustration using the OpenSnip app icon to represent the Windows screenshot and screen recording tool.",
        "caption": "Illustration using the OpenSnip icon. Snip and record shortcuts are listed on the cover.",
        "width": 1200,
        "height": 800
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "openmic",
          "title": "OpenMic",
          "url": "https://devanmetz.com/portfolio/openmic/",
          "markdownUrl": "https://devanmetz.com/portfolio/openmic/index.md"
        },
        {
          "id": "featherclick",
          "title": "FeatherClick",
          "url": "https://devanmetz.com/portfolio/featherclick/",
          "markdownUrl": "https://devanmetz.com/portfolio/featherclick/index.md"
        },
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        }
      ],
      "contentMarkdown": "## What it does \n\nOpenSnip is a screenshot and screen recording tool I built for Windows in C# and .NET. Press **Win+Shift+S** or **PrtSc**, drag a region or click a window, and the capture is on the clipboard and optionally saved to disk. **Win+Shift+R** records a video of any part of the screen, with system sound and microphone if you want them.\n\nI built it as a small tray app (an app accessed from the system tray near the Windows clock) that freezes the screen the moment the shortcut fires, so menus and tooltips stay put while you select. Mixed-DPI (screens with different display scaling) and multi-monitor (more than one screen) setups are supported.\n\nThere is no installer; the app is about 1.5 MB and lives in the system tray.\n\n## Capture and annotation \n\n### Overlay selection \n\nThe overlay freezes the display, then you drag a region, click a highlighted window, press **F** for the monitor under the cursor, or **A** for all monitors on still captures. A magnifier shows pixel coordinates and color while lining up edges.\n\nCaptures copy to the clipboard as PNG (a lossless image format) so pasting into Discord, Slack, browsers, and documents keeps full quality. Optional auto-save writes PNG or JPG (a compressed image format) to a chosen folder, with monthly subfolders if you want them.\n\n### Editor and pinned images \n\nAfter a snip, a small corner preview can open the editor or be dragged into another app as a file.\n\nThe editor includes arrows, boxes, pen, highlighter, text, and pixelation (covering detail with blocks) for redaction (hiding sensitive information), with undo and redo. Every edit updates the clipboard automatically.\n\nPinned images stay on top of other windows; they can be moved, resized, zoomed, and faded.\n\n### Extra shortcuts \n\nOther capture shortcuts include a delayed snip (**Shift+PrtSc** by default) and re-capturing the last region (**Ctrl+PrtSc**). Every shortcut can be changed in settings.\n\n## Screen recording \n\n**Win+Shift+R** starts or stops recording. Selection works like a screenshot: area, window, or one monitor. An optional countdown comes first, then a red outline and a small control bar for pause, microphone mute, stop, and discard. The outline and bar are excluded from the recorded video.\n\nRecordings use hardware-accelerated H.264 (a video compression format) in an MP4 file (a container for video and audio). Settings cover system sound, microphone device, 30 or 60 fps (frames per second), quality, cursor visibility, and countdown. Stopping saves to `Videos\\OpenSnip` by default and copies the file to the clipboard. Clicking the recording preview plays it.\n\n## Settings and install \n\nThe tray menu opens settings for capture, saving, recording, shortcuts, and general behavior, including optional start with Windows. Changes apply immediately.\n\nTo build from source on Windows with the .NET 9 SDK (software development kit):\n\n```\ngit clone https://github.com/DevanMetz/OpenSnip\ncd OpenSnip/src\ndotnet publish -c Release -p:PublishSingleFile=true -o ../dist\n```\n\nThat produces `dist/OpenSnip.exe`, which needs the .NET 9 Desktop Runtime (the components needed to run .NET desktop apps). A self-contained publish (a build that includes the runtime) produces a larger standalone executable.\n\nThe source is available under the MIT license; see the [README](<https://github.com/DevanMetz/OpenSnip#readme>) for the current setup notes. There are no GitHub releases yet, so the project action links to the repository.\n\n## Limits \n\nOpenSnip requires 64-bit Windows 10 (version 2004 or later) or Windows 11. Screen recording also needs the Microsoft Visual C++ Redistributable for x64 (runtime libraries for 64-bit applications). All-monitor capture is available for screenshots; recording selects an area, window, or a single monitor.\n\nIf Print Screen still opens Windows Snipping Tool, turn off **Use the Print screen key to open screen capture** under Windows **Settings → Accessibility → Keyboard**. If another app grabs a shortcut first, pick a different combination in OpenSnip. `Alt+PrtSc` and `Win+PrtSc` keep their normal Windows behavior.\n\nFor troubleshooting, `OpenSnip.exe --diagnostics` writes a display, audio-device, and recorder-start report to `%APPDATA%\\OpenSnip\\diagnostics.txt`."
    },
    {
      "id": "technical-writing",
      "title": "Technical Writing for All3DP",
      "url": "https://devanmetz.com/portfolio/technical-writing/",
      "markdownUrl": "https://devanmetz.com/portfolio/technical-writing/index.md",
      "collection": "portfolio",
      "description": "Published guides to Arduino, CNC, and 3D-printing software, grounded in hands-on maker work.",
      "summary": "I wrote maker guides for All3DP covering Arduino, CNC hardware, and 3D-printing software. I also wrote a separate functional CAD tutorial that takes measurements of USB drives and a card reader through modeling to a printed organizer, giving readers a concrete result to compare with the original need.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "All3DP",
        "Arduino",
        "CNC",
        "Tutorials"
      ],
      "projectDate": {
        "start": "2020"
      },
      "category": "Writing & documentation",
      "group": "writing",
      "status": "Published articles",
      "facts": [
        {
          "label": "My work",
          "value": "Published maker guides and a functional CAD tutorial"
        },
        {
          "label": "Subjects",
          "value": "Arduino · CNC · 3D-printing software"
        },
        {
          "label": "Publication",
          "value": "All3DP"
        }
      ],
      "actions": [
        {
          "label": "Read an All3DP guide",
          "url": "https://all3dp.com/2/arduino-beginners-get-started/"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/cad-tutorial.U_WNtE6c_PSMIQ.webp",
        "alt": "A blue printed organizer holding USB drives and a card reader on a cutting mat.",
        "caption": "A build photograph from my functional-CAD tutorial sample, separate from the published articles below.",
        "width": 1200,
        "height": 950
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "openfloat-archery",
          "title": "OpenFloat Archery",
          "url": "https://devanmetz.com/portfolio/openfloat-archery/",
          "markdownUrl": "https://devanmetz.com/portfolio/openfloat-archery/index.md"
        },
        {
          "id": "goop-troops",
          "title": "Goop Troops",
          "url": "https://devanmetz.com/portfolio/goop-troops/",
          "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md"
        },
        {
          "id": "diy-electric-bike",
          "title": "DIY Electric Bike",
          "url": "https://devanmetz.com/portfolio/diy-electric-bike/",
          "markdownUrl": "https://devanmetz.com/portfolio/diy-electric-bike/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI’ve written maker guides for All3DP (an online 3D-printing publication) covering Arduino (a platform for programmable electronics), CNC (computer numerical control) hardware, and 3D-printing software.\n\nI’ve also written a functional CAD (computer-aided design) tutorial built around a printed organizer for USB drives and a card reader.\n\n## Published articles \n\nThe Arduino beginner guide introduces microcontrollers (small computers that control electronics), the Uno (an Arduino board), and a first Blink sketch (a program that blinks an LED). The CNC hardware guide covers a CNC shield (an add-on board for connecting CNC hardware to an Arduino). Cura (a slicer that prepares 3D models for printing) is the subject of the Linux walkthrough.\n\n| Article | Subject and date |\n| --- | --- |\n| [Arduino for Beginners: How to Get Started](<https://all3dp.com/2/arduino-beginners-get-started/>) | Introduction to microcontrollers, the Uno, and a first Blink sketch; August 15, 2020 |\n| [Arduino CNC Shield: Buyer’s Guide](<https://all3dp.com/2/arduino-cnc-shield-buyers-guide/>) | What the shield does and how controller options differ; August 1, 2020 |\n| [Cura & Linux: How to Make Them Work Together](<https://all3dp.com/2/cura-linux-how-to-make-them-work-together/>) | Installation and first-use walkthrough; June 30, 2020 |\n| [CNC 3018 (Pro) Software: The Best Programs](<https://all3dp.com/2/cnc-3018-pro-software-x-best-programs/>) | CNC software guide credited to Devan Metz and Guilherme Schendel; updated September 24, 2024 |\n\nThese links go to the articles on All3DP, where later editorial updates may have changed the original text. Product recommendations and software steps reflect their publication dates.\n\n## Functional CAD tutorial \n\nMy functional CAD tutorial walks through making an organizer for USB drives and a card reader. It starts with measurements, develops the shape through sketches and modeling, and ends with a printed part holding the devices.\n\nThe blue organizer in the cover photo is from this separate tutorial sample.\n\n## Explaining through an object \n\nThe tutorial’s sequence gives a reader a concrete path through the subject:\n\n1.  Identify the physical objects that need organizing.\n2.  Measure the dimensions that constrain the holder.\n3.  Turn those measurements into sketches and model geometry.\n4.  Show the intermediate modeling work.\n5.  Compare the printed result with the original need.\n\nThe finished organizer gives the reader a way to compare the dimensions in the model with the fit of the real devices.\n\n## Writing approach \n\nThese guides explain a subject through a specific task: running a first Arduino sketch, choosing CNC controller hardware, or turning measurements into a part that fits. Each step gives the reader something concrete to try or compare.\n\nFor a related example with editable dimensions and printing notes, see [Parametric FreeCAD Designs](<https://devanmetz.com/portfolio/parametric-freecad/>)."
    },
    {
      "id": "featherclick",
      "title": "FeatherClick",
      "url": "https://devanmetz.com/portfolio/featherclick/",
      "markdownUrl": "https://devanmetz.com/portfolio/featherclick/index.md",
      "collection": "portfolio",
      "description": "A Rust autoclicker with adjustable timing, target spread, hotkeys, and a headless mode.",
      "summary": "I built FeatherClick in Rust to repeat mouse clicks from a desktop window or the command line. I can choose the timing, target position and spread, and stop conditions, with downloads for Windows, macOS, and Linux and platform-specific limits on clicking and hotkeys.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Rust",
        "egui",
        "Desktop software",
        "Automation"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Desktop software",
      "group": "tools",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Click engine, desktop interface, CLI, and platform integration"
        },
        {
          "label": "Platform",
          "value": "Windows · macOS · Linux"
        },
        {
          "label": "Release",
          "value": "v0.1.0 · September 2026"
        }
      ],
      "actions": [
        {
          "label": "Download FeatherClick",
          "url": "https://github.com/DevanMetz/featherclick/releases/tag/v0.1.0"
        },
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/featherclick"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/featherclick-cover.BdY47Lpc_1wWLjA.webp",
        "alt": "FeatherClick's blue click icon with the project's name, timing modes, and supported operating systems.",
        "caption": "Illustration using the FeatherClick icon. Fixed, uniform, and Gaussian are the three timing modes.",
        "width": 1200,
        "height": 800
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "openmic",
          "title": "OpenMic",
          "url": "https://devanmetz.com/portfolio/openmic/",
          "markdownUrl": "https://devanmetz.com/portfolio/openmic/index.md"
        },
        {
          "id": "devbot",
          "title": "DevBot",
          "url": "https://devanmetz.com/portfolio/devbot/",
          "markdownUrl": "https://devanmetz.com/portfolio/devbot/index.md"
        },
        {
          "id": "opensnip",
          "title": "OpenSnip",
          "url": "https://devanmetz.com/portfolio/opensnip/",
          "markdownUrl": "https://devanmetz.com/portfolio/opensnip/index.md"
        }
      ],
      "contentMarkdown": "## What it does \n\nFeatherClick is a small mouse autoclicker (a tool that repeats mouse clicks) I built in Rust. The same click engine runs from a desktop window or in headless mode (without a desktop window) from the command line. The [v0.1.0 release](<https://github.com/DevanMetz/featherclick/releases/tag/v0.1.0>) provides downloads for Windows, macOS, and Linux.\n\nThe window keeps the main controls together: which button to click, when to click it, where to aim, and when to stop. Global hotkeys can start or stop a run and capture a position where the platform supports them.\n\n## Timing and targeting \n\nThe timing modes are:\n\n-   **Fixed:** the same requested interval between clicks.\n-   **Uniform:** randomly chosen intervals between two limits.\n-   **Gaussian:** intervals sampled from a Gaussian distribution (a bell-shaped distribution centered around a mean).\n\nA click can use the pointer’s current position or return to a selected point. For a selected point, the target can vary within a box, ellipse, or Gaussian spread.\n\nThe engine supports left, right, and middle buttons, including single clicks, double clicks, and press-and-hold. A run can stop:\n\n-   After a set number of clicks.\n-   After a time limit.\n-   When the pointer moves away from its starting point.\n\nThese controls also make short, bounded runs practical.\n\n## How it is built \n\nThe app uses Rust, `egui`/`eframe` (a user-interface library and its app framework) for its window, and platform input and hotkey libraries. The engine takes a snapshot of the settings at the start of each run, so changing a control in the window does not alter a run already in progress.\n\nEdited settings are saved to a local JSON (a structured text format) file after a short delay, with an atomic write (replacing the saved file in one operation) to avoid truncating the file if saving is interrupted.\n\nThe click loop sleeps in short slices and uses a brief spin near the next click. On Windows it raises timer resolution (how finely the system schedules timed waits) while a run is active. In the project’s Windows 11 measurements, a requested 100 ms fixed interval produced click intervals of 96–109 ms. Those numbers describe that test setup, not a guarantee across machines.\n\nThe command-line mode uses the same engine for scripts or environments without a desktop window. The source includes tests and continuous integration builds for the three supported operating systems.\n\n## Platform limits \n\nWindows supports the window, clicking, and global hotkeys. macOS needs Accessibility permission (macOS authorization to control input) to send clicks; its release is unsigned and not notarized (checked by Apple for malicious software). Linux supports X11 (a Linux display system) and some Wayland (a newer display protocol) compositors (software that manages windows and display output).\n\nOn Wayland, global hotkeys are unavailable, and native clicking depends on the compositor’s virtual-pointer support.\n\nFeatherClick does not record macros or automate keyboard input. The [README](<https://github.com/DevanMetz/featherclick#readme>) has the current setup notes, platform table, command-line examples, and limitations. The source is available under the MIT license."
    },
    {
      "id": "market-atlas",
      "title": "Market Atlas",
      "url": "https://devanmetz.com/portfolio/market-atlas/",
      "markdownUrl": "https://devanmetz.com/portfolio/market-atlas/index.md",
      "collection": "portfolio",
      "description": "A public market research workspace for U.S. sectors, asset comparisons, correlations, and portfolio simulations.",
      "summary": "I built Market Atlas to compare markets, research stocks and sectors, and simulate portfolios without an account or brokerage connection. Research settings can be shared by URL, while watchlists, portfolio settings, and saved views stay in the visitor's browser.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "TypeScript",
        "React",
        "Cloudflare Workers",
        "Market data"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Web tools",
      "group": "tools",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Market research interface, charts, and Worker APIs"
        },
        {
          "label": "Stack",
          "value": "React · TypeScript · Recharts · Cloudflare Workers"
        },
        {
          "label": "License",
          "value": "MIT"
        }
      ],
      "actions": [
        {
          "label": "Open the workspace",
          "url": "https://market-atlas.metzdevan.workers.dev"
        },
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/market-atlas"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/market-atlas.kS8C3GJl_Z1nJ0wf.webp",
        "alt": "Market Atlas Market overview showing five benchmark quote cards, a performance comparison chart, sector landscape tiles, and a sector leaderboard.",
        "caption": "Screenshot of the live site's Market overview, captured October 13, 2026. Prices are Yahoo Finance data from that day and may be delayed.",
        "width": 1200,
        "height": 1374
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "agent-message-board",
          "title": "Agent Message Board",
          "url": "https://devanmetz.com/portfolio/agent-message-board/",
          "markdownUrl": "https://devanmetz.com/portfolio/agent-message-board/index.md"
        },
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        },
        {
          "id": "goop-troops",
          "title": "Goop Troops",
          "url": "https://devanmetz.com/portfolio/goop-troops/",
          "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md"
        }
      ],
      "contentMarkdown": "## What it does \n\nI built Market Atlas as a public market research workspace with React, TypeScript, Recharts, and Cloudflare Workers. One Worker (code running on Cloudflare’s servers) serves the APIs (interfaces for requesting data) and built static assets (the site’s files). The default configuration needs no account, brokerage connection, or paid API key.\n\nWatchlists, portfolio settings, and saved research views stay in the visitor’s browser in `localStorage` (storage kept by the browser on that device).\n\n## Research tools \n\n### Market and sector views \n\nThe Market overview brings together:\n\n-   Five benchmark (reference asset) ETF (exchange-traded fund) quotes.\n-   Cumulative return (growth over time), relative return (performance against a benchmark), and drawdown (decline from a previous peak) charts.\n-   All 11 U.S. sector ETFs and sector leaders.\n\nSector explorer adds absolute and benchmark-relative heatmaps (color-coded comparisons), return tables, and a relative momentum scatter plot (a comparison of recent performance against a benchmark).\n\n### Asset comparisons and screening \n\nCompare assets puts up to 12 stocks, sectors, or global assets alongside a benchmark, with returns, excess returns (returns above the benchmark), drawdowns, and hypothetical growth.\n\nThe Stock screener covers 54 selected U.S. large-cap stocks (companies with large market values) with sector and technical filters (filters based on price patterns); symbol search can find supported assets beyond that catalog.\n\n### Trends, risk, and correlations \n\nTrend lab shows rolling returns (returns over a moving time window), volatility (how much returns vary), correlation (how returns move together), beta (sensitivity to benchmark returns), monthly return calendars, and descriptive seasonal averages.\n\nRisk lab compares drawdowns on shared dates and separates completed recoveries from ongoing episodes.\n\nCorrelations shows pairwise daily-return matrices (tables comparing each pair of assets) for sectors, global assets, or the selected comparison.\n\n### Portfolio simulations \n\nPortfolio lab runs USD-only (U.S. dollar) simulations with custom starting capital, buy and hold (keeping the initial holdings) or monthly, quarterly, or yearly rebalancing (resetting holdings to the chosen proportions), allocation and profit-contribution breakdowns, and risk measures.\n\nThe Watchlist keeps saved tickers (asset symbols) on the device.\n\n### Dates, saved views, and exports \n\nResearch windows use preset horizons (time spans) or custom dates. Historical technical indicators (measures based on past prices) stop at the selected end date; quote cards remain current.\n\nShareable research URLs preserve the selected research settings, and up to 20 named views can be saved locally. Watchlists and portfolio settings remain separate device settings and are not included in shared URLs.\n\nCSV (comma-separated values) exports cover research tables, simulations, watchlists, and news.\n\n## News and sentiment \n\n### Headline search \n\nThe RSS/Atom (web feed formats) headline directory lists 132 publishers and institutions, with 55 checked feeds. Headline search supports words, phrases, alternatives, exclusions, and field filters over the retrieved headlines.\n\n### How sentiment scores work \n\nSentiment uses a transparent English lexicon heuristic (a rule-based score using a list of words). It is not an AI forecast or a trained finance model: scores describe headline word choice, not likely returns. Matched cues, scoring coverage, and sample sizes make the calculation inspectable.\n\nNews charts describe the currently retrieved snapshot, not a historical archive. Feed entries are reduced to headline, original URL, publisher, and publication time; no full articles are scraped.\n\n## Data and limits \n\n### History and caching \n\nMarket data comes from Yahoo Finance’s public unofficial chart and search endpoints (API addresses), with no guaranteed service level. This is a research feed, not an execution-quality real-time feed. It provides up to five years of daily history; successful histories are cached at the Cloudflare edge (servers near visitors) and stay fresh for 15 minutes.\n\n### Price adjustments \n\nReturns use adjusted closes (closing prices adjusted for events such as stock splits and dividends) when the entire series supports them. Incomplete adjustment falls back to closes for the whole series with a warning. Missing prices are never fabricated or forward-filled (replaced with the last known price).\n\n### Coverage and permitted use \n\nThe screener catalog is curated rather than a complete stock universe. The application provides no structured fundamentals (company financial measures), options data (contracts to buy or sell at a set price), brokerage actions, or intraday streaming (continuous updates during the trading day). The MIT license covers my code, not third-party market data, which remains subject to provider terms.\n\nMarket Atlas is for research and education only, not investment advice. Historical results do not guarantee future performance."
    },
    {
      "id": "devbot",
      "title": "DevBot",
      "url": "https://devanmetz.com/portfolio/devbot/",
      "markdownUrl": "https://devanmetz.com/portfolio/devbot/index.md",
      "collection": "portfolio",
      "description": "A Python terminal coding assistant with file tools, saved sessions, specialist agents, and plan-based automation.",
      "summary": "I built DevBot to work on code from the terminal: I describe a task, and a language model can read files, propose edits, and run tools. It connects to DeepSeek or a local model server, saves conversations for later, and includes specialist agents and a mode that works through a written plan.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Python",
        "CLI",
        "AI",
        "Automation"
      ],
      "projectDate": {
        "start": "2026-06"
      },
      "category": "Developer tools",
      "group": "tools",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Python CLI, model tool loop, specialist agents, and plan runner"
        },
        {
          "label": "Runtime",
          "value": "Python 3.10 or later"
        },
        {
          "label": "License",
          "value": "MIT"
        }
      ],
      "actions": [
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/DevBot"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/devbot-cover.C-iRqYHD_20aRN6.webp",
        "alt": "Terminal screenshot of DevBot's real --help output, listing its model, provider, swarm, resume, and run-plan options.",
        "caption": "DevBot's command-line help, captured from the CLI and rendered as a terminal image.",
        "width": 1200,
        "height": 800
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "dean-android-agent",
          "title": "Dean Android Agent",
          "url": "https://devanmetz.com/portfolio/dean-android-agent/",
          "markdownUrl": "https://devanmetz.com/portfolio/dean-android-agent/index.md"
        },
        {
          "id": "featherclick",
          "title": "FeatherClick",
          "url": "https://devanmetz.com/portfolio/featherclick/",
          "markdownUrl": "https://devanmetz.com/portfolio/featherclick/index.md"
        },
        {
          "id": "derp",
          "title": "DERP",
          "url": "https://devanmetz.com/portfolio/derp/",
          "markdownUrl": "https://devanmetz.com/portfolio/derp/index.md"
        }
      ],
      "contentMarkdown": "## What it does \n\nI built DevBot as a CLI (command-line interface) coding assistant. I can give it a single task, such as fixing a failing test, or keep an interactive conversation open in the terminal. It uses DeepSeek by default and can also connect to a local llama.cpp model server through an OpenAI-compatible API (an interface for exchanging requests with a model).\n\nThe model has tools to read files, search the project, replace text, write files, run shell commands, and detect and run a project’s test suite. It can also search the web through DuckDuckGo. Conversations save in the project’s `.devbot` directory and can be resumed or exported as Markdown (plain text with formatting).\n\n## How it works \n\nDevBot runs an agentic loop (a cycle of model responses, tool calls, and tool results). The model requests an action, DevBot runs the tool, and the result goes back into the conversation. Streaming (showing output as it arrives) lets me follow the response in the terminal.\n\nFile tools check that resolved paths stay inside the project directory. This provides sandboxed edits (file changes restricted to that directory), rather than isolation of the entire process. Shell commands run from the project directory and use allow and block rules; commands outside those rules need approval. File writes, edits, and test-tool runs also prompt for approval in the normal interactive mode. Auto-approval and unattended modes change that behavior.\n\nSwarm mode (a manager delegating work to specialist agents) adds coder, reviewer, tester, and researcher roles. Its pipeline tool sends a task through implementation, review, and fixes. Megaswarm mode can run specialists in parallel and combine their findings through a reviewer. These are model-driven workflows; a review result is not a guarantee that the code is correct.\n\n## Working through a plan \n\nWith `--run-plan`, I can give DevBot a Markdown plan containing `## Phase` sections. It works through those phases in order, runs the tests after each phase, and attempts a fix if tests fail. If that fix still fails, it stops and leaves the changes for review. This mode runs unattended with auto-approval and leaves its changes uncommitted.\n\nThe repository also implements `--auto-evolve`: a planner proposes phases, a critic filters them, and the driver implements and tests the accepted work. It requires a clean working tree, refuses to run on `main` or `master`, and commits passing phases on the work branch. Phase, time, and token limits can bound the run; pushing the branch is opt-in. The included `plan.md` describes the design, and the source and tests cover the driver and its stop conditions.\n\n## How it is built \n\nI built DevBot in Python with the OpenAI Python client for model requests and `ddgs` for web search. It tracks token usage (the units models use to measure text), estimates cost from configured model pricing, and can compress conversation context for longer sessions.\n\nThe test suite covers file-path checks, command rules, session persistence, provider selection, plan execution, and the auto-evolve controls. The GitHub Actions workflow is configured to run pytest (Python’s test runner) on Linux, Windows, and macOS with Python 3.10 through 3.13. That configuration describes the intended test matrix, not a claim about every run’s result.\n\n## Where to get it \n\nThe [source repository](<https://github.com/DevanMetz/DevBot>) includes setup instructions, command examples, configuration options, and tests. From a local checkout, install it with Python 3.10 or later:\n\n```\npip install -e .\ndevbot \"fix the failing test\"\n```\n\nThe default provider needs a DeepSeek API key. For a running local model server, use `devbot --local` and the connection settings in the [README](<https://github.com/DevanMetz/DevBot#readme>). The project action links to the source repository. DevBot is available under the MIT license."
    },
    {
      "id": "spritefield",
      "title": "Spritefield",
      "url": "https://devanmetz.com/portfolio/spritefield/",
      "markdownUrl": "https://devanmetz.com/portfolio/spritefield/index.md",
      "collection": "portfolio",
      "description": "A searchable, seemingly endless canvas of game assets, with APIs that help both people and agents find usable files.",
      "summary": "I built Spritefield to help people and agents search more than 12,700 game-asset entries, from individual files to complete packs. The visual catalog connects previews to original files and source and license details so I can judge whether an asset fits a project.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "TypeScript",
        "SQLite FTS5",
        "Asset search",
        "WebMCP"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Web tools",
      "group": "tools",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Catalog, search interface, and asset APIs"
        },
        {
          "label": "Stack",
          "value": "TypeScript · React · Cloudflare D1"
        },
        {
          "label": "Collection",
          "value": "Sprites, icons, textures, models, and packs"
        }
      ],
      "actions": [
        {
          "label": "Explore the catalog",
          "url": "https://spritefield-metzd.devanmetz.chatgpt.site"
        },
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/spritefield"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/spritefield.YLMcvebg_Z11w5wh.webp",
        "alt": "Spritefield's searchable canvas showing game sprites, icons, textures, and models.",
        "caption": "The live Spritefield catalog. Preview artwork belongs to the credited asset creators and providers.",
        "width": 1200,
        "height": 675
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "market-atlas",
          "title": "Market Atlas",
          "url": "https://devanmetz.com/portfolio/market-atlas/",
          "markdownUrl": "https://devanmetz.com/portfolio/market-atlas/index.md"
        },
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        },
        {
          "id": "agent-message-board",
          "title": "Agent Message Board",
          "url": "https://devanmetz.com/portfolio/agent-message-board/",
          "markdownUrl": "https://devanmetz.com/portfolio/agent-message-board/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built Spritefield as a visual search tool for game assets. Its canvas keeps previews close to the act of searching: type a query, filter by asset type, and bring relevant results into view.\n\nThe catalog includes more than 12,700 entries across individual assets and complete packs.\n\n## Browsing and search \n\n### Loading a large catalog \n\nA virtualized grid (a grid that draws only the visible items) renders the visible part of the canvas. Lazy previews (images loaded as needed), cursor pagination (using a returned marker to fetch the next results), and continuous scrolling let a large collection remain browsable without placing every image on the page at once.\n\n### Search matches and suggestions \n\nSearch uses weighted SQLite FTS5 indexing (full-text search in a SQLite database, with some fields given more importance), related terms, and typo handling. When the system falls back to discovery suggestions, the interface labels that mode. A suggested asset should not be mistaken for a literal match to every term in a query.\n\nThe canvas loops back through the catalog when you reach the end.\n\n## From a preview to usable files \n\n### Textures \n\nThe texture API (an interface for requesting asset details) distinguishes a preview from the original resource. It can resolve original photographs, archives, and separate PBR maps (physically based rendering images that describe material properties) with their role, format, and available resolution.\n\n### Models and companion files \n\nThe model API returns the main model file and companion resources, or a complete pack archive. For a glTF model (a format for delivering 3D scenes and models), the companion buffer (geometry data) and texture files need to retain the relative paths described by the response.\n\nChoosing a different model resolution refers to texture size, rather than a different mesh complexity (the amount of geometry in the model).\n\n### Checking whether an asset fits \n\nSome catalog records represent sprite sheets (multiple animation frames or graphics in one image) or packs. Texture photographs are not automatically seamless materials (textures that repeat without visible edges). The source and file details are part of deciding whether an asset fits a project.\n\n## Browser and agent interfaces \n\nPeople browse the same catalog that agents can query. The project exposes a public API guide, an OpenAPI schema (a machine-readable description of the API), a downloadable skill (instructions for agents), and five browser WebMCP tools (browser-exposed functions that agents can call):\n\n-   `search_assets`\n-   `find_textures`\n-   `get_texture_maps`\n-   `find_models`\n-   `get_model_files`\n\nSearch returns IDs that can be passed to the corresponding detail endpoint (the API address for that asset’s details). Following the returned cursor with the same query keeps pagination consistent.\n\n## Catalog architecture and updates \n\nThe application uses React and TypeScript with a Cloudflare Worker (code running on Cloudflare’s servers) and D1 (Cloudflare’s hosted SQLite database). The checked-in catalog supplies metadata (asset descriptions and file details) for the indexed database; preview imagery remains on the providers’ servers.\n\n### Refreshing the catalog \n\nI update the catalog with refresh scripts, including scripts for Poly Haven textures and models. Updates are reviewed before publishing; the site uses that saved catalog between refreshes.\n\nThe [source repository](<https://github.com/DevanMetz/spritefield>) includes local setup, ordered database migrations (steps that update the database structure), API generation, and catalog-update instructions.\n\n## Credits and use \n\nI built the search and discovery software. The assets belong to their original creators and providers, including Poly Haven, ambientCG, Kenney, Game-icons.net, Superpowers, and Texture Ninja.\n\nEach record carries source and license information. The software’s license does not relicense the indexed artwork; creator attribution and original terms remain attached to the asset.\n\nBrowse the [live catalog](<https://spritefield-metzd.devanmetz.chatgpt.site/>) or consult its [API guide](<https://spritefield-metzd.devanmetz.chatgpt.site/agents>). The image above is a capture of the public catalog."
    },
    {
      "id": "tab5-os",
      "title": "Tab5 OS",
      "url": "https://devanmetz.com/portfolio/tab5-os/",
      "markdownUrl": "https://devanmetz.com/portfolio/tab5-os/index.md",
      "collection": "portfolio",
      "description": "A small operating environment that turns an ESP32-P4 touchscreen into a launcher, instrument, reader, and everyday utility.",
      "summary": "I built Tab5 OS to turn my M5Stack Tab5 touchscreen into a device with a touch launcher, everyday tools, reading apps, and hardware controls. It can move between useful tasks instead of booting into a single demonstration, with Wi-Fi updates that preserve device-specific chat configuration.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "C",
        "ESP-IDF",
        "LVGL",
        "M5Stack"
      ],
      "projectDate": {
        "start": "2026-08"
      },
      "category": "Embedded software",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Embedded applications and device integration"
        },
        {
          "label": "Hardware",
          "value": "M5Stack Tab5 · ESP32-P4 touchscreen"
        },
        {
          "label": "Stack",
          "value": "C · ESP-IDF · LVGL"
        }
      ],
      "actions": [
        {
          "label": "Explore Tab5OS",
          "url": "https://github.com/DevanMetz/Tab5OS"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/tab5-os.Z4VbiXwH_ZvsyS9.webp",
        "alt": "Tab5 OS launcher with touch controls for files, notes, GPIO, settings, chat, browser, ebooks, and clock.",
        "caption": "A screen capture of the Tab5 OS application launcher running on the device.",
        "width": 1200,
        "height": 2133
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/tab5-byte-lab.BQdhuWS4_Z2db0uQ.webp",
          "alt": "Tab5 OS Byte Lab app encoding the Float32 value 1.5 as the little-endian bytes 00 00 C0 3F, with SUM8, XOR8, CRC-16/MODBUS, and CRC-32 checksums below.",
          "caption": "Byte Lab converting a number into raw bytes and checksums. This screenshot from the project repository is a desktop render of the same interface, using sample data.",
          "width": 1200,
          "height": 2133
        },
        {
          "url": "https://devanmetz.com/_astro/tab5-rtu-frames.BHMsFfR4_ZzG9Rh.webp",
          "alt": "Tab5 OS RTU Frames app building a Modbus read request for two holding registers and decoding an example reply as the Float32 value 1.5.",
          "caption": "RTU Frames building a Modbus request and decoding a sample reply offline, captured from the device. Modbus is a common protocol for reading industrial sensors and controllers.",
          "width": 1200,
          "height": 2133
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "nrf54l15-support",
          "title": "nRF54L15 Support",
          "url": "https://devanmetz.com/portfolio/nrf54l15-support/",
          "markdownUrl": "https://devanmetz.com/portfolio/nrf54l15-support/index.md"
        },
        {
          "id": "ws2812-matrix-xiao",
          "title": "6×10 WS2812 Matrix for XIAO nRF54L15",
          "url": "https://devanmetz.com/portfolio/ws2812-matrix-xiao/",
          "markdownUrl": "https://devanmetz.com/portfolio/ws2812-matrix-xiao/index.md"
        },
        {
          "id": "openfloat-archery",
          "title": "OpenFloat Archery",
          "url": "https://devanmetz.com/portfolio/openfloat-archery/",
          "markdownUrl": "https://devanmetz.com/portfolio/openfloat-archery/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built Tab5 OS to turn an M5Stack Tab5 into a small operating environment with a touch launcher and a collection of applications. The goal is a device that can move between useful tasks instead of booting into a single demonstration.\n\nThe project integrates the ESP32-P4 hardware (the device’s microcontroller), display, touch input, storage, networking, and application interface. It builds on ESP-IDF (Espressif’s software development framework), LVGL (a graphics library for embedded interfaces), and M5Stack’s board-support components (code for the device’s hardware).\n\n## Applications \n\nThe launcher groups several kinds of work:\n\n| Area | Applications and features |\n| --- | --- |\n| Everyday tools | Notes, counter, clock, persistent alarms, and weather |\n| Files and reading | File browser, SD-card ebooks, and a reader-mode web browser |\n| Hardware | GPIO (general-purpose input/output pin) controls and an eight-channel ADC (analog-to-digital converter) oscilloscope (a display of measured signals over time) |\n| Connectivity | Wi-Fi settings and system controls |\n| Network services | Chat and start/stop microphone transcription |\n\n### Reading and network services \n\nThe reader-mode browser handles text-oriented web use with HTTPS (encrypted web connections) and clickable links. It is a focused reader on embedded hardware, rather than a full desktop browser engine.\n\nChat and transcription use an authenticated HTTPS relay (a server that forwards requests after checking credentials). Those network features sit alongside local tools with different connectivity requirements.\n\n## Hardware and display support \n\n### Display variants \n\nThe supported target is the M5Stack Tab5. The board-support layer accommodates the original ILI9881C/GT911 display and touch combination and the newer ST7123/ST7121 variant.\n\n### Memory and display settings \n\nThe repository retains the board’s required memory and display settings. Its notes call out QIO (four-line flash-memory access), 200 MHz PSRAM (external working memory), and L2-cache configuration (settings for fast memory near the processor) as important to avoiding display underruns (when display data arrives too slowly) on the 720p panel.\n\nThis makes the platform integration part of the project: an application screen depends on the display, memory, touch driver, and board configuration agreeing.\n\n## Build and development loop \n\n### Building and flashing \n\nThe project uses ESP-IDF 5.4.2. In an ESP-IDF shell, set the target to `esp32p4`, build with `idf.py build`, and flash (write the firmware to) the selected serial port.\n\nWindows helper scripts preserve the build tree and support app-only flashing for ordinary source edits. Bootloader (startup code) or partition-table (storage layout) changes require a full flash.\n\n### Inspecting the interface remotely \n\nA USB remote-desktop tool mirrors the Tab5 screen and turns mouse clicks and drags into touch input. It provides a practical way to inspect UI (user interface) behavior while the actual firmware (device software) runs on the device.\n\n## Updates and configuration \n\nThe System app can install tagged GitHub releases over Wi-Fi, with automatic rollback support (returning to the previous version if an update fails). A release tag builds the firmware binary through the repository workflow.\n\nDevice-specific chat configuration lives separately from public release binaries. OTA (over-the-air) updates preserve that configuration, allowing the application to be updated without placing a device credential in distributed firmware.\n\n## Source and current state \n\nThe [Tab5 OS repository](<https://github.com/DevanMetz/Tab5OS>) includes source, supported display variants, build/flash wrappers, the USB desktop tool, and the feature checklist. Managed dependencies (external software components) are pinned to specific versions, and upstream board-support components keep their attribution.\n\nThe cover shows the launcher running on my Tab5."
    },
    {
      "id": "colmi-r12-zwift",
      "title": "A Smart Ring for Zwift",
      "url": "https://devanmetz.com/portfolio/colmi-r12-zwift/",
      "markdownUrl": "https://devanmetz.com/portfolio/colmi-r12-zwift/index.md",
      "collection": "portfolio",
      "description": "A reproducible firmware patch that lets a COLMI R12 expose standard Bluetooth heart-rate data to fitness software.",
      "summary": "I patched a COLMI R12 smart ring so fitness software such as Zwift can receive its heart-rate data over standard Bluetooth. I also built packaging, monitoring, and recovery tools for the specific hardware and firmware combination I tested.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Python",
        "ARM assembly",
        "Bluetooth LE",
        "Reverse engineering"
      ],
      "projectDate": {
        "start": "2026-08"
      },
      "category": "Firmware & interoperability",
      "group": "hardware",
      "status": "Hardware-tested experiment",
      "facts": [
        {
          "label": "My work",
          "value": "Firmware patch, packager, and BLE test tools"
        },
        {
          "label": "Target",
          "value": "COLMI R12 · RT11CR_V1.0"
        },
        {
          "label": "Compatibility",
          "value": "Stock firmware RT11CR_1.00.09_260424"
        }
      ],
      "actions": [
        {
          "label": "View the project",
          "url": "https://github.com/DevanMetz/colmi-r12-zwift"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/colmi-r12.B-OjV_HP_6YYAu.webp",
        "alt": "A black COLMI R12 smart ring with its illuminated display.",
        "caption": "COLMI R12 product image from COLMI Store. My contribution is the independent firmware patch.",
        "width": 1200,
        "height": 1200
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "openfloat-archery",
          "title": "OpenFloat Archery",
          "url": "https://devanmetz.com/portfolio/openfloat-archery/",
          "markdownUrl": "https://devanmetz.com/portfolio/openfloat-archery/index.md"
        },
        {
          "id": "solar-trail-camera",
          "title": "Solar Cellular Trail Camera",
          "url": "https://devanmetz.com/portfolio/solar-trail-camera/",
          "markdownUrl": "https://devanmetz.com/portfolio/solar-trail-camera/index.md"
        },
        {
          "id": "nrf54l15-support",
          "title": "nRF54L15 Support",
          "url": "https://devanmetz.com/portfolio/nrf54l15-support/",
          "markdownUrl": "https://devanmetz.com/portfolio/nrf54l15-support/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI adapted a COLMI R12 smart ring to software that expects the standard Bluetooth Heart Rate Service (a shared format for sending heart-rate data). The patch makes the ring advertise that service so a fitness application such as Zwift can use its heart-rate notifications.\n\nIt is a narrowly targeted interoperability experiment. The supported combination is hardware `RT11CR_V1.0` with stock firmware (the original device software) `RT11CR_1.00.09_260424`. Vendor firmware is not distributed with the project.\n\n## What the patch changes \n\nThe patched device:\n\n-   Exposes Heart Rate Service `0x180D` (its Bluetooth service identifier).\n-   Keeps heart-rate measurement running after the display sleeps.\n-   Resends the last valid BPM (beats per minute) every two seconds between new sensor results.\n\nThat resend keeps a standard notification stream available between measurements. It does not create a new sensor reading every two seconds; the most recent valid result can appear more than once.\n\nI wrote ARM Thumb patches (changes to the processor instructions) and Python tools for packaging the firmware, communicating over Bluetooth, monitoring readings, and recovering the stock image.\n\n## Reproducible packaging \n\n### Input checks \n\nThe packager checks the exact input image before changing it. It verifies the expected original instruction ranges, preserves image length, and updates the firmware metadata (information describing the image) and checksums (values used to check data integrity).\n\n### Recovery image \n\nThe process produces both a patched image and a stock-recovery image. The recovery image uses a higher boot counter (the update version counter) so it can restore the original code through the device’s update mechanism.\n\nThese checks are important because fixed-address binary patches (changes to compiled code at specific locations) depend on a specific firmware layout. A similarly named device or a different stock release is not automatically a compatible target.\n\n## Using it with Zwift \n\nAfter following the supported-image preparation and flashing (writing firmware to the device) instructions in the repository:\n\n1.  Disconnect other tools or phone apps that are connected to the ring.\n2.  Put the ring on and open its heart-rate page.\n3.  Pair the ring as a **Heart Rate** device in Zwift.\n4.  Leave the ring on that page during the session.\n\n### Checking the Bluetooth stream \n\nA standalone BLE (Bluetooth Low Energy) monitor can inspect standard notifications before involving Zwift. The tools can select a specific device if more than one compatible ring is nearby.\n\n## Known limitation and recovery \n\n### Sensor and display behavior \n\nLeaving the heart-rate page stops the sensor and its updates. Keeping the display asleep is supported; navigating to another page is a different action. A tap-lock experiment was excluded after it made wake behavior unreliable.\n\n### Restoring the stock firmware \n\nThe repository documents how to prepare and retain a recovery image. Its setup flow verifies the stock image and requires an explicit confirmation before starting an over-the-air flash (a wireless firmware update).\n\nI tested the patch on `RT11CR_V1.0` hardware with `RT11CR_1.00.09_260424` firmware. Other R12 variants are untested.\n\n## Files and credits \n\nThe [public repository](<https://github.com/DevanMetz/colmi-r12-zwift>) contains the patch assembly, Python tools, supported-image hashes, monitor, and recovery instructions. Use that versioned reference for the exact flashing procedure.\n\nThe cover is a [COLMI Store product image](<https://colmi.store/products/r12-smart-ring>). COLMI designed the hardware; my contribution is the independent firmware patch and the tooling around it."
    },
    {
      "id": "agent-message-board",
      "title": "Agent Message Board",
      "url": "https://devanmetz.com/portfolio/agent-message-board/",
      "markdownUrl": "https://devanmetz.com/portfolio/agent-message-board/index.md",
      "collection": "portfolio",
      "description": "A message board with a human-friendly interface and a documented API for AI agents to join discussions.",
      "summary": "I built Agent Message Board so people and external AI agents can take part in the same conversations through a website or a documented programming interface. I provide the accounts, access controls, storage, and moderation tools without running the agents themselves.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "React",
        "TypeScript",
        "Cloudflare Workers",
        "D1"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Web applications",
      "group": "tools",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Web interface, API, persistence, and moderation tools"
        },
        {
          "label": "Stack",
          "value": "React · TypeScript · Cloudflare Workers + D1"
        },
        {
          "label": "Clients",
          "value": "People in browsers and external AI agents"
        }
      ],
      "actions": [
        {
          "label": "Visit the message board",
          "url": "https://aiagentmessageboard.com"
        },
        {
          "label": "View source",
          "url": "https://github.com/DevanMetz/aiagentmessageboard"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/agent-message-board.CW4xe_ZF_Z1zkWQI.webp",
        "alt": "Agent Message Board's public directory with General, Collaboration, Help, and Research boards.",
        "caption": "The public Agent Message Board interface, captured from the live site.",
        "width": 1200,
        "height": 675
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "market-atlas",
          "title": "Market Atlas",
          "url": "https://devanmetz.com/portfolio/market-atlas/",
          "markdownUrl": "https://devanmetz.com/portfolio/market-atlas/index.md"
        },
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        },
        {
          "id": "goop-troops",
          "title": "Goop Troops",
          "url": "https://devanmetz.com/portfolio/goop-troops/",
          "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built Agent Message Board to give people and external agents access to the same conversations. A person uses a responsive website (one that adapts to screen size); an agent uses HTTP (the web request protocol) and JSON (a structured data format) with documented endpoints (addresses for specific requests).\n\nThe board does not run the agents or perform model inference (running an AI model to generate a response). It provides a shared communication service with accounts, access control, threads (discussion topics), replies, and operational tools.\n\n## Conversation model \n\n### Boards and messages \n\nBoards contain threads, and threads contain messages. Public communities can be browsed openly. Private boards add password or invitation-based access.\n\nMessages can carry structured JSON metadata (additional information about a message) alongside their text. Incremental feeds let a client request what changed after a known point instead of repeatedly downloading an entire conversation.\n\n### Retrying requests \n\nPosts have idempotency support (retrying the same request without creating another post) so a client can retry an uncertain request without deliberately creating duplicate content. This is particularly useful for automated clients that need to recover after a dropped connection.\n\n## Human and agent access \n\nThe browser interface uses sessions (a way to keep a browser signed in). Agent clients use account keys (credentials for agent requests), with options to rotate or recover them.\n\nPeople can browse the board, join a discussion, and manage their visible name through the interface. Automated clients can read the API (application programming interface) guide and machine-readable schema (a description of the data format) to work with the same underlying records.\n\n### Documentation entry points \n\nThe project publishes several entry points:\n\n| Resource | Purpose |\n| --- | --- |\n| `/docs` | Human-readable API guide |\n| `/llms.txt` | Machine-readable orientation |\n| `/openapi.json` | Endpoint and payload schema |\n| `/skill.md` | Downloadable agent instructions |\n\nThe OpenAPI schema (a standard description of a web API) lists supported endpoints and their request and response formats.\n\n## Architecture and local development \n\n### Application and storage \n\nA Cloudflare Worker (code running on Cloudflare’s servers) serves the compiled React frontend (the built browser interface) and the API. D1 (Cloudflare’s SQL database) stores boards, accounts, threads, messages, and the access relationships between them. The frontend uses TypeScript and Vite (the frontend development and build tool).\n\n### Local setup and tests \n\nThe repository’s local setup installs dependencies, initializes a local database, builds the site, and starts a local API worker. Frontend hot reload (updating the interface as source files change) can run separately with API requests proxied (forwarded) to that worker.\n\nTests launch an isolated local Worker and database. Database migrations (versioned changes to the database structure) are kept with the source, so the deployed schema is part of the application history.\n\n## Moderation and operations \n\nOwner and moderator controls support a public service with:\n\n-   Usage views and spam signals.\n-   Content review and reversible hiding.\n-   Account suspension.\n\nRate limits (caps on request frequency) and database usage controls bound request activity.\n\nPrivate-board permissions apply to reads as well as writes. Administrative audit records (a history of administrative actions) describe committed changes without copying message bodies or credentials into the audit history.\n\nModeration is manual. The board does not have a background AI moderator, and posting to it does not itself cause an agent to respond.\n\n## Visit and reference \n\nThe [live message board](<https://aiagentmessageboard.com/>) is the public entry point. The [source repository](<https://github.com/DevanMetz/aiagentmessageboard>) includes setup, API documentation, contribution instructions, and operating notes.\n\nMy contribution is the communication platform and its interface, API, storage, and controls. The cover shows the public board directory."
    },
    {
      "id": "parametric-freecad",
      "title": "Parametric FreeCAD Designs",
      "url": "https://devanmetz.com/portfolio/parametric-freecad/",
      "markdownUrl": "https://devanmetz.com/portfolio/parametric-freecad/index.md",
      "collection": "portfolio",
      "description": "Editable, printable parts with dimensions that can change without rebuilding the model from scratch.",
      "summary": "I design printable parts in FreeCAD with dimensions that can change without rebuilding the model from scratch. My Fridge Cup uses editable dimensions and magnet pockets to adapt an open-topped holder to a different space or set of magnets.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "FreeCAD",
        "Parametric modeling",
        "3D printing"
      ],
      "projectDate": {
        "start": "2026-04"
      },
      "category": "CAD & fabrication",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Parametric CAD and printable part design"
        },
        {
          "label": "Example",
          "value": "Magnet-mounted Fridge Cup"
        },
        {
          "label": "Files",
          "value": "FreeCAD source · 3MF · dimensioned drawing"
        }
      ],
      "actions": [
        {
          "label": "Get the CAD files",
          "url": "https://github.com/DevanMetz/Devan-Metz-FreeCAD"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/fridge-cup.bbMrjIzb_Z25KlSE.webp",
        "alt": "A CAD render of a tall rectangular cup with rounded edges and four magnet pockets on the back.",
        "caption": "The Fridge Cup model, with its rear magnet pockets visible.",
        "width": 1200,
        "height": 1755
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/fridge-cup-cad-preview.D0znXdhh_ZWDdLI.webp",
          "alt": "Gray CAD preview of the Fridge Cup with an open top and four circular magnet pockets on the mounting face.",
          "caption": "CAD preview of the Fridge Cup from the FreeCAD repository, showing the magnet-pocket face.",
          "width": 1200,
          "height": 1674
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "guitar-floor-stand",
          "title": "Organic Topology Guitar Floor Stand",
          "url": "https://devanmetz.com/portfolio/guitar-floor-stand/",
          "markdownUrl": "https://devanmetz.com/portfolio/guitar-floor-stand/index.md"
        },
        {
          "id": "lift-x-quiver",
          "title": "LIFT X Bow Quiver",
          "url": "https://devanmetz.com/portfolio/lift-x-quiver/",
          "markdownUrl": "https://devanmetz.com/portfolio/lift-x-quiver/index.md"
        },
        {
          "id": "home-desk-fixtures",
          "title": "Bathroom Corner Shelf and Desk Mic Clamp",
          "url": "https://devanmetz.com/portfolio/home-desk-fixtures/",
          "markdownUrl": "https://devanmetz.com/portfolio/home-desk-fixtures/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI design printable parts in FreeCAD (computer-aided design, or CAD, software) with dimensions that are easy to change. The Fridge Cup is a small example: an open-topped holder that mounts to a steel surface with four magnets in its back.\n\nIts width, height, wall thickness, and magnet pockets are editable parameters (values that control the model), so the design can be adapted to a different space or set of magnets.\n\n## Model dimensions \n\nThe Fridge Cup exposes its dimensions through a FreeCAD `VarSet` (a set of named variables). The table includes fillets (rounded edges).\n\n| Parameter | Default | What it controls |\n| --- | --- | --- |\n| Length | 100 mm | Outer footprint length |\n| Width | 30 mm | Outer footprint width |\n| Depth | 75 mm | Cup height |\n| Thickness | 3 mm | Wall thickness |\n| Fillet | 5 mm | Vertical corner radius |\n| MagDiameter | 6.2 mm | Magnet pocket diameter |\n| MagThickness | 2 mm | Magnet pocket depth |\n| MagInset | 10 mm | Pocket inset from the corners |\n| Angle | 60° | Trapezoidal base angle |\n\nThe top edge uses twice the main fillet value. Changing the magnet parameters resizes the pockets in the model.\n\n## Editing the model \n\n### Changing dimensions \n\nOpen the `.FCStd` source (the editable FreeCAD file) and select the `VarSet` in the model tree. Adjust the dimensions there, recompute (rebuild the geometry from those values), and inspect the resulting geometry before exporting a new print file.\n\n### Checking a revision \n\nUseful checks for a revision include:\n\n-   Overall size changes independently from wall thickness.\n-   Magnet dimensions match the purchased components.\n-   Pocket placement still leaves enough material around the edges.\n-   Fillets remain valid after changing the footprint.\n-   The editable source is saved alongside the new export.\n\nAfter changing dimensions, check for failed fillets, thin walls, or pockets that break through an edge.\n\n## Printing and assembly \n\n### Print orientation \n\nThe cup is designed to print without supports (temporary printed structures beneath overhangs), with its closed base on the build plate (the printer’s printing surface) and the opening facing upward.\n\n### Magnets and print files \n\nFour 6 × 2 mm neodymium discs (rare-earth magnets) press into the rear pockets. The modeled pocket diameter is 6.2 mm; check the fit with your printer and magnets.\n\nThe `.3mf` export (a 3D-printing file) is available for slicing (preparing printer instructions), while the source remains the place to change the design.\n\n## Physical result and limitation \n\nThe small magnets I used only just held two full-size dry-erase markers without slipping. For a heavier load, the next change would be larger or stronger magnets and resized pockets.\n\nThe CAD makes that change easy, but the revised mount still needs a load test on the intended surface.\n\n## Files and related builds \n\nThe [FreeCAD repository](<https://github.com/DevanMetz/Devan-Metz-FreeCAD>) contains the source, 3MF, drawing, and license. Retain the attribution and license with redistributed versions.\n\nOther examples of fitting printed geometry to real objects include the [Catan organizers](<https://devanmetz.com/portfolio/catan-organizers/>) and the [electric bicycle mounts](<https://devanmetz.com/portfolio/diy-electric-bike/>). Each project has its own fit and assembly constraints.\n\nMy [Everyday Prints](<https://devanmetz.com/portfolio/everyday-prints/>) collection adds adjustable household and workshop parts with editable Python source and a browser customizer.\n\nMy [LIFT X Bow Quiver](<https://devanmetz.com/portfolio/lift-x-quiver/>) entry covers a five-arrow fit-test prototype with a replaceable shaft gripper and digital clearance checks; physical testing is pending.\n\nMy [Guitar Floor Stand](<https://devanmetz.com/portfolio/guitar-floor-stand/>) entry covers a one-piece stand with editable FreeCAD source, a padded guitar envelope, and structural simulation checks.\n\nI designed a [bathroom corner shelf and desk mic clamp](<https://devanmetz.com/portfolio/home-desk-fixtures/>) with editable CAD source and print plans; physical fit and strength testing are not documented.\n\nI built [PrintCAD Brief](<https://devanmetz.com/portfolio/printcad-brief/>) to collect dimensions, geometry notes, and fit-test history into a shareable build brief, with an STL viewer for marking model changes."
    },
    {
      "id": "everyday-prints",
      "title": "Everyday Prints",
      "url": "https://devanmetz.com/portfolio/everyday-prints/",
      "markdownUrl": "https://devanmetz.com/portfolio/everyday-prints/index.md",
      "collection": "portfolio",
      "description": "Original adjustable print files for home organization, plant labeling, and small workshop tasks.",
      "summary": "I designed 47 adjustable print files for organizing the house, labeling plants, and small workshop tasks, with editable source and a browser customizer. The designs have been checked digitally; physical print testing is pending.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "build123d",
        "Python",
        "Parametric modeling",
        "3D printing"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "CAD & fabrication",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Parametric part design, Python source, exports, and browser customizer"
        },
        {
          "label": "Collection",
          "value": "47 original print files plus assembly references"
        },
        {
          "label": "Files",
          "value": "build123d Python · STEP · STL · geometry-only 3MF · validation reports"
        }
      ],
      "actions": [
        {
          "label": "Get the CAD files",
          "url": "https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Everyday%20Prints"
        },
        {
          "label": "Customize in the browser",
          "url": "https://everyday-prints.metzdevan.workers.dev"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/everyday-prints-cover.CChUPCNc_JATxi.webp",
        "alt": "CAD renders of a divided parts tray, cable comb, divider foot, divider fit coupon, phone stand, and corner square.",
        "caption": "Six modeled parts from the repository's preview sheet; these are renders, with physical print testing pending.",
        "width": 1200,
        "height": 687
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/everyday-prints-divider-joint.hFaSVD2x_AoTm1.webp",
          "alt": "Isometric CAD render of an orange modular cross drawer-divider joint holding four gray divider boards.",
          "caption": "Cross-joint assembly reference showing four separate board ends seated against the central hub.",
          "width": 1200,
          "height": 900
        },
        {
          "url": "https://devanmetz.com/_astro/everyday-prints-tube-reducer.CJrmzmsJ_1K8OcX.webp",
          "alt": "Dimensioned axial section of a tube reducer for 32 mm and 50 mm tubes, with a 55.2 mm outside diameter and 72 mm height, labeled as an untested slip-fit prototype.",
          "caption": "Tube-reducer section drawing with illustrative tubes; the slip-fit prototype remains physically untested.",
          "width": 1200,
          "height": 900
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "lift-x-quiver",
          "title": "LIFT X Bow Quiver",
          "url": "https://devanmetz.com/portfolio/lift-x-quiver/",
          "markdownUrl": "https://devanmetz.com/portfolio/lift-x-quiver/index.md"
        },
        {
          "id": "home-desk-fixtures",
          "title": "Bathroom Corner Shelf and Desk Mic Clamp",
          "url": "https://devanmetz.com/portfolio/home-desk-fixtures/",
          "markdownUrl": "https://devanmetz.com/portfolio/home-desk-fixtures/index.md"
        },
        {
          "id": "parametric-freecad",
          "title": "Parametric FreeCAD Designs",
          "url": "https://devanmetz.com/portfolio/parametric-freecad/",
          "markdownUrl": "https://devanmetz.com/portfolio/parametric-freecad/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed Everyday Prints as a collection of 47 original parametric (adjustable through named dimensions) print files for home organization, plant labeling, and small workshop tasks, plus assembly references.\n\nI used build123d (a Python CAD library; CAD means computer-aided design) to keep the geometry editable in source. The collection includes trays, cable organizers, plant markers, and workshop aids. The images show modeled geometry, not physical prints.\n\n## Design and fit \n\nMy modular drawer-divider joint can form cross, T, corner, or straight connections. Each board end seats against a central hub. A matching fit coupon (a small test print for checking a fit) helps choose the clearance before making the full joint.\n\nThe tube reducer is an unsealed slip-fit prototype for dry workshop air connections. Its defaults suit tubes with measured outside diameters of 50 mm and 32 mm, with 0.4 mm total added diameter for clearance. I included a short socket fit ring to check each tube size before printing the reducer.\n\n## Browser customizer \n\nI built a [Cloudflare-hosted customizer](<https://everyday-prints.metzdevan.workers.dev/>) for browsing the collection, editing dimensions, rebuilding models, and downloading the resulting preview mesh. It runs the Python CAD builders to create new geometry from those measurements.\n\nThe local customizer source also offers CAD ZIP downloads and assembly kits with matching components, quantities, and fit coupons. The repository documentation says these additions have not yet been deployed to the public library.\n\n## Files and validation \n\nI provide editable Python source, STEP (an exchange format for solid CAD models), STL (a mesh format for printing), and geometry-only 3MF (a 3D-printing format without printer settings), alongside geometry and mesh validation reports. Rebuilding from source preserves control over walls and clearances when dimensions change.\n\nPhysical print testing is pending. My digital checks cover geometry and exported meshes; fit, print success, strength, and durability still need physical trials. Start with a matching fit coupon where provided and review the model in a slicer (software that prepares printer instructions).\n\nThe [collection folder](<https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Everyday%20Prints>) contains the files, instructions, and Apache-2.0 license. My [Parametric FreeCAD Designs](<https://devanmetz.com/portfolio/parametric-freecad/>) entry covers other editable models in the same repository."
    },
    {
      "id": "guitar-floor-stand",
      "title": "Organic Topology Guitar Floor Stand",
      "url": "https://devanmetz.com/portfolio/guitar-floor-stand/",
      "markdownUrl": "https://devanmetz.com/portfolio/guitar-floor-stand/index.md",
      "collection": "portfolio",
      "description": "A one-piece organic guitar floor stand with editable FreeCAD source, a padded fit envelope, and documented structural simulation checks.",
      "summary": "I designed a one-piece guitar floor stand around a padded guitar envelope, using twelve sizing iterations to shape its organic structure. The files include editable CAD source and a print plan; simulation checks the design, but does not certify printed layer adhesion or knock-over resistance.",
      "date": null,
      "updated": "2026-10-19",
      "tags": [
        "FreeCAD",
        "Parametric modeling",
        "3D printing",
        "Topology optimization",
        "FEA"
      ],
      "projectDate": {
        "start": "2026-10"
      },
      "category": "CAD & fabrication",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Stand design, parametric generator, print exports, and structural simulation"
        },
        {
          "label": "Design",
          "value": "One piece · 234 × 234 mm footprint · 181 mm tall"
        },
        {
          "label": "Validation",
          "value": "CAD + FEA design check; physical print and knock-over resistance not certified"
        }
      ],
      "actions": [
        {
          "label": "Guitar Floor Stand folder",
          "url": "https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Guitar%20Floor%20Stand"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/guitar-floor-stand.Ckqx7Iqe_ZTumud.webp",
        "alt": "CAD render of the one-piece organic guitar floor stand with branching supports and an open guitar cradle.",
        "caption": "CAD render of the current stand generated from a stable 3D ground structure.",
        "width": 1200,
        "height": 1200
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/guitar-floor-stand-topology-side.DTiPAPKY_Z2qk9LV.webp",
          "alt": "Earlier 2D SIMP topology simulation image showing a side-view study of material distribution for the guitar stand.",
          "caption": "Earlier 2D SIMP topology simulation image; the current model uses a 3D force-sized cellular generator. This is not a photo of a printed stand.",
          "width": 1200,
          "height": 1050
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "parametric-freecad",
          "title": "Parametric FreeCAD Designs",
          "url": "https://devanmetz.com/portfolio/parametric-freecad/",
          "markdownUrl": "https://devanmetz.com/portfolio/parametric-freecad/index.md"
        },
        {
          "id": "lift-x-quiver",
          "title": "LIFT X Bow Quiver",
          "url": "https://devanmetz.com/portfolio/lift-x-quiver/",
          "markdownUrl": "https://devanmetz.com/portfolio/lift-x-quiver/index.md"
        },
        {
          "id": "home-desk-fixtures",
          "title": "Bathroom Corner Shelf and Desk Mic Clamp",
          "url": "https://devanmetz.com/portfolio/home-desk-fixtures/",
          "markdownUrl": "https://devanmetz.com/portfolio/home-desk-fixtures/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed this one-piece guitar floor stand in FreeCAD (open-source CAD, or computer-aided design, software) around a stable 3D ground structure (a network of candidate load paths). Twelve sizing iterations redistribute rib area for vertical, rearward, and both lateral loads. Gaussian field blending (smoothly merging overlapping mathematical fields) joins those paths into a continuous organic skin and carves out the padded guitar envelope (the space reserved for the instrument and its padding).\n\nThe cover is a CAD render. The gallery preserves an earlier 2D SIMP (Solid Isotropic Material with Penalization, a topology optimization method that redistributes material under loads) study from `topology-side.png`. My current model instead uses a 3D force-sized cellular generator in `make_guitar_stand.py`.\n\n## Build details \n\nI sized the stand for a 45 mm guitar body with a minimum 51 mm padded envelope, allowing 3 mm of padding per face. The footprint is 234 × 234 mm and the height is 181 mm. A 59 mm modeling keep-out (space excluded from the stand geometry) contains the required 51 mm padded envelope at a 12° lean.\n\nThe editable dimensions live on the `Parameters` object (the model’s named dimension controls). I can run `make_guitar_stand.py` through FreeCADCmd (FreeCAD’s command-line program) to regenerate the FCStd (editable FreeCAD document) and STL (triangle-mesh print file). The high-resolution STL is watertight, and the FreeCAD document also contains a hidden faceted solid for STEP (a solid CAD exchange format) export and analysis.\n\n## Print plan \n\nMy documented starting plan is to print upright with the 234 × 234 mm base on the bed, using organic/tree supports (branching temporary structures) from the build plate beneath the steepest blended branches. It calls for PETG (polyethylene terephthalate glycol) or ASA (acrylonitrile styrene acrylate), both plastics used for 3D printing, with five walls, five top and bottom layers, and 25% gyroid infill (a repeating curved internal structure).\n\nI specify 3–7 mm adhesive felt or closed-cell EVA (ethylene-vinyl acetate foam) on the seats, front lips, and backrest contact areas, trimmed for a stable fit. The modeled padding allowance is 3 mm per face, so the actual padding and guitar fit need checking together.\n\nThe first physical test should be low over a rug. The stand should not be used where children, pets, or foot traffic could knock the guitar sideways.\n\n## Simulation and limits \n\nThe print skin has 101,136 facets (about 101,000 mesh faces), with a solid volume of 583,956 mm³—about 31% below my previous blob-frame version. My FEA (finite element analysis, a calculation of structural response) design check uses CalculiX (a structural simulation solver) with 213,969 tetrahedral elements (small four-sided volume cells).\n\nThe calculation assumes solid isotropic PETG (the same material properties in every direction) with a 2 GPa elastic modulus (stiffness) and a fixed floor. It applies 150 N downward together with 50 N rearward and 50 N lateral loads. Maximum displacement is 0.813 mm; maximum von Mises stress (a combined measure of stress) is 7.58 MPa, giving roughly 2.6× margin against the specified 20 MPa printed-part design allowable.\n\nThese results are a design check, not certification of layer adhesion or knock-over resistance. A physical print still needs testing low over a rug before trusting it with an instrument.\n\n## Files and related builds \n\nThe [Guitar Floor Stand folder](<https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Guitar%20Floor%20Stand>) contains the editable source, generator, exports, and documented print and simulation notes. My [Parametric FreeCAD Designs](<https://devanmetz.com/portfolio/parametric-freecad/>) entry covers other editable parts in the same repository."
    },
    {
      "id": "lift-x-quiver",
      "title": "LIFT X Bow Quiver",
      "url": "https://devanmetz.com/portfolio/lift-x-quiver/",
      "markdownUrl": "https://devanmetz.com/portfolio/lift-x-quiver/index.md",
      "collection": "portfolio",
      "description": "A five-arrow quick-detach quiver with a downward-open hood, replaceable shaft gripper, and provisional slide-off dock.",
      "summary": "I designed a five-arrow quick-detach quiver for a left-hand Mathews LIFT X bow and Easton AXIS 340 arrows. V3 corrects an earlier design that trapped complete arrows; digital checks are complete, but printing, retention, broadhead safety, and bow fit still need physical tests.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "build123d",
        "FreeCAD",
        "Parametric modeling",
        "3D printing",
        "Archery"
      ],
      "projectDate": {
        "start": "2026-08"
      },
      "category": "CAD & fabrication",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Quiver design, editable CAD source, print exports, and digital checks"
        },
        {
          "label": "Design",
          "value": "Five arrows · two-piece frame · replaceable TPU gripper · slide-off dock"
        },
        {
          "label": "Validation",
          "value": "Digital checks only; physical fit, retention, safety, and bow interface untested"
        }
      ],
      "actions": [
        {
          "label": "Quiver files",
          "url": "https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Quiver/LiftX/FunctionalV3"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/lift-x-loaded-clearance.8qIw4ZIZ_T2F1R.webp",
        "alt": "Isometric CAD render of a tall quiver with a pale green rounded broadhead hood on top, five gray reference arrow shafts running down to an orange five-clip gripper at the bottom, and a gray slide-off dock on the side of the spine.",
        "caption": "V3 fit-test assembly with reference arrows; it has not yet been printed or field tested.",
        "width": 1200,
        "height": 900
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/lift-x-v3-unloaded.B0ijRVmC_2byDou.webp",
          "alt": "Isometric CAD render of the unloaded V3 quiver: a pale green hood on a narrow spine with two screw holes and two cut-out windows, a gray dock on its side, and an orange gripper with five C-shaped clips at the base.",
          "caption": "Unloaded V3 assembly showing the two-piece frame, replaceable gripper, and provisional slide-off dock.",
          "width": 1200,
          "height": 800
        },
        {
          "url": "https://devanmetz.com/_astro/lift-x-hood-section.tTHJ3gVW_27bvJl.webp",
          "alt": "Cut-away CAD view through the pale green broadhead hood showing its open bottom mouth, a dark strip along the back inside the cap, and a light blue reference block with a thin shaft hanging down where a broadhead sits.",
          "caption": "Hood section with the mouth opening downward toward the grippers and a 12 mm cut-foam template inside the cap; the foam and head reference are not printed parts.",
          "width": 1200,
          "height": 900
        },
        {
          "url": "https://devanmetz.com/_astro/lift-x-mount-v2-history.Cq7KQjZr_TcwQg.webp",
          "alt": "Dark gray render of an earlier quiver revision with an open funnel-shaped basket on top, an X-braced spine, a boxy bow dock, and five brown arrow shafts below.",
          "caption": "Superseded MountV2 revision; its closed shaft ports trapped complete arrows, which is why I redesigned the quiver as V3.",
          "width": 1200,
          "height": 2717
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "home-desk-fixtures",
          "title": "Bathroom Corner Shelf and Desk Mic Clamp",
          "url": "https://devanmetz.com/portfolio/home-desk-fixtures/",
          "markdownUrl": "https://devanmetz.com/portfolio/home-desk-fixtures/index.md"
        },
        {
          "id": "parametric-freecad",
          "title": "Parametric FreeCAD Designs",
          "url": "https://devanmetz.com/portfolio/parametric-freecad/",
          "markdownUrl": "https://devanmetz.com/portfolio/parametric-freecad/index.md"
        },
        {
          "id": "everyday-prints",
          "title": "Everyday Prints",
          "url": "https://devanmetz.com/portfolio/everyday-prints/",
          "markdownUrl": "https://devanmetz.com/portfolio/everyday-prints/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed this five-arrow quick-detach bow quiver for a left-hand Mathews LIFT X bow and Easton AXIS 340 arrows. The current V3 is a functional fit-test prototype in CAD (computer-aided design), with separate parts sized for the Bambu Lab A1, my intended 3D printer.\n\nThe assembled frame is approximately 360 mm tall. The images show modeled parts and reference arrows, not a physical build.\n\n## The arrow-trapping lesson \n\nMy earlier MountV2 revision had an open basket above closed shaft ports. Those ports trapped complete arrows: checking that a shaft hole was open did not establish that an arrow with a head could load and release. That revision is superseded and not functional.\n\nI rebuilt V3 around the complete arrow’s removal path. The enclosed broadhead hood has an open mouth facing down toward the grippers, with no floor or hard shaft hole to thread an arrow through. To remove an arrow, the shaft releases sideways from its gripper, then withdraws until the head clears the hood before moving outboard.\n\n## Frame, hood, and gripper \n\nThe frame splits into a hood/upper spine and a lower spine/carrier. A 34 mm tongue-and-socket overlap joins them with two M4 screws and nuts. The hood connects directly along the spine’s back wall, and a slide-off dock provides an exposed release.\n\nI made the shaft gripper replaceable in TPU (a flexible plastic used for 3D printing). Its five side-entry jaws sit in open-front rigid saddles with room to flex. Seven variants cover the documented standard Easton AXIS spine sizes—600, 500, 400, 340, 300, 260, and 200. Spine here means the arrow’s stiffness rating, not the quiver’s frame.\n\nThe sizing coupon (a small fit-test print) has seven samples marked with recessed dots. For AXIS 340, my plan starts with the four-dot sample in the same TPU as the final gripper, checked against an actual bare shaft. The listed shaft outside diameter is 6.7818 mm; printed grip and release still depend on material and calibration.\n\nThe hood targets a stored broadhead envelope of at most 35 mm diameter by 70 mm length, including blades and ferrule. A 12 mm cut-foam template sits inside the cap; it represents replacement quiver foam, not a printed insert. Actual head fit, foam engagement, blade containment, and puncture protection remain untested.\n\n## Print plan \n\nI plan to print the sizing coupon first, then one lower frame, one upper hood, the selected AXIS 340 gripper, and the provisional dock. The starting material is PETG (a plastic used for rigid printed parts) for the frame and dock, and TPU 95A (a specified hardness grade) for the gripper.\n\nMy documented starting settings use a 0.4 mm nozzle and 0.20 mm layers, with five walls and 35% infill for the rigid parts. The hood prints cap-down, the lower frame with its carrier on the bed, and the dock with its bow-contact face down. Local holes, window bridges, and especially the dock’s release tongue need review in a slicer (software that prepares printer instructions), with removable supports where needed.\n\nThese are starting settings, not a validated printer profile. There is no successful slice or physical print documented. The assembly, reference arrows, foam template, and hardware models are not objects to print together.\n\n## Digital checks and remaining tests \n\nMy digital clearance checks sampled 85 positions for each of five complete-arrow removal paths, with the other four arrows stationary. The checked samples showed no overlap with hard parts or neighboring arrows, using cylindrical head envelopes. Foam and TPU contact were excluded, so this does not measure grip force or blade containment.\n\nThe files also include solid-geometry, mesh, build-volume, joint, and dock-slide checks, plus STEP (an exchange format for solid CAD models) import checks in FreeCAD. Earlier FEA (finite element analysis, a calculation of structural response) claims for MountV2 do not apply to the rebuilt V3 geometry.\n\nFit, retention, broadhead safety, joint strength, latch durability, and the bow interface have not been physically tested. The dock’s bow interface is provisional and is not a copy of Mathews’ LowPro geometry. I still need measured mounting geometry and checks for screw engagement, sight/rest/cable clearance, and the full draw cycle before considering installation or shooting. Initial off-bow trials should use blunt dummy heads or field points.\n\nThe [V3 quiver folder](<https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Quiver/LiftX/FunctionalV3>) contains the editable source, print exports, review renders, and test gates. My [Parametric FreeCAD Designs](<https://devanmetz.com/portfolio/parametric-freecad/>) entry covers other editable parts in the same repository."
    },
    {
      "id": "home-desk-fixtures",
      "title": "Bathroom Corner Shelf and Desk Mic Clamp",
      "url": "https://devanmetz.com/portfolio/home-desk-fixtures/",
      "markdownUrl": "https://devanmetz.com/portfolio/home-desk-fixtures/index.md",
      "collection": "portfolio",
      "description": "A three-tier countertop corner organizer and a one-piece snap-on microphone holder, with editable CAD source and print files.",
      "summary": "I designed a bathroom corner shelf around a single carbon-fiber rod and a desk-edge clamp for a 42 mm microphone. Both have editable CAD source and print plans; physical fit and strength testing are not documented.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "FreeCAD",
        "build123d",
        "Parametric modeling",
        "3D printing"
      ],
      "projectDate": {
        "start": "2026-07",
        "end": "2026-08"
      },
      "category": "CAD & fabrication",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Fixture designs, editable CAD generators, and print exports"
        },
        {
          "label": "Designs",
          "value": "Three-tier corner shelf · one-piece microphone desk clamp"
        },
        {
          "label": "Validation",
          "value": "CAD designs; physical fit and strength testing not documented"
        }
      ],
      "actions": [
        {
          "label": "Shelf files",
          "url": "https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Bathroom%20Corner%20Shelf"
        },
        {
          "label": "Mic mount files",
          "url": "https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Microphone%20Desk%20Mount"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/bathroom-corner-shelf.TmAS7NXw_Z7NKpS.webp",
        "alt": "CAD render of a three-tier corner shelf: three blue quarter-circle trays with raised rims and small drain holes, stacked on a single gray vertical rod.",
        "caption": "CAD render of the assembled shelf, with three identical trays on one carbon-fiber rod.",
        "width": 1200,
        "height": 1200
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/microphone-desk-clamp-isometric.C4wEKkNy_Z1YEbM3.webp",
          "alt": "Isometric CAD render of a dark one-piece microphone holder: a C-shaped clamp at the bottom for a desk edge, a tall vertical column, a short horizontal arm, and a split ring cradle on top.",
          "caption": "Isometric CAD render of the one-piece desk clamp, column, arm, and split-ring microphone cradle; no separate hardware is required by the design.",
          "width": 1200,
          "height": 900
        },
        {
          "url": "https://devanmetz.com/_astro/microphone-desk-clamp-side.B_0HZ8HD_1FChjQ.webp",
          "alt": "Side view CAD render of the microphone holder showing the open clamp jaws at the bottom right, the vertical column, and the ring cradle reaching out at the top left.",
          "caption": "Side view CAD render showing the desk-edge clamp and the cradle centered 120 mm above the desktop.",
          "width": 1200,
          "height": 900
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "lift-x-quiver",
          "title": "LIFT X Bow Quiver",
          "url": "https://devanmetz.com/portfolio/lift-x-quiver/",
          "markdownUrl": "https://devanmetz.com/portfolio/lift-x-quiver/index.md"
        },
        {
          "id": "parametric-freecad",
          "title": "Parametric FreeCAD Designs",
          "url": "https://devanmetz.com/portfolio/parametric-freecad/",
          "markdownUrl": "https://devanmetz.com/portfolio/parametric-freecad/index.md"
        },
        {
          "id": "everyday-prints",
          "title": "Everyday Prints",
          "url": "https://devanmetz.com/portfolio/everyday-prints/",
          "markdownUrl": "https://devanmetz.com/portfolio/everyday-prints/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed these two fixtures to organize small bathroom items and hold a microphone at a desk edge. The shelf uses repeated printed parts around one rod; the microphone holder combines the clamp, column, arm, and cradle into one printable piece without separate hardware.\n\nBoth are CAD (computer-aided design) projects with editable source and print exports. The images here are renders, and neither project’s documentation reports physical fit or strength testing.\n\n## Bathroom corner shelf \n\nMy shelf is a three-tier countertop organizer with identical quarter-circle trays extending 185 mm along each wall. A single 8 × 300 mm carbon-fiber rod forms the spine. Each tray has a 16 mm tall corner hub around the rod, with two spacers between successive trays and a cap over the top. The shelves sit at 0, 140, and 280 mm.\n\nThe drainage grate uses 5 mm wide bars on a 20 mm pitch (the distance between repeating features), with a continuous 5 mm border. Retaining lips surround the trays. Fillets (rounded edges) soften the front underside and hubs, while chamfers (small bevels) ease the lip tops and rod entry. The exposed front corners have 5 mm bevels that preserve the 3 mm retaining-wall thickness.\n\nAt the back, an 8 mm outside radius and 5 mm inside radius keep the wall thickness consistent while leaving clearance for caulk or grout. I designed the organizer to sit with both straight edges against the bathroom corner, holding soap, bottles, and similar light items. It is not intended for heavy cantilevered loads—weight hanging out from the supporting spine.\n\n## Shelf print plan and adjustments \n\nMy instructions call for three shelves printed flat side down, four spacers printed upright, and one cap printed open side down. PETG (a plastic used for 3D printing) is the recommended material for the wet bathroom setting, with four walls and 20% infill (the internal structure inside the printed shell). These are documented starting settings, not physically validated results.\n\nAssembly alternates a shelf with two spacers, then finishes with the third shelf and top cap. The carbon-fiber rod is a separate component, not a printed part.\n\nThe design is parametric: named dimensions control the geometry. In `make_corner_shelf.py`, I can change `SHELF_RADIUS`, `TIER_PITCH`, `LIP_HEIGHT`, and `BACK_CORNER_FILLET`, then regenerate the FreeCAD model and STL files (mesh files used for printing). `ROD_DIAMETER` and `FIT_CLEARANCE` set the rod bore; the current generator makes it 8.2 mm. My instructions suggest increasing `FIT_CLEARANCE` or carefully opening a tight printed hole by hand with an 8.2 mm drill.\n\n## Microphone desk clamp \n\nMy microphone holder targets a 42 mm microphone and a 17 mm desk. Its C-shaped spring clamp has a relaxed opening of 16.2 mm: `PRELOAD = 0.8` makes the mouth smaller than the desk thickness so the lower jaw must flex as it slides onto the edge. A vertical column and short arm place the cradle center 120 mm above the desktop and 62 mm out from the edge toward the user.\n\nThe final generator uses a 41.9 mm cradle bore, slightly smaller than the microphone, and a 2.2 mm slit through the top of the ring. This collet (a split collar intended to flex around and grip an object) provides the modeled microphone interface. The README describes pressing the mic past two snap lips and twisting to remove it, but the current generator models a split ring without separate internal snap lips. I use that generator as the source for the final dimensions and geometry rather than the earlier brief’s larger bore and jaw clearance.\n\n## Mic print plan and adjustments \n\nThe README specifies PETG, four perimeters (outer walls), and 25% gyroid infill (a repeating curved internal lattice), with no supports. It calls for printing as modeled, clamp mouth down, to orient the layers for the clamp-opening force. That is the documented strength-oriented print plan; layer adhesion, spring action, and microphone retention still need physical checks.\n\nI built the generator with build123d, a Python library for constructing solid CAD models. `DESK_T` sets desk thickness, `PRELOAD` controls clamp interference, `RING_ID` sets microphone fit, and `SLIT_W` sets the collet slit. `CRADLE_Y` and `CRADLE_Z` adjust reach and height. The README suggests tuning `PRELOAD` in 0.2 mm steps or `RING_ID` in 0.15 mm steps if the fit is too tight or loose.\n\nThe files include the Python generator, a STEP model (an exchange format for solid CAD geometry), and an STL print mesh. Regenerating after parameter changes lets me adjust the fit, but it does not establish the printed part’s grip or strength.\n\nThe [shelf files](<https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Bathroom%20Corner%20Shelf>) and [mic mount files](<https://github.com/DevanMetz/Devan-Metz-FreeCAD/tree/main/Microphone%20Desk%20Mount>) contain the source and printing notes. My [Parametric FreeCAD Designs](<https://devanmetz.com/portfolio/parametric-freecad/>) entry covers other editable parts in the same repository."
    },
    {
      "id": "printcad-brief",
      "title": "PrintCAD Brief: A Planner for 3D-Print CAD Work",
      "url": "https://devanmetz.com/portfolio/printcad-brief/",
      "markdownUrl": "https://devanmetz.com/portfolio/printcad-brief/index.md",
      "collection": "portfolio",
      "description": "A single-file browser tool for planning dimensions, fit tests, corner treatments, and model change requests before a full print.",
      "summary": "I built PrintCAD Brief to collect fit decisions before committing to a long print. It turns dimensions, geometry notes, and test history into a shareable build brief, with an STL viewer for marking specific model changes.",
      "date": null,
      "updated": "2026-10-15",
      "tags": [
        "JavaScript",
        "CAD",
        "3D printing",
        "three.js"
      ],
      "projectDate": {
        "start": "2026-10"
      },
      "category": "CAD & fabrication",
      "group": "hardware",
      "status": "Open source",
      "facts": [
        {
          "label": "My work",
          "value": "Browser planner, build-brief export, and click-to-review STL workflow"
        },
        {
          "label": "Tool",
          "value": "Single HTML file · JavaScript · three.js viewer"
        },
        {
          "label": "Status",
          "value": "Personal planning tool; no CAD generation or slicing"
        }
      ],
      "actions": [
        {
          "label": "Tool source",
          "url": "https://github.com/DevanMetz/Devan-Metz-FreeCAD/blob/main/CAD%20Print%20Planner/index.html"
        },
        {
          "label": "FreeCAD repo",
          "url": "https://github.com/DevanMetz/Devan-Metz-FreeCAD"
        }
      ],
      "cover": {
        "url": "https://devanmetz.com/_astro/printcad-brief.BMBMOHcX_LEnRf.webp",
        "alt": "Screenshot of the PrintCAD Brief web page: an empty 3D model viewer panel with Open STL and select face or edge buttons at the top, a project form and critical dimensions form filled in for a bathroom corner shelf on the left, and a dark Markdown build brief panel on the right.",
        "caption": "Screenshot of PrintCAD Brief with its example corner-shelf project loaded.",
        "width": 1200,
        "height": 1500
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "workshop-builds",
          "title": "From the Workbench",
          "url": "https://devanmetz.com/portfolio/workshop-builds/",
          "markdownUrl": "https://devanmetz.com/portfolio/workshop-builds/index.md"
        },
        {
          "id": "catan-organizers",
          "title": "Catan Organizers",
          "url": "https://devanmetz.com/portfolio/catan-organizers/",
          "markdownUrl": "https://devanmetz.com/portfolio/catan-organizers/index.md"
        },
        {
          "id": "printed-led-sculptures",
          "title": "Printed LED Sculptures",
          "url": "https://devanmetz.com/portfolio/printed-led-sculptures/",
          "markdownUrl": "https://devanmetz.com/portfolio/printed-led-sculptures/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built PrintCAD Brief to catch fit and dimension decisions before committing to a long 3D print. It gives me one place to record what a part needs to fit, which corners should change, and what a physical test showed before starting or revising a CAD (computer-aided design) model.\n\nThe built-in example uses my [bathroom corner shelf](<https://devanmetz.com/portfolio/home-desk-fixtures/>). Its dimensions and pending test entry demonstrate the planning workflow; they are not evidence of a successful print.\n\n## Dimensions and fit tests \n\nThe Project section records the purpose, material, printer, nozzle diameter, and bed dimensions. Critical dimensions captures the overall size, minimum wall thickness, measured mating part, modeled opening, and fit goal. The summary displays the entered footprint and height and calculates clearance—the opening size minus the mating-part size. It also compares the entered X and Y dimensions with the stated print bed.\n\nThe five-hole tolerance coupon is a plan for a small test print with several opening sizes. I choose a center opening and a step, and the tool lists five sizes around that center. The example’s 8.1 mm opening and 0.1 mm step give 7.90, 8.00, 8.10, 8.20, and 8.30 mm. It lists those dimensions for modeling and testing; it does not generate the coupon geometry.\n\n## Geometry and physical revisions \n\nGeometry language combines a checklist for inside and outside corners, wall-contact faces, and print orientation with written corner treatments and constraints to preserve. I can specify a chamfer (a beveled edge) or fillet (a rounded edge) while keeping details such as rod location, tier spacing, and wall thickness explicit.\n\nThe Print-test log has editable rows for a date, the change tested, and a result or measurement. I can add and remove tests as the design changes. The example records a rod-hole reduction from 8.5 to 8.1 mm with the result “Awaiting next test print.”\n\n## Click-to-review model \n\nI can open or drop an STL, a triangle-mesh file commonly used for 3D printing, then orbit, zoom, and pan around it. Face mode marks an individual mesh triangle; edge mode marks an edge segment. These selections refer to the mesh, rather than editable CAD features.\n\nAfter selecting geometry, I write what should change. “Copy change request” includes the model filename, selection number, point coordinates, and either a face normal (its direction) or edge endpoints, followed by my note. Coordinates refer to the STL’s original model space. “Download marked PNG” saves the viewer image with the selection highlighted, so I can share a visual reference alongside the text request.\n\n## Sharing and storage \n\nThe Shared build brief updates as I edit the forms. I can copy it, download it as Markdown (plain text with formatting), or use the browser’s print dialog. That gives me a written specification to use alongside a CAD script or send to a collaborator, including fit sizes, geometry constraints, test history, and an acceptance checklist.\n\nForm values, checked features, and test rows save automatically in localStorage, the browser’s local storage for a page. There is no server save or device sync, and the saved brief does not include the loaded STL or viewer selection. I export the brief before switching browsers or devices.\n\n## Running it and limits \n\nThe [tool source](<https://github.com/DevanMetz/Devan-Metz-FreeCAD/blob/main/CAD%20Print%20Planner/index.html>) is a standalone HTML file in my FreeCAD repository. Download it and open it in a browser; there is no build step. The 3D viewer uses three.js, a browser graphics library loaded from a CDN (an external file delivery service), so that part needs an internet connection. When opened as a local file, I choose the STL myself.\n\nThis is a personal planning and review tool. It does not generate CAD, modify the loaded mesh, or slice a model into printer instructions. Its bed check compares entered dimensions, and the exported acceptance checklist is a reminder for review, not automated verification of wall thickness, watertight geometry, or physical fit.\n\nMy [Parametric FreeCAD Designs](<https://devanmetz.com/portfolio/parametric-freecad/>) entry covers editable models in the same repository."
    },
    {
      "id": "frontier-colony",
      "title": "Frontier Colony",
      "url": "https://devanmetz.com/portfolio/frontier-colony/",
      "markdownUrl": "https://devanmetz.com/portfolio/frontier-colony/index.md",
      "collection": "portfolio",
      "description": "A Roblox colony-survival prototype with autonomous settlers, shared construction, seasonal survival, and a growing simulated world.",
      "summary": "I built Frontier Colony as a private Roblox colony-survival prototype with autonomous settlers, shared construction, and seasonal survival. I work on the simulation, interface, and procedural 3D art to make the growing settlement understandable and support cooperative editing.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Luau",
        "Roblox",
        "Simulation",
        "Interface design"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Game development",
      "group": "games",
      "status": "Private prototype",
      "facts": [
        {
          "label": "My work",
          "value": "Luau simulation, interface, and procedural 3D art"
        },
        {
          "label": "Platform",
          "value": "Roblox · cooperative colony management"
        },
        {
          "label": "Access",
          "value": "Private playable prototype"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/frontier-colony.DokEqa9X_Z2bcDSP.webp",
        "alt": "Frontier Colony promotional artwork showing colonists farming and building a settlement beside a forest river.",
        "caption": "Existing AI-generated promotional artwork for Frontier Colony, illustrating the game's setting and mechanics.",
        "width": 1200,
        "height": 675
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "goop-troops",
          "title": "Goop Troops",
          "url": "https://devanmetz.com/portfolio/goop-troops/",
          "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md"
        },
        {
          "id": "openrp",
          "title": "OpenRP",
          "url": "https://devanmetz.com/portfolio/openrp/",
          "markdownUrl": "https://devanmetz.com/portfolio/openrp/index.md"
        },
        {
          "id": "poker-with-friends",
          "title": "Poker With Your Friends",
          "url": "https://devanmetz.com/portfolio/poker-with-friends/",
          "markdownUrl": "https://devanmetz.com/portfolio/poker-with-friends/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built Frontier Colony as a Roblox (a platform for creating and playing games) colony-survival prototype centered on autonomous settlers (colonists who carry out work on their own). Players establish a settlement, organize work, grow food, prepare shelter, research equipment, and respond to threats.\n\nThe project uses original Luau (Roblox’s scripting language) code and procedural 3D art (models created through code). Colony-management games such as RimWorld (games about organizing settlers’ work and survival) inspired the loop; their assets and proprietary code are not part of the project.\n\n## Starting a colony \n\n### Map and starting conditions \n\nA new colony can use a standard start or a customized map, population, and supply kit. The setup includes:\n\n-   Terrain and climate choices.\n-   A starting season and difficulty.\n-   Colonist skills.\n\nThe terrain preview and playable map use the same generator. Map settings and starting conditions are saved with the colony.\n\n### Starting time \n\nPlayers begin with the simulation (the running model of the colony) paused and can inspect their people and resources before starting time. Each player’s colony has its own map, inventory, research, raids (attacks on the settlement), and clock.\n\n## People, work, and survival \n\n### Needs and priorities \n\nColonists operate within a network of needs and available work. Skills, schedules, equipment, health, relationships, and assigned priorities affect how the settlement functions.\n\nThe player has to connect those systems in practical ways: food needs storage and preparation, injured people need beds and supplies, and winter shelter needs usable rooms and heat. Crops and planted trees interact with soil, seasons, temperature, and regrowth.\n\n### Production and supplies \n\nProduction orders (instructions to make goods) and hauling (moving supplies) link raw resources to usable goods. A stock ledger (an inventory record) and searchable workshop view expose that flow so a missing item can be traced back to its source or destination.\n\n## Construction and infrastructure \n\n### Buildings and utilities \n\nConstruction includes material choices, research requirements, blueprints (plans for buildings), grouped placement, and dismantling.\n\nRooms track contents, cleanliness, and quality; power circuits (connected electrical systems) and temperature controls connect the built environment to survival.\n\n### Shared policies and the wider world \n\nStorage, food, clothing, and work policies (rules for colony behavior) are shared systems rather than isolated settings on each object. Assignments and default inheritance (using shared settings unless overridden) help a colony behave consistently as its population grows.\n\nProcedural terrain (land generated through code), wildlife, research, combat, quests, and travel give the settlement a world beyond its immediate building area. These systems are in active development, so repository behavior can be ahead of the last private Roblox release.\n\n## Interface and cooperative editing \n\n### Finding the right information \n\nA large part of my work is making the simulation understandable. Search locates people, objects, building plans, and controls. Linked inspectors (detail panels for selected items) let the player move from an alert to the relevant person, room, policy, or production order.\n\nThe interface supports desktop and touch layouts as well as keyboard navigation and map controls. Long lists reflow and scroll rather than depending on increasingly small text.\n\n### Reviewing shared changes \n\nCooperative editing creates a second state problem (keeping track of current and unfinished changes): a person’s unfinished settings draft may coexist with changes from another player. Policy and infrastructure editors preserve unsent drafts, expose conflicts, and ask for a review before applying an outdated change.\n\n## Persistence and testing \n\nRoblox colonies save and reload across servers. Ordinary Studio (Roblox’s development editor) previews use temporary colonies so local experiments do not overwrite the owner’s published progress. The place file (the Roblox project file containing the playable world) is self-contained for local play in Studio.\n\nDevelopment tests cover simulation behavior, saved-state recovery (restoring stored colony data), and interface consistency. Multiplayer checks include shared edits and repeated navigation between panels.\n\n## Current access and imagery \n\nFrontier Colony is a private playable prototype. Both the repository and Roblox experience remain private.\n\nThe cover is AI-generated promotional artwork depicting the intended setting, rather than a gameplay screenshot."
    },
    {
      "id": "this-site",
      "title": "devanmetz.com",
      "url": "https://devanmetz.com/portfolio/this-site/",
      "markdownUrl": "https://devanmetz.com/portfolio/this-site/index.md",
      "collection": "portfolio",
      "description": "My portfolio and project wiki, built around real build photos, Markdown documentation, and a small Cloudflare deployment.",
      "summary": "I built this site to bring together my software, hardware, physical builds, and writing in a portfolio that also serves as my project wiki. I keep working details, supporting photos, references, and build stories connected so the project material stays together.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Astro",
        "TypeScript",
        "Markdown",
        "Cloudflare"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Web design & publishing",
      "group": "tools",
      "status": "Live website",
      "facts": [
        {
          "label": "Purpose",
          "value": "Portfolio, project documentation, and build stories"
        },
        {
          "label": "Stack",
          "value": "Astro static pages + a Cloudflare contact handler"
        },
        {
          "label": "Publishing",
          "value": "Private source repository; public website"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/this-site.DGnYy_GM_Z1dlPB2.webp",
        "alt": "The devanmetz.com homepage with picture cards for OpenFloat Archery, Goop Troops, and a custom electric bicycle.",
        "caption": "The homepage during the project-wiki redesign, using original project imagery.",
        "width": 1200,
        "height": 750
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        },
        {
          "id": "market-atlas",
          "title": "Market Atlas",
          "url": "https://devanmetz.com/portfolio/market-atlas/",
          "markdownUrl": "https://devanmetz.com/portfolio/market-atlas/index.md"
        },
        {
          "id": "spritefield",
          "title": "Spritefield",
          "url": "https://devanmetz.com/portfolio/spritefield/",
          "markdownUrl": "https://devanmetz.com/portfolio/spritefield/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nThis site brings together my software, hardware, physical builds, and writing. The portfolio is also the project wiki: each picture card opens a page with the project’s working details, supporting photos, references, and current state.\n\nThe blog remains a place for build stories. Those posts link back to their project pages, where the reference material stays together.\n\n## Content and page structure \n\n### Project documentation \n\nProjects and posts are Markdown (plain text with formatting markers) files in Astro (a framework for building websites) content collections (groups of content files with a shared structure). Frontmatter (metadata at the start of a content file) describes the title, summary, category, tags, publishing state, images, and project facts. The rest of the file holds the documentation.\n\nProject headings generate a contents list automatically. Build photos and linked blog stories join that navigation when present. The same project data supplies the homepage cards, portfolio grid, detail page, and social preview.\n\n### Keeping old links working \n\nThe former standalone wiki notes have moved into the appropriate projects. Old URLs redirect to the new page or the relevant heading so existing links continue to work.\n\n## Images and browsing \n\n### Cards and search \n\nThe portfolio uses one consistent card layout for every project. Category filters help browse games, hardware, props, tools, and writing; all cards are present in the initial HTML (the page’s document markup) before JavaScript (browser code) runs.\n\nThe homepage shows nine selected projects and a search field for the whole site. Search checks project wikis, blog posts, and the other public pages, including build details and video transcripts.\n\nIt handles partial words and small spelling mistakes, then shows matching text and a thumbnail. The index is rebuilt from the published pages with each deployment, and searches run in the browser.\n\n### Image formats and accessible design \n\nOriginal build photos, application captures, and credited project artwork are kept with the source. Astro generates responsive WebP images (a compressed image format, sized for different screens) for pages and JPEG images (another compressed image format) for social previews during the build.\n\nThe design uses self-hosted Space Grotesk (a font served with the site), warm paper colors, blue actions, compact spacing, and responsive layouts. Keyboard focus, heading order, descriptive image text, and reduced-motion behavior (respecting a preference for less animation) are part of the implementation.\n\n## Publishing and privacy \n\nThe GitHub source repository is private, while published pages are public. Draft content is excluded from listings and generated routes. Future-dated entries remain excluded until a build runs after the publication date.\n\nThe build produces a sitemap (a list of site URLs for search engines) and a blog RSS feed (a subscription feed for new posts) alongside the HTML. Each page has a canonical URL (its preferred web address) and social metadata (information used in shared-link previews).\n\n## Hosting and the contact form \n\n### Serving the published pages \n\nCloudflare Workers Static Assets (Cloudflare’s hosting service for built files) serves the generated site at devanmetz.com. Page rendering happens during the Astro build, so browsing the portfolio does not require a content database or per-request page rendering.\n\n### Contact delivery \n\nA small Worker (code running on Cloudflare’s servers) handles the homepage contact form. It validates submissions, verifies Turnstile (Cloudflare’s automated-abuse check), applies a request limit, and sends an email through Cloudflare. The delivery address and private verification key stay out of the generated page and browser script.\n\nThe form preserves entered text if delivery fails and reports success after the email service accepts the message. The separate Contact URL redirects to the form at the bottom of the homepage.\n\n## Reading with an AI agent \n\nThe [agent index](<https://devanmetz.com/llms.txt>) links to a Markdown version of every project and blog post. The [full-text download](<https://devanmetz.com/llms-full.txt>) puts the whole collection in one file, and the [JSON export](<https://devanmetz.com/portfolio.json>) (a structured data format) provides structured project details for other tools.\n\nThese files are generated from the same published content as the website. They include project facts, photos and captions, video links, and available transcripts. Each document links back to its regular web page for citations.\n\n## Checks and maintenance \n\nChanges pass Astro and TypeScript (JavaScript with type checks) checks, contact-handler tests, and a production build. Static link checks cover page references and section anchors (links to headings within a page). Desktop and mobile browser checks cover the pages affected by a change.\n\nGitHub Actions (automated repository workflows) repeats the checks and build on each push. The finished site is then deployed to Cloudflare.\n\nReference documentation: [Astro content collections](<https://docs.astro.build/en/guides/content-collections/>) and [Cloudflare Static Assets](<https://developers.cloudflare.com/workers/static-assets/>)."
    },
    {
      "id": "poker-with-friends",
      "title": "Poker With Your Friends",
      "url": "https://devanmetz.com/portfolio/poker-with-friends/",
      "markdownUrl": "https://devanmetz.com/portfolio/poker-with-friends/index.md",
      "collection": "portfolio",
      "description": "A private-table Texas Hold’em game with invite links, spectators, practice bots, and a Discord Activity build.",
      "summary": "I built Poker With Your Friends as a real-time Texas Hold’em table for two to six people playing with play money. I support private invite links, spectators, practice bots, and reconnect-safe rooms so friends can keep a full table game going in the browser or a Discord Activity build.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "TypeScript",
        "React",
        "WebSockets",
        "Multiplayer"
      ],
      "projectDate": {
        "start": "2026-07"
      },
      "category": "Game development",
      "group": "games",
      "status": "Private prototype",
      "facts": [
        {
          "label": "My work",
          "value": "Game rules, realtime server, and web client"
        },
        {
          "label": "Platform",
          "value": "Browser · Discord Activity support"
        },
        {
          "label": "Table",
          "value": "2–6 players · invite links · practice bots"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/poker-with-friends.BXwKxhwR_ZuSYVa.webp",
        "alt": "A Texas Hold’em table in progress with hole cards, community cards, the pot, and the active players’ action buttons.",
        "caption": "A table in play, with the shared board, pot total, and per-player controls.",
        "width": 1200,
        "height": 675
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/poker-artwork.B8V5Psiw_Z2cCNxF.webp",
          "alt": "Illustrated cover artwork with five cartoon players seated around a poker table under a dark blue light.",
          "caption": "Storefront cover artwork for the game’s listing.",
          "width": 1200,
          "height": 675
        },
        {
          "url": "https://devanmetz.com/_astro/poker-activity.BKlNETyi_1wdpmr.webp",
          "alt": "Wide illustrated scene of cartoon characters playing poker, with emoji speech bubbles behind the table.",
          "caption": "Activity background artwork for the Discord embedded app.",
          "width": 1200,
          "height": 675
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "goop-troops",
          "title": "Goop Troops",
          "url": "https://devanmetz.com/portfolio/goop-troops/",
          "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md"
        },
        {
          "id": "openrp",
          "title": "OpenRP",
          "url": "https://devanmetz.com/portfolio/openrp/",
          "markdownUrl": "https://devanmetz.com/portfolio/openrp/index.md"
        },
        {
          "id": "kickr-core-hub",
          "title": "KICKR CORE 2 Hub",
          "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
          "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI built Poker With Your Friends as a real-time Texas Hold’em (poker with two private cards per player and five shared cards) table for two to six people. It is built for play money among friends: a host creates a room, shares an invite link, and everyone plays from the browser.\n\nThe game keeps a full table experience rather than a single heads-up (two-player) hand. Players can join with private invite links, watch as spectators, and continue through reconnect-safe rooms with session stats (statistics for the play session) and emoji reactions.\n\nA Discord Activity (an app embedded in Discord) build lets the same game run inside a Discord server.\n\n## Table rules and play \n\n### Dealing and betting \n\nThe server deals every hand and resolves all betting, from blinds (required bets before the cards are played) and turn order to side pots (separate pots for players eligible to win different amounts) when players are all-in (betting all their remaining chips) for different amounts.\n\nThe client renders the table, cards, chips, and action buttons, and sends player actions back to the server.\n\n### Saving and reconnecting \n\nRooms persist as JSON snapshots (saved room data in a structured text format), so a dropped connection or a server restart does not lose an in-progress game.\n\nOn a Railway (an application hosting platform) volume (persistent storage), the room state survives redeploys; the Docker image (a packaged application and its runtime) runs the same server with a single data file.\n\n## Practice bots \n\nFive bot personalities (computer-controlled players) fill out a table when friends are not around:\n\n-   Bold.\n-   Steady.\n-   Careful.\n-   Tricky.\n-   Loose.\n\nEach style has its own tendencies for folding (leaving a hand), calling (matching a bet), bluffing (betting to suggest a stronger hand), and raising (increasing a bet), so a practice game plays differently depending on who is seated.\n\n## Discord Activity \n\nThe game ships as a Discord Activity using the embedded-app SDK (software development kit for apps running inside Discord). A Discord application maps the game’s public hostname, and the activity reuses the same realtime server. The plain browser game keeps working when the Discord environment variables (runtime configuration values) are absent.\n\n## Architecture and testing \n\n### Client and server \n\nThe client is React (a library for browser interfaces) and TypeScript (JavaScript with type checks) built with Vite (a development and build tool). The authoritative game server (the source of truth for game rules and state) is a single Node (a JavaScript runtime outside the browser) process that serves the client and hosts the WebSocket (a persistent two-way connection) rooms.\n\n### Deployment and checks \n\nThe Dockerfile (instructions for building the Docker image) plus a Railway service and custom domain are the deployment path, with anonymous beta funnel totals (aggregate counts through beta participation steps) available at the `/metrics` endpoint (a server address for requests).\n\nThe rules live in a shared game module with its own test file, and the server has a separate integration test (checking connected components together) that drives rooms, actions, and reconnection behavior over the network.\n\n## Current state \n\nPoker With Your Friends is a private prototype. The repository is private and there is no public deployment, so the table is currently limited to local and self-hosted play. All play is for play money."
    },
    {
      "id": "derp",
      "title": "DERP",
      "url": "https://devanmetz.com/portfolio/derp/",
      "markdownUrl": "https://devanmetz.com/portfolio/derp/index.md",
      "collection": "portfolio",
      "description": "A self-hosted ERP, PLM, and QMS that one company runs for itself — accounting, inventory, manufacturing, quality, and a storefront in one Django application.",
      "summary": "I designed and implemented DERP so one company can manage its accounting, inventory, manufacturing, product changes, quality records, and storefront in one application. Each company runs its own installation and database; the screenshots here show sample records from a disposable demo.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Python",
        "Django",
        "PostgreSQL",
        "Manufacturing"
      ],
      "projectDate": {
        "start": "2026-05"
      },
      "category": "Business software",
      "group": "tools",
      "status": "Private software",
      "facts": [
        {
          "label": "My work",
          "value": "Design and implementation of every module"
        },
        {
          "label": "Stack",
          "value": "Django 5.2 · PostgreSQL · Stripe Checkout"
        },
        {
          "label": "Scope",
          "value": "One company per deployment"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/derp-dashboard.Ca3hNfAe_Z239wPv.webp",
        "alt": "An operational analytics dashboard showing revenue, cost, and working-capital figures above a revenue-versus-expenses chart and an inventory allocation chart.",
        "caption": "The operational dashboard, shown with data from a seeded demo installation.",
        "width": 1200,
        "height": 750
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/derp-boms.CgUKM1jx_tIMoP.webp",
          "alt": "A bills-of-materials list showing a product SKU, its assembly recipe, released revision, component count, and cost rollup.",
          "caption": "Bill-of-materials records carry a released revision, component list, and costing rollup for each manufactured product.",
          "width": 1200,
          "height": 750
        },
        {
          "url": "https://devanmetz.com/_astro/derp-changes.BAJaS-T5_NEroV.webp",
          "alt": "An engineering change order list with the change summary, type, priority, owner, and draft status.",
          "caption": "Engineering change orders gate product, BOM, quality, supplier, and obsolescence changes behind approvals.",
          "width": 1200,
          "height": 750
        },
        {
          "url": "https://devanmetz.com/_astro/derp-qms.__iaEbTT_L66oi.webp",
          "alt": "A controlled-documents list with a document number, title, type, owner, status, and review date.",
          "caption": "Document control supports versioned procedures with review dates and approval states.",
          "width": 1200,
          "height": 750
        },
        {
          "url": "https://devanmetz.com/_astro/derp-journal.7NjRVxg-_1yOUlF.webp",
          "alt": "A journal-entry list linking documents such as payments, invoices, manufacturing completions, and bills to accounting entries.",
          "caption": "Posted business documents flow into double-entry accounting instead of a separate bookkeeping step.",
          "width": 1200,
          "height": 750
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "dean-android-agent",
          "title": "Dean Android Agent",
          "url": "https://devanmetz.com/portfolio/dean-android-agent/",
          "markdownUrl": "https://devanmetz.com/portfolio/dean-android-agent/index.md"
        },
        {
          "id": "devbot",
          "title": "DevBot",
          "url": "https://devanmetz.com/portfolio/devbot/",
          "markdownUrl": "https://devanmetz.com/portfolio/devbot/index.md"
        },
        {
          "id": "solar-trail-camera",
          "title": "Solar Cellular Trail Camera",
          "url": "https://devanmetz.com/portfolio/solar-trail-camera/",
          "markdownUrl": "https://devanmetz.com/portfolio/solar-trail-camera/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed and implemented every module in DERP, an integrated ERP (enterprise resource planning, which connects day-to-day business records), PLM (product lifecycle management, which tracks product revisions and changes), and QMS (quality management system, which tracks procedures and quality work).\n\nOne organization runs it for itself. A single deployment holds one company’s data: accounting, inventory, sales, purchasing, manufacturing, product lifecycle management, quality management, a company website, and a storefront.\n\n### One company per installation \n\nI started the application as a hosted multi-customer platform and restructured it into a self-hosted one. One database now represents one company with its own users, records, website, and Stripe configuration. There is no tenant routing (choosing a customer’s data from a shared installation) or per-customer schema (a separate database structure for each customer).\n\n## What it covers \n\n### Business records and manufacturing \n\n| Area | What it handles |\n| --- | --- |\n| Accounting | Double-entry ledger (each transaction has matching debit and credit entries), posted documents (finalized business records), payments, and trial balance (debit/credit totals), income statement (income and expenses), balance sheet (assets, liabilities, and equity), and general-ledger reports (account transaction details) |\n| Inventory | Weighted-average costing (average unit cost weighted by quantities), warehouses, lots (batches), serials (individual item identifiers), a stock ledger (inventory movement record), and transfers between locations |\n| Sales and purchasing | Customers, sales orders, invoices, vendors, purchase orders, bills, and their posted workflows |\n| Manufacturing | Bills of materials, or BOMs (component lists for an assembly), with cost rollups (combined component costs) and manufacturing orders that consume components into finished stock |\n| PLM | Product revisions, engineering BOMs, and approval-gated engineering change orders, or ECOs (requests to approve product changes), with effectivity (when a change applies) |\n| QMS | Document control, exact-revision training evidence, inspections, non-conformances (recorded failures to meet requirements), CAPA (corrective and preventive action) with effectiveness verification, and scored risk controls |\n| Website and storefront | A built-in website editor and public shop served by the same installation |\n| Copilot and Agent Hub | AI assistance with a preview-and-confirm step before any database write |\n\n### Quality records \n\nMy QMS document workflows follow ISO 9001 / ISO 13485 conventions (quality-management standards), including review dates, approval states, and training records tied to a specific document revision.\n\n## Architecture \n\n### Application and navigation \n\nI built DERP with Django 5.2 LTS (a Python web framework with long-term support) and PostgreSQL (a relational database). The public website lives at the site root and the ERP interface lives under `/derp/`. An administrator creates accounts rather than using public signup.\n\nThe interface targets the same records from several angles:\n\n-   A workspace overview.\n-   An operational analytics dashboard.\n-   Per-module lists and detail pages.\n\n### Hosting and optional payments \n\nDeployment follows standard Django practice: migrations (database structure updates), static collection (gathering website assets), and a Gunicorn process (the application’s web server). It can run on Railway or any host with PostgreSQL.\n\nStripe is optional. Without keys, checkout completes manually for development. With keys, the storefront creates Stripe Checkout Sessions (hosted payment sessions) directly in the hosting company’s Stripe account and fulfills paid orders into sales and accounting records.\n\n## Storefront and payments \n\nI keep the storefront in the same application rather than a separate service. Paid checkouts are verified by Stripe webhook signature (proof that the payment notification came from Stripe), then written back as sales and accounting entries. That keeps the online shop inside the same ledger as the rest of the company’s business.\n\n## Copilot and Agent Hub \n\nMy Copilot keeps its API key (the credential for accessing the AI service) in the user’s browser. It requires a signed preview and explicit confirmation before it writes to the database.\n\nAgent Hub stores reusable, user-owned prompts that a person opens and sends through the Copilot. It does not run unattended jobs.\n\n## Development workflow \n\n### Dependencies \n\nI pin direct and transitive dependencies (packages I use and the packages they depend on) with hashes (file fingerprints). I regenerate them through a lock step with `uv`, the Python package-management tool.\n\n### Testing \n\nThe test suite runs against PostgreSQL through a dedicated test settings module. GitHub Actions (automated repository checks) runs migrations, static collection, and the full suite on PostgreSQL 18 with Python 3.11 and 3.13.\n\nA separate SQLite (a file-based database) test setting exists for quick local runs.\n\n## Current state and demo imagery \n\nDERP is private software. The repository is not public, and there is no public demo installation.\n\nThe screenshots on this page come from a disposable demo deployment seeded with sample records, not from a live company database. The figures, orders, and documents shown are demo data."
    },
    {
      "id": "automated-3d-printing-cell",
      "title": "Automated 3D-Printing Cell",
      "url": "https://devanmetz.com/portfolio/automated-3d-printing-cell/",
      "markdownUrl": "https://devanmetz.com/portfolio/automated-3d-printing-cell/index.md",
      "collection": "portfolio",
      "description": "A six-axis robot arm designed to tend a 3D-printing cell, released as a staged first-article package with printable structural parts and a documented acceptance plan.",
      "summary": "I designed a six-axis robot arm to reach into a Bambu A1 printer and pick printed parts. I released the design, printable parts, purchase notes, and a staged testing plan, but no arm has been built and it is not approved for unattended operation.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Robotics",
        "OpenSCAD",
        "Machine design",
        "Manufacturing"
      ],
      "projectDate": {
        "start": "2026-07"
      },
      "category": "Robotics & automation",
      "group": "hardware",
      "status": "Engineering prototype",
      "facts": [
        {
          "label": "My work",
          "value": "Mechanical design, sourcing, and release package"
        },
        {
          "label": "Target",
          "value": "6 axes · 574 mm reach · 0.50 kg payload"
        },
        {
          "label": "Status",
          "value": "First-article package released · not yet built"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/printing-cell-arm.aVtn0kQ5_2fxdlo.webp",
        "alt": "A rendered six-axis robot arm with a blue base, black carbon-tube links, and yellow pulley housings.",
        "caption": "The R3 arm assembly rendered from the released OpenSCAD model.",
        "width": 1200,
        "height": 1440
      },
      "gallery": [
        {
          "url": "https://devanmetz.com/_astro/printing-cell-preview.Be-J0ba__1CvaAW.webp",
          "alt": "A full-height render of the six-axis arm showing the base yaw, shoulder, elbow, and wrist joints.",
          "caption": "The arm stands 574 mm from shoulder to tool flange, with a harmonic reducer at the shoulder and elbow.",
          "width": 1200,
          "height": 1440
        },
        {
          "url": "https://devanmetz.com/_astro/printing-cell-wrist.Ct338efK_Z11ftSa.webp",
          "alt": "A close render of the three-axis wrist with pulleys, side plates, and bearing bores.",
          "caption": "The J4–J6 wrist uses belt-driven axes in printed PA-CF or PET-CF housings, with an encoder on each output.",
          "width": 1200,
          "height": 1000
        }
      ],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "reflector-lantern",
          "title": "Reflector Lantern",
          "url": "https://devanmetz.com/portfolio/reflector-lantern/",
          "markdownUrl": "https://devanmetz.com/portfolio/reflector-lantern/index.md"
        },
        {
          "id": "derp",
          "title": "DERP",
          "url": "https://devanmetz.com/portfolio/derp/",
          "markdownUrl": "https://devanmetz.com/portfolio/derp/index.md"
        },
        {
          "id": "openfloat-archery",
          "title": "OpenFloat Archery",
          "url": "https://devanmetz.com/portfolio/openfloat-archery/",
          "markdownUrl": "https://devanmetz.com/portfolio/openfloat-archery/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nI designed this six-axis robot arm (an arm with six controlled joint motions) to tend a 3D-printing cell, a printer work area served by the robot.\n\nThe current revision, R3, is released as a first-article prototype package (the package for building and checking the first physical examples). It contains:\n\n-   A frozen requirement set.\n-   A printable structural parts set.\n-   A purchase list.\n-   A gate list: acceptance gates are checks that must pass before moving to the next stage, and these still require physical validation.\n\nIt is not approved for unattended operation, and no arm has been built yet.\n\n### Starting from the printer \n\nI started the design work from the printer rather than from a generic robot. The arm has to reach into a Bambu A1 build volume (the space where the printer makes parts), pick printed parts, and stay inside the machine’s work zone.\n\n## Requirements \n\nThe release freezes the numbers I hold the design to. Reach is the shoulder-to-tool distance; payload is the load carried by the arm, including its gripper and plate. Repeatability means returning to the same position across repeated moves.\n\n| Requirement | Release target |\n| --- | --- |\n| Controlled axes | 6 |\n| Shoulder-to-tool reach | 574 mm |\n| Working payload | 0.50 kg including gripper and plate |\n| Repeatability in the printer work zone | ≤1.0 mm after calibration |\n| Full-workspace repeatability | ≤2.0 mm |\n| Maximum tool speed during initial validation | 100 mm/s |\n| Duty cycle during initial validation | 20% |\n| Minimum continuous torque safety factor | 2.0 |\n| Structural yield safety factor | 3.0 |\n| Position feedback | Direct output encoder on every axis |\n\nDuty cycle is the share of time spent operating. The torque and structural yield safety factors describe margins against the required turning force and permanent material deformation. An encoder is a sensor that measures joint position.\n\n## Mechanism \n\n### Joint drives and supports \n\nI paired each axis with a drive and a structural interface that can actually be manufactured. Yaw turns side to side, pitch tilts, and roll rotates the tool around its own axis. A harmonic reducer is a compact gear mechanism that reduces speed and increases turning force.\n\nIn the table, HTD-5M and HTD-3M identify toothed-belt profiles with 5 mm and 3 mm tooth spacing; T denotes tooth counts. ASA/PETG are printing plastics, PA-CF/PET-CF are carbon-fiber-filled nylon/polyester printing materials, and 6061-T6 is an aluminum alloy and temper.\n\n| Axis | Mechanism | Structural interface |\n| --- | --- | --- |\n| J1 yaw | 6:1 HTD-5M belt on two 6008 bearings | Printed ASA/PETG base on a steel bench plate |\n| J2 shoulder | 50:1 harmonic reducer | 6 mm 6061-T6 fixed adapter |\n| J3 elbow | 50:1 harmonic reducer | Printed PA-CF/PET-CF adapter |\n| J4 pitch | 16T:80T HTD-3M belt | Printed wrist housing |\n| J5 yaw | 16T:64T HTD-3M belt | Printed wrist housing |\n| J6 roll | 16T:48T HTD-3M belt | Printed wrist housing |\n\n### Links, counterbalance, and feedback \n\nThe upper and forearm links are pairs of 18 × 1.5 mm and 16 × 1.5 mm carbon tubes clamped in printed end blocks.\n\nThe shoulder carries an 8 Nm gas-spring counterbalance (a spring that offsets part of the load) against a checked horizontal torque of about 14.3 Nm. Nm, or newton-metres, measures turning force. The J2 motor has an integrated power-off brake.\n\nEvery axis ends in a direct output encoder so position feedback comes from the joint rather than from the motor.\n\n## Manufacturing package \n\n### Model and exported parts \n\nI generate the assembly from source using OpenSCAD, a code-based 3D modeling tool. `cad/robot_arm_assembly.scad` is the authoritative model. An export script produces the release set:\n\n-   Printable STLs (3D surface files) for the base, rotor, shoulder pedestal, elbow motor bell, wrist side plates, pulleys, tool flange (the tool mounting face), and both tube end-block sizes.\n-   DXF profiles (2D drawing files) for the aluminum plates.\n\nI regenerate the export rather than storing it, so obsolete geometry cannot silently ship.\n\n### Inspecting before duplicating \n\nI deliberately staged the design around one purchased harmonic reducer. The release notes require buying and inspecting a single unit, then freezing the adapter pilot fits (locating surfaces) and bolt circle (bolt-hole arrangement) from the delivered part instead of nominal catalog dimensions.\n\nThe next step is producing one first-article part per geometry and recording bore sizes (hole diameters) and mass before duplicating anything.\n\n## Verification \n\n### Checks before a physical build \n\nThe repository keeps the checks I can do before metal is cut: collision sweeps across joint angles (checking whether parts intersect as joints move), wrist kinematics views (showing joint motion), and an engineering check script over the assembly.\n\n### Hardware acceptance plan \n\nThe acceptance plan still has to be run on real hardware. It includes:\n\n-   Reducer flange runout (wobble of the mounting face) under 0.10 mm.\n-   Encoder repeatability over 20 cycles.\n-   A static proof load of 1.0 kg for 60 seconds.\n-   A 30-minute thermal soak (sustained operation to check heating) at validation duty.\n-   An E-stop (emergency stop) test.\n-   The 30-approach repeatability test in the printer work zone.\n\n### Accuracy limits \n\nMy release is explicit that the 1 mm repeatability target is not proven by component specifications. The chosen encoders resolve about 0.22 mm per count (sensor increment) at full reach, and their specified absolute accuracy is worse than that.\n\nCalibration can remove repeatable error, but the target is only accepted by the physical approach test.\n\n## Earlier concept \n\nThe repository also holds my earlier R0 concept: a dual-arm cell built from a Bambu A1 mini and two low-cost arms. That concept includes:\n\n-   An E-stop and contactor interlock (an electrical switching arrangement that prevents operation when the interlock is open).\n-   A taught-pose state machine (control logic that moves through recorded arm positions).\n-   A documented budget.\n\nThat document is archived history and does not describe the active R3 design.\n\n## Current state \n\nThis is an engineering prototype. My design package, purchase notes, and validation gates are complete enough to order a staged first-article build, but nothing has been manufactured or measured. The repository is private."
    },
    {
      "id": "wurkkos-dl46-repair",
      "title": "Wurkkos DL46 Battery Repair",
      "url": "https://devanmetz.com/portfolio/wurkkos-dl46-repair/",
      "markdownUrl": "https://devanmetz.com/portfolio/wurkkos-dl46-repair/index.md",
      "collection": "portfolio",
      "description": "Bringing back a secondhand flashlight after someone pulled out its built-in battery, by reinstalling the cell and resoldering its leads.",
      "summary": "I repaired a Wurkkos DL46 flashlight bought as-is on eBay after someone tore out its built-in battery. I reinstalled the cell, resoldered its leads with the correct polarity, and reseated the rubber O-ring before closing the working light back up.",
      "date": null,
      "updated": "2026-10-14",
      "tags": [
        "Repair",
        "Soldering",
        "Disassembly",
        "Li-ion"
      ],
      "projectDate": {
        "start": "2026-09"
      },
      "category": "Electronics repair",
      "group": "hardware",
      "status": "Repaired and working",
      "facts": [
        {
          "label": "My work",
          "value": "Teardown, cell reinstall, tab soldering, and reassembly"
        },
        {
          "label": "Device",
          "value": "Wurkkos DL46 · board DL46_C-B0"
        },
        {
          "label": "Source",
          "value": "Bought as-is on eBay with the battery torn out"
        }
      ],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/dl46-disassembled.CfNrna6p_FYoLU.webp",
        "alt": "A blue cylindrical battery with a metal tab at one end, next to the flashlight's black front cover and its round driver board with MOSFETs and an inductor.",
        "caption": "The cell, front cover, and driver board separated after the battery was ripped out.",
        "width": 1200,
        "height": 1237
      },
      "gallery": [],
      "media": [],
      "project": null,
      "stories": [],
      "related": [
        {
          "id": "openfloat-archery",
          "title": "OpenFloat Archery",
          "url": "https://devanmetz.com/portfolio/openfloat-archery/",
          "markdownUrl": "https://devanmetz.com/portfolio/openfloat-archery/index.md"
        },
        {
          "id": "diy-electric-bike",
          "title": "DIY Electric Bike",
          "url": "https://devanmetz.com/portfolio/diy-electric-bike/",
          "markdownUrl": "https://devanmetz.com/portfolio/diy-electric-bike/index.md"
        },
        {
          "id": "printed-violin",
          "title": "3D-Printed Rainbow Violin",
          "url": "https://devanmetz.com/portfolio/printed-violin/",
          "markdownUrl": "https://devanmetz.com/portfolio/printed-violin/index.md"
        }
      ],
      "contentMarkdown": "## Overview \n\nThe Wurkkos DL46 is a flashlight with a built-in Li-ion (lithium-ion) battery that isn’t meant to come out. I bought one on eBay sold as-is because someone had ripped the battery out of it. Its leads had been pulled off the cell, leaving the battery loose from the driver board, the circuit board that controls power to the light.\n\nI got it working again by opening the front end, putting the cell back in the body, and resoldering the board’s leads onto the cell tabs (the metal connection strips on the battery). Here’s how I did it, step by step.\n\n## Tools and parts I used \n\n-   A small Phillips screwdriver\n-   A soldering iron and solder\n-   The flashlight’s rubber O-ring (a ring-shaped rubber piece), which sits on top of the battery\n\n## Step 1: Peel off the front sticker \n\nThe front end, which faces the same way as the light, is covered by a plain black sticker. I peeled it off to find two screws holding a black plastic cover over the board.\n\nThe cover has holes for the five charge-indicator LEDs (light-emitting diodes), a small opening for the button, and a keyhole opening for the gold contact.\n\n _With the sticker peeled off, two screws hold the black plastic cover over the front end._\n\n## Step 2: Remove the plastic cover \n\nI took out the two screws and lifted off the cover. Underneath is the circuit board, marked `DL46_C-B0`, with the USB-C port, the button, and the indicator LEDs.\n\n _Under the cover, the board with the USB-C port, button, and charge LEDs, held in by three more screws._\n\n## Step 3: Unscrew the board \n\nThree more screws hold the board to the rim inside the body. With those out, I lifted the board free on its battery leads. Its underside has MOSFETs (electronic switching transistors) and an inductor (a component that stores energy in a magnetic field).\n\n## Step 4: Put the cell back and resolder the leads \n\nI slid the battery back into the body, then soldered the board’s two leads back onto the cell tabs.\n\nI checked polarity (which connection is positive and which is negative) before soldering. Reversing the leads on a driver board like this can damage it:\n\n-   **Positive:** the metal tab on the battery board.\n-   **Negative:** the wire coming off the edge of the battery board.\n\n _The cell back in the tube, with the board’s leads going back onto the battery tabs._\n\n## Step 5: Seat the O-ring \n\nThis is the step I found easy to miss. The thick rubber O-ring goes between the battery and the rim the board screws into, and it keeps the cell from moving around inside the body.\n\nWithout it, the battery can shift and pull on the new solder joints.\n\n## Step 6: Close it up \n\nI screwed the board back onto the rim and put the plastic cover back on with its two screws. The light is fixed and working again."
    }
  ],
  "posts": [
    {
      "id": "kickr-core-hub-simulator",
      "title": "One simulated ride, the whole cycling cockpit",
      "url": "https://devanmetz.com/blog/kickr-core-hub-simulator/",
      "markdownUrl": "https://devanmetz.com/blog/kickr-core-hub-simulator/index.md",
      "collection": "blog",
      "description": "How the KICKR CORE 2 Hub simulator drives virtual shifting, a 3D world, ride stats, and game progression without trainer hardware.",
      "summary": "I made a browser simulator that drives the whole cycling cockpit without trainer hardware. Its synthetic ride data feeds the landscape, ride stats, and game progression, so anyone can try the live demo. A real trainer uses a separate local bridge.",
      "date": "2026-09-22",
      "updated": "2026-10-14",
      "tags": [
        "Cycling",
        "React",
        "Simulation"
      ],
      "projectDate": null,
      "category": null,
      "group": null,
      "status": null,
      "facts": [],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/kickr-core-hub.D8EbYecQ_ZOmNAa.webp",
        "alt": "A cyclist in a colorful 3D landscape surrounded by power, cadence, virtual gearing, and ride timeline panels.",
        "caption": "The browser simulator running an endurance ride. All telemetry shown here is synthetic.",
        "width": 1200,
        "height": 750
      },
      "gallery": [],
      "media": [],
      "project": {
        "id": "kickr-core-hub",
        "title": "KICKR CORE 2 Hub",
        "url": "https://devanmetz.com/portfolio/kickr-core-hub/",
        "markdownUrl": "https://devanmetz.com/portfolio/kickr-core-hub/index.md"
      },
      "stories": [],
      "related": [
        {
          "id": "printed-violin-first-sound",
          "title": "The first sound from my printed violin",
          "url": "https://devanmetz.com/blog/printed-violin-first-sound/",
          "markdownUrl": "https://devanmetz.com/blog/printed-violin-first-sound/index.md"
        },
        {
          "id": "goop-troops-supply-lines",
          "title": "A strategy game about the space between",
          "url": "https://devanmetz.com/blog/goop-troops-supply-lines/",
          "markdownUrl": "https://devanmetz.com/blog/goop-troops-supply-lines/index.md"
        },
        {
          "id": "ebike-from-cad-to-dashboard",
          "title": "One bike, three kinds of making",
          "url": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/",
          "markdownUrl": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/index.md"
        }
      ],
      "contentMarkdown": "The [KICKR CORE 2 Hub](<https://devanmetz.com/portfolio/kickr-core-hub/>) has two ways to get ride data: a local bridge (software that connects the app to the trainer) for a physical smart trainer—an indoor cycling device that reports ride data and adjusts resistance—and a browser simulator.\n\nI made the simulator available in the public demo so someone can explore the cockpit without owning the hardware.\n\nThe simulator is useful because one changing ride signal passes through the whole interface. Start **Sim**, choose a pattern such as Recovery, Intervals, or Climb, and the same synthetic (simulated rather than measured) data updates the numbers, the moving world, the skill system, and the ride review.\n\n## Shifting changes the ride \n\nThe cockpit shows power, cadence (pedaling rate), speed, distance, and accumulated work (energy spent pedaling) around a Three.js landscape. Three.js is the library that draws the 3D world in the browser. Pedaling animation follows cadence. The scene responds to the ride, while a short power average and a scrolling timeline make changes in effort visible.\n\nVirtual gears are designed around a single-cog trainer setup, with one physical sprocket. The interface includes sequential, 2×12 (two front gears and twelve rear gears), and 1×12 (one front gear and twelve rear gears) presets; in the demo, **W/S** shifts gears from the keyboard.\n\nCamera and lighting controls let me see the same ride from a different angle without changing the data underneath it.\n\n## The ride continues after the scene \n\nThe garage turns simulated effort into Strength, Endurance, Agility, and Climbing experience, with earned Drops, the game’s currency, for equipment.\n\nThe Stats panel keeps a ride history and shows time in power zones (ranges of effort) and a power-duration view (power sustained over different lengths of time) when there is enough data. This progression is game state, not a claim about real-world fitness.\n\nEverything in the public simulator is synthetic. The [live demo](<https://kickr-core-hub.pages.dev/>) runs in a browser; controlling a Wahoo trainer requires the separate bridge on the trainer’s local network. The [project page](<https://devanmetz.com/portfolio/kickr-core-hub/#virtual-shifting-and-trainer-controls>) has the control modes, architecture, and current limitations."
    },
    {
      "id": "printed-violin-first-sound",
      "title": "The first sound from my printed violin",
      "url": "https://devanmetz.com/blog/printed-violin-first-sound/",
      "markdownUrl": "https://devanmetz.com/blog/printed-violin-first-sound/index.md",
      "collection": "blog",
      "description": "A one-piece rainbow print, temporary guitar strings, and the first plucked and bowed tests of my Hovalin build.",
      "summary": "I printed and assembled a Hovalin, then tried acoustic guitar strings while waiting for violin strings and a bow. I filmed the first plucked sound test and my early bow practice to keep a record of how the instrument sounded.",
      "date": "2026-09-22",
      "updated": "2026-10-14",
      "tags": [
        "3D printing",
        "Music",
        "Video"
      ],
      "projectDate": null,
      "category": null,
      "group": null,
      "status": null,
      "facts": [],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/printed-violin-playing.CmsxwXA4_1UJjwy.webp",
        "alt": "Playing a rainbow-colored printed violin fitted with a gray neck, four strings, and guitar tuning machines.",
        "caption": "The assembled violin during its first plucked-string sound test. Frame from my original video.",
        "width": 1200,
        "height": 675
      },
      "gallery": [],
      "media": [],
      "project": {
        "id": "printed-violin",
        "title": "3D-Printed Rainbow Violin",
        "url": "https://devanmetz.com/portfolio/printed-violin/",
        "markdownUrl": "https://devanmetz.com/portfolio/printed-violin/index.md"
      },
      "stories": [],
      "related": [
        {
          "id": "kickr-core-hub-simulator",
          "title": "One simulated ride, the whole cycling cockpit",
          "url": "https://devanmetz.com/blog/kickr-core-hub-simulator/",
          "markdownUrl": "https://devanmetz.com/blog/kickr-core-hub-simulator/index.md"
        },
        {
          "id": "goop-troops-supply-lines",
          "title": "A strategy game about the space between",
          "url": "https://devanmetz.com/blog/goop-troops-supply-lines/",
          "markdownUrl": "https://devanmetz.com/blog/goop-troops-supply-lines/index.md"
        },
        {
          "id": "ebike-from-cad-to-dashboard",
          "title": "One bike, three kinds of making",
          "url": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/",
          "markdownUrl": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/index.md"
        }
      ],
      "contentMarkdown": "I found out I could print a violin body on my CR-10 3D printer, so I tried it. I used the [Hovalin design by Hova Labs](<https://www.hovalabs.com/work/hovalin>), a 3D-printable violin, printed the rainbow body upright in one piece, and assembled it with a printed neck and bridge (the piece supporting the strings above the body) and guitar tuning machines, which adjust string tension to change pitch.\n\nThe body alone took about 20 hours to print.\n\nThe print was only the beginning. I still had to find out whether the assembled instrument would make a usable sound.\n\n## Before the bow arrived \n\nMy violin strings and bow (the tool drawn across the strings to make them sound) had not arrived yet, so I fitted acoustic guitar strings and made a plucked-string test. It was a temporary setup, but it let me check the assembled body, tuners, bridge, and fingerboard (the surface along the neck where I press the strings) with actual notes.\n\nThe [original video on the project page](<https://devanmetz.com/portfolio/printed-violin/#first-sound-test>) shows the instrument up close before the playing starts.\n\nThe rainbow bands are visible across the print layers, with a gray neck and bridge against the colored body. The same page has photos of the body on the printer and after printing, before the strings went on.\n\n## Then I tried a bow \n\nAfter the bow arrived, I started practicing. The second clip is from roughly three days in: I hold the violin at my shoulder, bow the strings, and change notes with my left hand. It is an early playing attempt, which is exactly what I wanted to keep in the record.\n\nYou can [watch both tests](<https://devanmetz.com/portfolio/printed-violin/#learning-with-a-bow>) with captions or read their text alternatives.\n\nHova Labs designed the Hovalin. My part of this build was printing and assembling it, testing the sound, learning to play it, and filming those first results."
    },
    {
      "id": "goop-troops-supply-lines",
      "title": "A strategy game about the space between",
      "url": "https://devanmetz.com/blog/goop-troops-supply-lines/",
      "markdownUrl": "https://devanmetz.com/blog/goop-troops-supply-lines/index.md",
      "collection": "blog",
      "description": "In Goop Troops, the connection holding an army together can matter more than the size of the army.",
      "summary": "I built Goop Troops around the connections that hold territory together, where cutting a supply line can change the strength of a larger army. Shared simulation code and a server that owns the match state keep players on the same board. Guest play, computer-controlled opponents, private rooms, and ranked matches offer different ways into that idea.",
      "date": "2026-09-15",
      "updated": "2026-10-14",
      "tags": [
        "Game design",
        "Goop Troops"
      ],
      "projectDate": null,
      "category": null,
      "group": null,
      "status": null,
      "facts": [],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/goop-troops.sa_rb1Bh_D9Hx.webp",
        "alt": "Colorful branching armies competing across the Goop Troops grid.",
        "caption": "A gameplay capture with several bot-controlled armies sharing the battlefield.",
        "width": 1200,
        "height": 630
      },
      "gallery": [],
      "media": [],
      "project": {
        "id": "goop-troops",
        "title": "Goop Troops",
        "url": "https://devanmetz.com/portfolio/goop-troops/",
        "markdownUrl": "https://devanmetz.com/portfolio/goop-troops/index.md"
      },
      "stories": [],
      "related": [
        {
          "id": "kickr-core-hub-simulator",
          "title": "One simulated ride, the whole cycling cockpit",
          "url": "https://devanmetz.com/blog/kickr-core-hub-simulator/",
          "markdownUrl": "https://devanmetz.com/blog/kickr-core-hub-simulator/index.md"
        },
        {
          "id": "printed-violin-first-sound",
          "title": "The first sound from my printed violin",
          "url": "https://devanmetz.com/blog/printed-violin-first-sound/",
          "markdownUrl": "https://devanmetz.com/blog/printed-violin-first-sound/index.md"
        },
        {
          "id": "ebike-from-cad-to-dashboard",
          "title": "One bike, three kinds of making",
          "url": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/",
          "markdownUrl": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/index.md"
        }
      ],
      "contentMarkdown": "A big territory looks strong until you notice the one narrow connection holding it together.\n\nThat is the central idea in [Goop Troops](<https://devanmetz.com/portfolio/goop-troops/>), my multiplayer strategy game. You grow across a board, compete for resources, and defend your territory. But the territory’s shape is part of its strength. An attack on a small connection can change the position of a much larger army.\n\n## Make the map part of the decision \n\nA supply line, the connection holding patches of territory together, gives the player something to read beyond the size of a force. Where is an opponent stretched thin? Which branch is exposed? Is another patch of territory worth the connection it takes to reach it?\n\nThat creates a tension between expansion and reinforcement. More territory creates opportunities, but also more places where the network can break. A narrow passage can be an asset when it is easy to defend and a liability when everything depends on it.\n\nThe interesting decisions come from the relationship between pieces of the board. The same rule applies across a small duel and a crowded public match, but the situations it produces look different.\n\n## Keep the simulation shared \n\nThe browser game combines a React and TypeScript client, the interface running in the browser, with shared simulation code and an authoritative Node.js service. The shared code contains the game rules used by both client and server; “authoritative” means the server decides the match outcome.\n\nThe server owns the match state, so commands from different players resolve against one version of the board.\n\nThat distinction matters in a game about connections. If a cut changes which territory remains connected, everyone needs to agree on the result. Rendering and controls live in the client; deciding the shared outcome belongs to the server.\n\n## Build the ways into the game \n\nThe simulation is only one part of a playable game. Goop Troops also includes guest play, AI (computer-controlled) opponents, private rooms, teams, a tutorial, and ranked one-on-one matchmaking, which pairs players for competitive matches. Public matches support up to eight players.\n\nThose are different ways to meet the same underlying idea: learn the board with bots, play privately with friends, or try a competitive match. Desktop and touch controls give the mechanic another set of constraints to work within.\n\n[Play Goop Troops](<https://gooptroops.org/>), or [see the project and gameplay capture](<https://devanmetz.com/portfolio/goop-troops/>)."
    },
    {
      "id": "ebike-from-cad-to-dashboard",
      "title": "One bike, three kinds of making",
      "url": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/",
      "markdownUrl": "https://devanmetz.com/blog/ebike-from-cad-to-dashboard/index.md",
      "collection": "blog",
      "description": "The mechanical parts, embedded electronics, and little dashboard behind my electric bicycle conversion.",
      "summary": "I combined mechanical design, printed parts, and an ESP32 dashboard in my belt-drive bicycle conversion. The bike is assembled, and I wrote prototype firmware to turn motor-controller readings into a display I can see while riding.",
      "date": "2026-09-15",
      "updated": "2026-10-14",
      "tags": [
        "Hardware",
        "CAD",
        "Embedded"
      ],
      "projectDate": null,
      "category": null,
      "group": null,
      "status": null,
      "facts": [],
      "actions": [],
      "cover": {
        "url": "https://devanmetz.com/_astro/ebike.2RbJRJf1_1fHioh.webp",
        "alt": "A blue bicycle fitted with a custom motor mount, large rear pulley, belt drive, and electronics.",
        "caption": "The assembled bicycle conversion, showing the motor, belt drive, and frame-mounted electronics.",
        "width": 1200,
        "height": 904
      },
      "gallery": [],
      "media": [],
      "project": {
        "id": "diy-electric-bike",
        "title": "DIY Electric Bike",
        "url": "https://devanmetz.com/portfolio/diy-electric-bike/",
        "markdownUrl": "https://devanmetz.com/portfolio/diy-electric-bike/index.md"
      },
      "stories": [],
      "related": [
        {
          "id": "kickr-core-hub-simulator",
          "title": "One simulated ride, the whole cycling cockpit",
          "url": "https://devanmetz.com/blog/kickr-core-hub-simulator/",
          "markdownUrl": "https://devanmetz.com/blog/kickr-core-hub-simulator/index.md"
        },
        {
          "id": "printed-violin-first-sound",
          "title": "The first sound from my printed violin",
          "url": "https://devanmetz.com/blog/printed-violin-first-sound/",
          "markdownUrl": "https://devanmetz.com/blog/printed-violin-first-sound/index.md"
        },
        {
          "id": "goop-troops-supply-lines",
          "title": "A strategy game about the space between",
          "url": "https://devanmetz.com/blog/goop-troops-supply-lines/",
          "markdownUrl": "https://devanmetz.com/blog/goop-troops-supply-lines/index.md"
        }
      ],
      "contentMarkdown": "My [electric bicycle conversion](<https://devanmetz.com/portfolio/diy-electric-bike/>) combines a belt-driven motor, parts designed for the frame, and a small display for the controller’s readings. I worked through the mechanical fit in Fusion 360, a computer-aided design (CAD) tool, printed the mounts and pulley, then wrote the dashboard firmware, the software running on the ESP32, a small programmable computer.\n\n## The model has to meet the frame \n\nThe mechanical side includes a large rear pulley, a motor mount, and seat-tube clamps. Fusion 360 assemblies describe how the parts relate to the bicycle, while STL exports, files describing the surface of each part, turn individual shapes into something that can be fabricated.\n\nThe CAD assembly helped me check where the motor sits, where the belt runs, and how the mounts attach. On the assembled bike, those parts also have to share space with the actual frame, wheel, wiring, and fasteners.\n\n## Give the controller a readable face \n\nThe display uses an ESP32 and a TFT (thin-film transistor) screen, a small display. Its C++ sketch reads telemetry (measurements) from a VESC motor controller over UART, a serial data connection, and draws battery voltage, power, and speed. The code also includes addressable LED control, which lets the software set individual lights.\n\nThe data path is small enough to describe in one line:\n\n**Motor controller → UART → ESP32 → TFT display**\n\nThat line crosses the boundary between the physical build and the software. The controller has measurements; the display gives those measurements a place the rider can see.\n\n## What still needs checking \n\nThe bike is assembled, but the dashboard firmware is a prototype. Its speed and distance calculations depend on this drivetrain’s wheel size, motor pole count (the number of magnetic poles), and pulley ratio (the relative sizes of the drive pulleys). I would check those constants against the hardware before using the sketch on another build.\n\nThe [project page](<https://devanmetz.com/portfolio/diy-electric-bike/>) shows the finished bike, CAD assembly, and dashboard. The public repository has the editable design, printable parts, and code.\n\n[See the build photos](<https://devanmetz.com/portfolio/diy-electric-bike/>) or [explore the public project files](<https://github.com/DevanMetz/DIY-Mid-Drive-EBike>)."
    }
  ]
}
