The Arcology Garden

The Monticello Drive

Contents

Why make your own keyboard?

I love my Keyboardio Atreus but it doesn't quite fit my hands, my pinkies are really much shorter than it's modeled for so I'm building a hand-wire customized variant, and will some day build out a low profile PCB revision.

I have been implementing Revision One, a computer-generated PCB and 3D printable case files drawn up in a set of Org Mode documents and published to the web. Much of the documentation and code powering this keyboard has been written on this keyboard, and it features:

  • 48 key atreus-like ergonomic keyboard

  • hand wired columnar ergonomic prototyping system (The Monticello Drive Revision Zero)

  • dynamically generated cases, pcbs, and layouts so that you can tune the board to your hand size preference (The Monticello Drive Revision One)

  • split-able design but current iteration is a single object

  • 2x5 key thumb cluster, 4 extra utility keys per hand on near the pinkies

  • designed for ZMK

  • OLED display for layer management and status information

  • wireless bluetooth support

  • trackpoint module support (TODO) I've been ideating on future versions and integrations of this keyboard as well, I want to build an extremely low-profile board which might work perfectly on the input tray of a Framework Laptop and fit in my travel bag, and the keyboard as a weird cyberdeck build on top of a GPD Pocket's innards, with the display driven to either e-paper or a pair of xReal glasses.

Monticello Drive

From Technomancy's Atreus FAQ:

I want to make my own version of the Atreus!

That's great! Please feel free to do this if it's done in a way that respects the license of the original work. We would ask that you not use the name "Atreus"; instead if you want to name it in a way that reflects where it came from please use the term "Atreoid".

I spent quite some time coming up with something clever, but I fell back as always to naming it after a track from The Flashbulb, as I do for all my other computers. This thing is definitely a computer, for better or worse.

Revision One

The current revision of the Monticello Drive is a fully documented build of a custom atreus-like ergonomic keyboard from idea to PCB and case and working firmware. I use this keyboard every day, on my desk, sitting in my lap, carefully positioned on my laptop's input tray.

2025-07-07_22-01-04_de3d95802423086e14a067660060d63485575a43ab54626fdfd8be096e5db04d.jpg

2025-07-07_22-01-15_ccee75317123aa619988ca1a4c974493bd2c91893d426c28d6704213cc4fd4b8.jpg

2025-07-07_22-01-31_456d68c9e24d3cfebb763b2dd534c8b80aa2a9cc48d10d3abd6fcd16464bd9c0.jpg

Revision One of the Monticello Drive is a dynamically generated KiCAD footprint, and a custom designed and manufactured PCB from pcbway. It supports the same feature set as the Revision 0, but is much more robust and capable of being used as a portable bluetooth keyboard.

The PCB and 3D printed case is generated from a configuration file which describes how the Monticello Drive's finger spacing and stagger work, which keyswitches to use, etc.

Through revision 0, I have defined the key position and outlines and now it's useful to look at the generated KiCAD PCB files with the foot prints of the components in place but un-routed. Kicad helpfully provides a "rat's nest" of virtual lines and that you trace out on the PCB to carefully lay them out manually. The current revision has a really simple and subtly rounded and softened design of long arcing cuts and traces. I expect to need one or two more revisions of this board, hopefully just one more after Revision 1.1 in the future. I might stream and record a design session at some point in the future.

My Split Keymap

My Keyboardio Atreus is well-loved, and over the course of nearly 15 years of using specialty keyboards and linux keymap software, I have designed a keyboard layout designed to be compact and work well with my modal workflows. This keyboard presents three layers; a split QWERTY layout with modifiers and whatnot that can be reached with a either thumbs, a symbol layer inspired by an old XModMap file custom laptop keymap but I cannot find it now, and a layer for system navigation media and navigation. The primary goal of my layouts here is to make it so that the most common behaviors are on my thumbs, and that all the letters and symbols I use can be easily picked without twisting my wrists or moving away from the core cluster of keyboard keys.

conf:tangle zmk-config/monticello_drive.conf
CONFIG_ZMK_DISPLAY=y
CONFIG_ZMK_DISPLAY_INVERT=n
CONFIG_ZMK_WIDGET_LAYER_STATUS=y
CONFIG_ZMK_WIDGET_BATTERY_STATUS=y
CONFIG_ZMK_WIDGET_BATTERY_STATUS_SHOW_PERCENTAGE=y
CONFIG_ZMK_WIDGET_OUTPUT_STATUS=y
CONFIG_ZMK_WIDGET_WPM_STATUS=y

CONFIG_ZMK_DISPLAY_BLANK_ON_IDLE=n

I have added a few new tricks in here, finally integrating the mod-tap behavior on the backspace/shift combo key in the thumb cluster, a lock key, and a more natural-feeling media-keys/function key layer.

dts:tangle zmk-config/monticello_drive.keymap
#include <behaviors.dtsi>
#include <dt-bindings/zmk/keys.h>
#include <dt-bindings/zmk/bt.h>
#include <dt-bindings/zmk/outputs.h>

#define _DEFT 0
#define _SYMB 1
#define _CTRL 2
#define _BLUE 3

&mt {
    tapping-term-ms = <150>;
};
&lt {
    tapping-term-ms = <150>;
};

/ {
	keymap {
		compatible = "zmk,keymap";
		default_layer {
			display-name = "QWERTY";
			bindings = <
			/* |------+---+------+------+-----+-----+               +-----+-----+-----+------+---+------| */
			/* | ESC  | Q | W    | E    | R   | T   +-----+   +-----+ Y   | U   | I   | O    | P | TAB  | */
			/* | GRAV | A | S    | D    | F   | G   | M-x |   | ESC | H   | J   | K   | L    | ; | LOCK | */
			/* |      | Z | X    | C    | V   | B   | ESC |   | GUI | N   | M   | ,   | .    | ? |      | */
			/* |      |   | SYM5 | SYM6 | SFT | BSP | ALT |   | CTL | RAI | SPC | RET | CTRL |   |      | */
			/* |------+---+------+------+-----+-----+-----+---+-----+-----+-----+-----+------+---+------| */
			&kp ESC		&kp Q		&kp W		&kp E		&kp R		&kp T
			&kp GRAV	&kp A		&kp S		&kp D		&kp F		&kp G			&kp GRAV
						&kp Z		&kp X		&kp C		&kp V		&kp B			&kp ESC
						&mo _CTRL	&mo _SYMB	&kp BSPC	&mt LSFT BSPC	&kp LALT

						&kp Y			&kp U		&kp I		&kp O		&kp P		&kp TAB
			&kp TILDE	&kp H			&kp J		&kp K		&kp L		&kp SEMI	&kp K_LOCK
			&kp RGUI	&kp N			&kp M		&kp COMMA	&kp DOT		&kp FSLH
			&kp RCTL	&lt _SYMB SPACE	&kp SPACE	&kp RET	 	&tog _CTRL
			>;
		};

		symbol_layer {
			display-name = "$%^&_";
			bindings = <
			/* |------+---+------+------+-----+-----+               +-----+----+-----+-----+---+---| */
			/* | SYM1 | $ | %    | ^    | &   | _   +-----+   +-----+ PYP | 7  | 8   | 9   | + | * | */
			/* | SYM2 | { | }	| (    | )   | "   | LC1 |   | RC1 | @   | 4  | 5   | 6   | - | = | */
			/* |      | [ | ]    | <    | >   | '   | LC2 |   | RC2 | #   | 1  | 2   | 3   |   |   | */
			/* |      |   | SYM5 | SYM6 | LC5 | LC4 | LC3 |   | RC3 | RC4 | 0  | RET | RET |   |   | */
			/* |------+---+------+------+-----+-----+-----+---+-----+-----+----+-----+-----+---+---| */
			&kp ESC		&kp DOLLAR	&kp PRCT	&kp CARET	&kp AMPS	&kp UNDER
			&kp TILDE	&kp LBRC	&kp RBRC	&kp LPAR	&kp RPAR	&kp DQT		&kp TILDE
						&kp LBKT	&kp RBKT	&kp EXCL	&kp PIPE	&kp SQT		&kp RGUI
									&trans		&trans		&trans		&trans		&kp RCTL

						&kp BSLH	&kp N7	&kp N8	&kp N9	&kp PLUS	&kp STAR
			&kp GRAV	&kp AT		&kp N4	&kp N5	&kp N6	&kp MINUS	&kp EQUAL
			&kp ESC		&kp HASH	&kp N1	&kp N2	&kp N3	&none
			&kp LALT	&trans		&kp N0	&kp RET &kp RET
			>;
		};

		control_layer {
			display-name = "CTRL";
			bindings = <
			/* |------+---+------+------+-----+-----+               +-----+----+------+-----+-----+-----| */
			/* | FLSH | ⏮️ | ⏯️   | ⏭️   | ➖  | ➕   +-----+   +-----+ 🔆  | F7 | F8   | F9  | F12 |     | */
			/* | SYM2 |   | LEFT | UP   | RGHT| PGUP|     |   |     | 🔅  | F4 | F5   | F6  | F11 | LCK | */
			/* |      |   | HOME | DOWN | END | PGDN|     |   |     | 🌚  | F1 | F2   | F3  | F10 |     | */
			/* |      |   |      |      |     |     |     |   |     |     |    |     | QWER|     |      | */
			/* |------+---+------+------+-----+-----+-----+---+-----+-----+----+-----+-----+-----+------| */
			&bootloader	&kp	C_PREV	&kp C_PLAY	&kp C_NEXT	&kp C_VOL_DN	&kp C_VOL_UP
			&kp TILDE	&none		&kp LEFT	&kp UP 		&kp RIGHT		&kp PG_UP	&none
						&none		&kp HOME	&kp DOWN	&kp END			&kp PG_DN	&none
									&trans		&trans		&none			&none		&none

					&kp C_BRI_UP	&kp F7	&kp F8	&kp F9		&kp F12 &none
			&none	&kp C_BRI_DN	&kp F4	&kp F5	&kp F6		&kp F11 &kp K_LOCK
			&none	&kp C_BKLT_TOG	&kp F1	&kp F2	&kp F3		&kp F10
			&none	&none			&none	&none	&to _DEFT
			>;
		};

		bluetooth_layer {
			display-name = "BLE";
			bindings = <
			&none	&none	&none	&none	&none	&none
			&none	&none	&none	&none	&none	&none	&none
					&none	&none	&none	&none	&none	&none
							&none	&none	&none	&none 	&none

					&bt BT_NXT		&none			&none			&none			&none		&bt BT_CLR
			&none	&bt BT_PRV		&none			&none			&none			&none		&bt BT_CLR_ALL
			&none	&bt BT_SEL 0	&bt BT_SEL 1	&bt BT_SEL 2	&bt BT_SEL 3	&bt BT_SEL 4
			&none	&out OUT_USB	&out OUT_BLE	&none			&none
			>;
		};
	};

	conditional_layers {
		compatible = "zmk,conditional-layers";
		tri_layer {
			if-layers = <1 2>;
			then-layer = <3>;
		};
	};
};

With the board built, the keymap flashed, and the keyboard plugged in, my laptop and my desktop then have new powers! A lock button! Visibility in to when I trap myself in my control layer! More thumb keys than I can shake a stick at!

Future Work

if this thing is going to be a computer, i might as well make it as cursed as possible. here are some possibilities:

WAITING Revision Two: low-profile hand-solder and the gpd pocket cyberdeck build

i have always wanted to build a tiny little shittier laptop, i've been tryin it for years.

this thing could have room to put a rtl-sdr or flipper zero or something inside of it. i really want a laptop that i can tune to NOAA or do dump1090 or, fuck, watch OTA digital television on it haha. what if instead of a display i just hooked up xReal AR glasses to the thing and Jacked In?

if those kailh notebook keyswitches work out it could be notepad-thin, a little cyberdeck case built around the rev1 and a gpd pocket motherboard and battery.

This is probably going to on its own page before we're working on it.

WAITING Framework.12 input cover

https://github.com/FrameworkComputer/Framework-Laptop-12/tree/main/InputCover

WAITING Revision Forth a monticello drive that has another MCU on it running a makerlisp/forth environment

a true Dumb Ideas Done Dirt Cheap, sticking a ATMega or something that can run a RunCPM or a MakerLisp or a DuskOS or a CollapseOS or whatever on it somehow.

hell, it could just have that little makerlisp ez80 board attached to it via the board-to-board connector, recreating the GPIO expansion board on the woodbox computer. that's probably the easiest, it would just be a pain to solder that connector at home haha.

could use a notepad.exe process or other textbox on an attached computer, as a terminal; or re-use the makerlisp serial modules

breadboard GPIO, a pocketable CollapseOS or DuskOS machine, a little self-contained environment, an undercover computer

WAITING A kicad project generator [that runs on Revision Forth]

it's kind of crazy that kicad files are "just" s-expressions. you could probably make this ergogen thing in like 2500 lines of lisp. shit. SHIT

this coulda been a defmacro

~ rrix 1992-2024 RIP stung to death by wasps

how do you make a keyboard that is self-hosting?

what if i wrote a lisp program that takes a domain-specific s-expression and outputs a kicad project's s-expressions?

what if that program ran on Revision Forth's MCU?

you'd run the makerlisp text editor hooked up to a display or whatever, and then sometimes you'd have it "compile" the DSL and spit out a project file you can save and look at and make and assemble a PCB with if you choose to.

this is certifiably one of my most Dumb Ideas Done Dirt Cheap ever and probably one of the ones that is more likely to get done some day.

Project Updates

Revision 1.1 of the Monticello Drive is a successful build

After my last update, I spent some time making two revisions of the PCB which I ordered from PCBWay. They have a nice efficient system and some good guidance on their documentation site on the design considerations and kicad configuration/etc you need to properly export to their platform.

Revision 1.0 was a good first approximation of what a keyboard needed, but had some issues that I needed to rectify.

2025-07-07_22-01-48_dca6bce9f88bc034a9883c0366144918f3757669e693fb259e62e99a8b29e592.png

In the first revision the OLED hookup is placed incorrectly, and the battery circuit did not include a switch. I left some drawings on the board to estimate where the trackpoint could go in the future but did not have any slots cut in to it or anything like that. I had to scrap some other keyboards for projects, i need to order some parts from boardsource or someone to rebuild them, but this was a nice quick prototype to show that the PCB design was feasible.

I took that and quickly iterated on 1.1, re-generating the board with a smoother rounded plate outline, integrated switch and battery circuit, a better OLED layout, etc. Using Ergogen it's also possible to generate STL case files for 3D Printing.

2025-07-07_22-01-04_de3d95802423086e14a067660060d63485575a43ab54626fdfd8be096e5db04d.jpg

I also took an attempt at adding some silkscreen art to the back of it and made my 3D printed case open in the middle to show the silkscreen art. I haven't taken it out too far from my desk yet, but I think it's solid enough to fit in the bag that my laptop travels in.

2025-07-07_22-01-15_ccee75317123aa619988ca1a4c974493bd2c91893d426c28d6704213cc4fd4b8.jpg

It's thin and lightweight and basically solid/rigid enough to use while sitting or standing, even lying down on a lap-desk in bed. So far I am quite pleased with this thing, and it's very easy to do if you feel like doing it.

Nix lets me describe the dependencies and build a development environment you can replicate, and so you could go and look at The Monticello Drive Revision Zero and start to 3D print base-plates for a matrix layout that works for you. If you build one let me know.

If you decide you like it and want a low-profile bluetooth portable ergonomic Atreus-like keyboard you can make one by reading and following The Monticello Drive Revision One. You have the ability to generate PCBs, route them beautifully, and have them manufactured in about the same amount of time it would take to get the bill of materials filled and the case parts printed. If you do that let me know.

There's no fine surface mount soldering or fiddly keyboard matrix soldering required outside of placing the power switch and reset button, though you can solder surface mount diodes in to the keyboard matrix if you'd like.

I was really intimidated at the start of this project, for many years I felt like it was beyond my ability to build a high quality mechanical keyboard with my own design, and over the last six months I've disabused myself of this notion. I still am a long way from being comfortable with it or working on circuit design starting from data sheets and schematics, but this is a nice time learning how all this works or doesn't, and it's definitely something the average computer wizard could pull off.

I would love to have a better case made some day from metal, but for now this thing is definitely good enough to keep using for a while! Don't ask me about the final cost of the project please, just follow and try it out yourself maybe.

I can generate custom PCBs for my ergonomic mechanical keyboard

Over the last few days I've been starting work on Revision One of the Monticello Drive keyboard, a low-profile PCB-sandwich ergonomic keyboard. Ergogen supports generating kicad PCBs but first I wanted to familiarize myself with kicad a bit by trying to implement the keyboard myself by constructing a schematic, mapping the symbols to footprints, and then laying the footprints out on a PCB. I did that, and it was mostly fine, but I came back pretty quickly to wanting to generate the PCBs.

Ergogen supports this, but I never quite understood how to make it work and generate a rat's nest until I spent some time playing around and studying other example configurations I found. To get a working matrix, you need to label columns and rows with variables like row_net and column_net that can then be used in the footprint generator along with an auto-generated colrow variable that is used to define the network between the switch and its column, the switch and its diode, the diode and its row.

You end up with a bunch of components but no routing, kicad's pcbnew can then be used to manually lay out the routes.

Ergogen also lets you easily drop in other components that are commonly used when building a keyboard, like the keyswitch footprints, diodes, MCU, buttons, etc. These have the same auto-placement and layout engine which the keys themselves have so you can say "oh, uhh, just put that midway between the left hand and right hand homing keys, and move it up 20 mm" and the system will handle placing it for you. I had to fork ergogen to add a footprint for the nice!nano v2 since it has some extra pins I need to use, and I added a flake.nix to it along the way.

2025-07-07_22-03-20_ea2f254dcd6dfc1bc7f6f7fb135b14ff0a901899dd9beb1ce42ff2c9f699f208.png

All of this is laid out above in this document, the neat thing about this is that you can follow some straightforward if laborious directions to generate and 3D print your own keyboard, and then maybe even have PCBs made for it if you like typing on it. We'll see if I decide to do that. I saw some really cool low-profile switches which Kailh is demoing on reddit which can be used to make 5mm thick keyboards with real mechanical switches, I would love to use those instead of Chocs for this, but for now I might ask pcbway for a few of these to play around with.

I'm really happy with how easy it would be, in theory, for someone to clone this doc's repo, change a YAML file, run some nix run commands and have all the materiel necessary to make a custom Atreus-like keyboard.

Updated my Atreoid project model to use BOSL2

I finally got around to adding BOSL2 to my OpenSCAD repository and re-wrote the keyboard model to use it.

2025-07-07_22-03-36_6ba8a397dffcf717e5cc0cac1ade5ce7978bc49013d99e55bba6022785362649.png

It has some nice utilities for building "real-world" objects like these hinges that I added, but more importantly it makes it easy to lay out and arrange objects and define attachments without it turning into a soup of magic numbers added and subtracted to each other, but it requires you to re-think some of the ways you are building your models.

Compare this to this and besides the "magic" numbers which are spat out from the ergogen yaml doc at the top of this document it's much simpler and easier to reason about, neat stuff.

I did recently solder the keyboard's wiring in preparation for wiring up the microcontroller. I'm going to re-print this BOSL2 version tomorrow and transfer the build over to that. I still have to lay out the center cluster where the microcontroller and LEDs will live, but once these prints work out and I've verified the hinge mechanism I've changed to from the nut-and-bolt setup it used previously, it'll just about be time to start working on ZMK support for the base board. I've decided to get the basic grid working before I tackle the trackpoint module and necessary ps/2 support.

Exciting stuff!

First post on the new Atreoid prototype

On I got the first prototype of my Atreoid build booting with the ZMK keyboard firmware package! I'm writing this post on it now, it's nice to finally be able to see if my original finger position guess-work at the top of this page was any good. It's pretty good, though. I still need to wire up my old QMK configuration on to it and get all the layers wired up and on top of that I have two or four more keys to work with on each layer than I did with the Atreus.

It took only a few days to get it soldered together once I had the new BOSL2 models validated. after that, getting ZMK running on it was fairly straightforwrd but i had a late-night soldering think-o where i forgot that most pins on a controller like the arduino pro micro or nice!nano serve a purpose and had wired a bunch of the matrix lines up to ground, confident i had enough pins to get it done. Five minutes after I finished, the word "pinout" popped in to my head and i had to desolder the whole mess, bridge the rows across the middle of the board, and more carefully route this all out.

And at the end of that, I still had a bit of a SNAFU where the default i2c pins where used in the matrix grid and I spent quite some hours today trying to figure out how to trick ZMK and Zephyr in to running i2c on a different set of pins, two of the three extra pins in the middle of the microcontroller where the blue and green wires lead up to to the bottom of the center hinge of the board.

After quite some google and careful GitHub searches, I found Koen Vervloesem's blog post on how to change the default i2c pins in Zephyr, the RTOS which ZMK is built on top of. This included an example device-tree include file which let one override the pins used, and this thankfully worked with ZMK's display drivers.

Overall, I am pretty excited with how this feels already. I've been planning already a "rev1" board which is a manufactured PCB for low-profile choc keyswitches and less soldering, perhaps a return to through-hole for the diodes, the most painful part of the soldering job. The switch-biscuits I bought to prototype this did their job wonderfully, they're a great prototyping tool for keyboards like this. I need to do a bit of work organizing the code for the models and the board before I publicize the code for this thing.

Common Resources

Nix flake encapsulation

I wish I didn't feel like the ecosystem was constantly compelling me to use flakes. but here you go, you can nix run https://code.rix.si/rrix/monticello-drive which is cool i guess.

nix:tangle flake.nix:noweb yes
{
  inputs = {
    <<inputs>>
  };

  outputs = { self, nixpkgs, zmk-nix, ... }@inputs: let
    forAllSystems = nixpkgs.lib.genAttrs (nixpkgs.lib.attrNames inputs.zmk-nix.packages);
    localSource = nixpkgs.lib.sourceFilesBySuffices self
      [
        # zmk files
        ".conf" ".keymap" ".yml" ".overlay" ".shield" ".defconfig"
        # openscad files
        ".scad"
        # py script
        ".py"
        # ergogen yaml
        ".yaml"
        # openjscad
        ".jscad"
        ".stl"
      ];
    mkPyEnv = pkgs:
      pkgs.python3.withPackages (pp: with pp; [
        click
        pyyaml
      ]);
  in {
    packages = forAllSystems (system: 
      let
        pkgs = import nixpkgs { inherit system; };
        pyEnv = mkPyEnv pkgs;
      in rec {
        <<packages>>
        default = firmware;
   });

    devShells = forAllSystems (system: let
      pkgs = import nixpkgs { inherit system; };
      pyEnv = mkPyEnv pkgs;
      zmk-shell = inputs.zmk-nix.devShells.${system}.default;
    in {
      default = pkgs.mkShell {
        inputsFrom = [ zmk-shell ];
        packages = with pkgs; [
          pyEnv
        ];
      };
    });
  };
}

Firmware

zmk-nix is a fine little encapsulation to spit out ZMK firmwares, especially if your shield is already upstream. In our case, the board definitions are also in the zmk-config

nix#+name: inputs:noweb-ref inputs
zmk-nix = {
  url = "github:lilyinstarlight/zmk-nix";
  inputs.nixpkgs.follows = "nixpkgs";
};
nix#+name: packages:noweb-ref packages
firmware = zmk-nix.legacyPackages.${system}.buildKeyboard {
  name = "firmware";

  src = localSource;

  board = "nice_nano_v2";
  shield = "monticello_drive_rev1";
  config = "zmk-config";

  zephyrDepsHash = "sha256-JakmFT5VDxL3KoOTXSzM3kvE6aqbMVRIHovveKN7OdA=";

  meta = {
    description = "ZMK firmware";
    license = nixpkgs.lib.licenses.mit;
    platforms = nixpkgs.lib.platforms.all;
  };
};

flash = zmk-nix.packages.${system}.flash.override { inherit firmware; };
update = zmk-nix.packages.${system}.update;

Ergogen

I have a fork of Ergogen at //code.rix.si/rrix/ergogen with a Nix flake in it, and a nice!nano v2 footprint included.

nix#+name: inputs:noweb-ref inputs
ergogen = {
  url = "git+https://code.rix.si/rrix/ergogen";
  inputs.nixpkgs.follows = "nixpkgs";
};

I have a small script wrapper generate_board that will output the PCB to ./pcbs if you nix run .#generate_board &

nix#+name: packages:noweb-ref packages
ergogen = inputs.ergogen.packages.${system}.default;
node_modules = inputs.ergogen.packages.${system}.node_modules;
openjscad = pkgs.writeScriptBin "openjscad" ''
  ${node_modules}/bin/openjscad $@
'';
generate_board = pkgs.writeScriptBin "generate_board" ''
  ${ergogen}/bin/ergogen ./ergogen.yaml -d -o ./generated/
  find generated -type f -name "*jscad" -exec ${node_modules}/bin/openjscad {} \;
'';

Prototype STL Generation

The SCAD files for the plate-build prototype are evaluated using BOSL2, a really nice construction kit for OpenSCAD.

nix#+name: inputs:noweb-ref inputs
bosl2 = {
  url = "github:BelfrySCAD/BOSL2";
  flake = false;
};

=nix run .#stls= will invoke openscad three times to generate STLs for each component and then stick all three in the result directory:

nix#+name: packages:noweb-ref packages
stls = let
  mkOpenSCAD = 
    variant: (import ./nix/openscad.nix { inherit pkgs; }) {
      name = "atreoid-${variant}";
      src = localSource;
      scadLibs = { BOSL2 = inputs.bosl2; };
      scadFiles = [ "./scad/atreoid-${variant}.scad" ];
    };
in pkgs.symlinkJoin {
  name = "stls";
  paths = [
    (mkOpenSCAD "left")
    (mkOpenSCAD "right")
    (mkOpenSCAD "center")
  ];
};

I bodged this together from some work that pjones made, a nice little Nix derivation that will take a list of scad files and some libraries, and some variable definitions to output an STL file:

nix:tangle nix/openscad.nix
# greetz pjones
{ pkgs ? import <nixpkgs> { }
}:

{
  # Source code directory, usually `./'.  Passed to `mkDerivation'.
  src

# List of files to process.  The default is an empty list which means
# to process all `*.scad' files.
, scadFiles ? [ ]
# File variables to define
, defines ? { }
# Add SCAD libraries to openscad PATH
, scadLibs ? {}

# The name of the derivation.
, name ? "project"
}:
let
  lib = pkgs.lib;

  scadLibPath = pkgs.stdenv.mkDerivation {
    name = "scad-paths";
    inherit src;

    installPhase = ''
      mkdir -p $out
      ${pkgs.lib.concatStrings
        (pkgs.lib.mapAttrsToList
          (name: path: "cp -r ${path} $out/${name}\n") scadLibs)}
    '';
  };

  buildScript =
    pkgs.writeShellScript
      "openscad-build" ''
      #!/usr/bin/env bash
      
      set -eu
      set -o pipefail
      
      files=()
      if [ $# -eq 0 ]; then
        readarray -t files < <(find . -type f -name '*.scad')
      else
        files=("$@")
      fi
      
      for file in "''${files[@]}"; do
        echo "==> STL: $file"
        export OPENSCADPATH=${scadLibPath} 
        echo $OPENSCADPATH
        openscad \
          ${pkgs.lib.concatStrings
            (pkgs.lib.mapAttrsToList
              (var: val: "-D${var}=${val} ")
              defines)} \
          -o "$(dirname "$file")/$(basename "$file" .scad).stl" \
          "$file"
      done
    '';

  installScript =
    pkgs.writeShellScript
    "openscad-install" ''
      #!/usr/bin/env bash
      
      set -eu
      set -o pipefail
      
      out=$1
      ext=$2
      
      files=()
      mkdir -p "$out"
      
      readarray -t files < <(find . -type f -name "*.$ext")
      
      for file in "''${files[@]}"; do
        echo $file $out
        install -m 0444 "$file" "$out"
      done
    '';
in
pkgs.stdenvNoCC.mkDerivation {
  inherit src;
  name = "openscad-${name}";

  buildInputs = with pkgs; [
    openscad
  ];

  buildPhase = ''
    ${buildScript} ${lib.escapeShellArgs scadFiles}
  '';

  installPhase = ''
    ${installScript} "$out/stl" stl
  '';
}

skgen

the points used by the prototype plate build are generated from points.yaml from ergogen with this script which takes the YAML and spits out some code that I can shove in to openscad. maybe make that generate import-able code so that it can be re-generated.

This thing is very simple, run it and redirect stdout: python scripts/skgen.py ~/Downloads/points.yaml > scad/points.scad &:

nix#+name: packages:noweb-ref packages
skgen = let
in pkgs.writeScriptBin "skgen" ''
  ${pyEnv}/bin/python3 scripts/skgen.py ./generated/points/points.yaml | grep -v mirror > scad/points.scad
  echo 👍
'';
python:tangle scripts/skgen.py
import yaml
import click

@click.command()
@click.argument('points', type=click.File('rb'))
def doit(points):
    """
    Convert the `points.yaml` file at POINTS to
    openscad-compatible point cloud for the atreoid scad generator to
    load.
    """
    doc = yaml.safe_load(points)

    print("array = [")
    for k, d in doc.items():
        print(f"[{d['x']}, {d['y']}, {d['r']}], // {k}")
    print("];")

if __name__  == "__main__":
    doit()