[{"content":"Claude Code\u0026rsquo;s /doctor command told me ~/.local/bin wasn\u0026rsquo;t in my PATH. That\u0026rsquo;s where it installs its native binary. Simple fix, right?\nThe symptom was easy to reproduce: open a new shell, run echo $PATH, and ~/.local/bin is nowhere in there. Claude Code was installed fine; it just couldn\u0026rsquo;t find itself.\nI\u0026rsquo;d just make sure that the path was configured in my .zshrc and all would be good. Since it\u0026rsquo;s NixOS, the obvious fix from home-manager was with home.sessionPath. One line in home/roles/base.nix, rebuild, done:\nhome.sessionPath = [ \u0026#34;$HOME/.local/bin\u0026#34; ]; Tried it. Rebuilt. Ran exec zsh. Opened Claude Code. Ran /doctor. Still broken.\nIn Which I Learn How NixOS Boots a Shell Ok, so NixOS generates a script called set-environment at build time. You can find the one currently running your shell at something like /nix/store/\u0026lt;hash\u0026gt;-set-environment. It gets sourced via /etc/zshenv on every shell start, and it does this:\nexport PATH=\u0026#34;$HOME/.nix-profile/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin\u0026#34; A hard export PATH=. Not PATH=\u0026quot;something:$PATH\u0026quot;. A fresh path every time. Usually a good thing to prevent cruft building up, but now important stuff was missing.\nhome-manager\u0026rsquo;s hm-session-vars.sh — which does correctly add ~/.local/bin — runs before /etc/zshenv. So it sets the path, and then NixOS immediately resets it. The entry is right there in hm-session-vars.sh if you go looking:\nexport PATH=\u0026#34;$HOME/.local/bin${PATH:+:}$PATH\u0026#34; Was this documented anywhere obvious? I mean, probably, but I didn\u0026rsquo;t do a lot of investigation.\nThe Actual Fix Searching online got me to a better option pretty quickly: NixOS has environment.localBinInPath. Setting it to true injects ~/.local/bin directly into the set-environment script, so it survives the reset.\nOne line in nixos/profiles/base.nix:\nenvironment.localBinInPath = true; Rebuild. New shell. ~/.local/bin is in $PATH. Claude Code\u0026rsquo;s /doctor command goes quiet. Mission accomplished.\nOnions and Parfaits There are layers here I didn\u0026rsquo;t consider, and the lesson isn\u0026rsquo;t subtle: on NixOS, if something isn\u0026rsquo;t in the PATH that set-environment builds, it isn\u0026rsquo;t in your PATH. Full stop. home-manager\u0026rsquo;s sessionPath, sessionVariables, anything in hm-session-vars.sh — all of it happens before the hard reset. Anything you want to survive has to come from the NixOS level.\nWas home.sessionPath the wrong tool? Yes. Would I have tried it first anyway? Also, yes.\n$ echo $PATH | tr \u0026#39;:\u0026#39; \u0026#39;\\n\u0026#39; | grep local /home/grue/.local/bin Good enough.\n","permalink":"https://asphaltbuffet.com/posts/2026/05/nixos-local-bin-path/","summary":"\u003cp\u003eClaude Code\u0026rsquo;s \u003ccode\u003e/doctor\u003c/code\u003e command told me \u003ccode\u003e~/.local/bin\u003c/code\u003e wasn\u0026rsquo;t in my \u003ccode\u003ePATH\u003c/code\u003e. That\u0026rsquo;s where it installs its native binary. Simple fix, right?\u003c/p\u003e","title":"Rooting around in .local/bin"},{"content":"Docks are basically plug-and-play, right? So if the monitor isn\u0026rsquo;t working, there must be a setting that\u0026rsquo;s off somewhere. This will be easy.\n(narrator voice: it would not, in fact, be easy)\nThe hardware path is simple: ThinkPad T14 Gen1 (Intel) connects via USB-C to a Lenovo dock, dock connects via HDMI to a TESmart 4K@60Hz 4:4:4 KVM, KVM connects to a monitor. When I plug the laptop directly into the KVM via HDMI, video works perfectly. When that same HDMI cable runs through the dock, the laptop doesn\u0026rsquo;t detect any external monitor at all. Keyboard and mouse on the dock work fine. This is the worst kind of partial failure — where I seriously wondered if the problem was me all along.\nThe correct diagnosis would have taken five seconds. I did not find it in five seconds.\nThe Reasonable Wrong Answer USB-C dock on a Thunderbolt-capable laptop, video not working. The obvious suspect is Thunderbolt authorization. Linux requires Thunderbolt devices to be explicitly authorized before they can pass video — the bolt daemon handles this, and the security level on most systems defaults to user or secure, which means unrecognized devices are blocked until approved. USB peripherals (keyboard, mouse) work because they\u0026rsquo;re enumerated through the USB controller, not the Thunderbolt controller. DisplayPort Alt Mode video goes through Thunderbolt. If bolt isn\u0026rsquo;t running, or if the device hasn\u0026rsquo;t been authorized, it would look exactly like what I saw.\nTurns out, that reasoning is entirely correct. For a Thunderbolt dock.\nMy T14-specific NixOS profile (nixos/profiles/laptop/t14.nix) looked like this before I touched it:\n{inputs, ...}: { imports = [ ./. ./power.nix inputs.nixos-hardware.nixosModules.lenovo-thinkpad-t14-intel-gen1 ]; } Sensible. Minimal. I made it worse:\nservices.hardware.bolt.enable = true; boot.kernelParams = [\u0026#34;i915.enable_dp_mst=1\u0026#34;]; The bolt line enables the Thunderbolt daemon and, because KDE Plasma ships plasma-thunderbolt, you get a little system tray widget to authorize devices. The i915.enable_dp_mst=1 was purely speculative — I threw it in because some docks have an internal DisplayPort Multi-Stream Transport hub that needs MST support from the GPU driver, and I figured it couldn\u0026rsquo;t hurt. (I was correct here, it didn\u0026rsquo;t hurt. More precisely, the only thing it accomplished was \u0026ldquo;not hurting\u0026rdquo;).\nBuild was clean. nh os build added bolt 0.9.8 and plasma-thunderbolt 6.6.4. After just switch and a reboot:\n$ systemctl status bolt ● bolt.service - Thunderbolt system service Active: active (running) ... May 05 17:59:24 wendigo boltd[5619]: udev: found 0 domain May 05 17:59:24 wendigo boltd[5619]: udev: enumerating devices May 05 17:59:44 wendigo boltd[5619]: power: setting force_power to OFF $ boltctl list # (no output) udev: found 0 domain. The kernel isn\u0026rsquo;t exposing any Thunderbolt domains at all. boltctl list is silent. The dock isn\u0026rsquo;t showing up because, from the kernel\u0026rsquo;s perspective, there is no Thunderbolt controller talking to anything. This wasn\u0026rsquo;t a configuration problem; the hardware wasn\u0026rsquo;t what I assumed it was.\nI reverted both changes and went to look at what was actually there.\nlsusb. Obviously. Run lsusb \u0026ldquo;First\u0026rdquo; The diagnostic I should have started with:\n$ sudo dmesg | rg -i \u0026#34;thunderbolt|usb4|tbt\u0026#34; [ 0.015200] ACPI: SSDT 0x00000000A652D000 001F15 (v02 LENOVO WHL_Tbt_ ...) [ 6.155008] ACPI: bus type thunderbolt registered Only ACPI table entries — the firmware knows Thunderbolt exists as a bus type, but no actual controller was probed, no domain registered. Then:\n$ sudo dmesg | rg -i \u0026#34;i915|drm|displaylink\u0026#34; [ 4.119714] usb 2-2.2: New USB device found, idVendor=17e9, idProduct=6015, bcdDevice=31.04 [ 4.119719] usb 2-2.2: Manufacturer: DisplayLink Hmm\u0026hellip; Manufacturer: DisplayLink. Let\u0026rsquo;s just dig in there a little deeper:\n$ lsusb Bus 002 Device 003: ID 17e9:6015 DisplayLink ThinkPad Hybrid USB-C with USB-A Dock There\u0026rsquo;s the dock\u0026rsquo;s full product name: ThinkPad Hybrid USB-C with USB-A Dock. The word \u0026ldquo;Hybrid\u0026rdquo; is doing a lot of work there, and I\u0026rsquo;d completely missed it. Lenovo designed this dock to connect to hosts via either USB-C or USB-A — because USB-A has no DisplayPort Alt Mode capability at all, they couldn\u0026rsquo;t build it around native video passthrough. Instead, the dock contains a DisplayLink USB graphics chip that compresses the framebuffer, ships it over USB, and decompresses it in the dock. The graphics chip doesn\u0026rsquo;t care whether the host connected via USB-C or USB-A; it just needs USB bandwidth.\nThe architecture difference matters enormously for driver requirements:\nArchitecture How video works Driver required DP Alt Mode GPU drives display directly via repurposed USB-C lanes None (native i915) Thunderbolt 3/4 DP signal tunneled through the TB controller None (bolt for auth) DisplayLink CPU compresses framebuffer, sent over USB to the dock chip Proprietary kernel module + userspace daemon A native DP Alt Mode dock just works on Linux. A Thunderbolt dock needs bolt for authorization but otherwise just works. A DisplayLink dock needs a proprietary kernel module (evdi) and a closed-source userspace daemon from Synaptics. I had the wrong dock for the thing I wanted — zero-config Linux compatibility.\nWas this clear from the product name? In retrospect, absolutely. Would I have noticed it without the lsusb output slapping me in the face? Shmaybe?\nIn Which I Briefly Try to Make DisplayLink Work Look, I knew this was unlikely to go well \u0026ndash; but I already had several browser tabs open.\nThe DisplayLink Linux package lives in nixpkgs under pkgs/os-specific/linux/displaylink/default.nix. It uses requireFile because Synaptics\u0026rsquo;s license prohibits redistribution — you have to manually download the installer and nix-store --add-fixed it yourself. The NixOS module lives at nixos/modules/hardware/video/displaylink.nix. I should have read it before trying anything. I skipped reading it before trying anything.\nEasy enough to activate: services.xserver.videoDrivers = [\u0026quot;displaylink\u0026quot;]. The module keys off that list to decide whether to load evdi and start the dlm.service daemon.\nAfter finding the correct option name, I actually opened the module source. Here\u0026rsquo;s the relevant piece:\nlet enabled = lib.elem \u0026#34;displaylink\u0026#34; config.services.xserver.videoDrivers; in { config = lib.mkIf enabled { boot.extraModulePackages = [ evdi ]; boot.kernelModules = [ \u0026#34;evdi\u0026#34; ]; services.xserver.externallyConfiguredDrivers = [ \u0026#34;displaylink\u0026#34; ]; environment.etc.\u0026#34;X11/xorg.conf.d/40-displaylink.conf\u0026#34;.text = \u0026#39;\u0026#39; Section \u0026#34;OutputClass\u0026#34; Identifier \u0026#34;DisplayLink\u0026#34; MatchDriver \u0026#34;evdi\u0026#34; Driver \u0026#34;modesetting\u0026#34; ... EndSection \u0026#39;\u0026#39;; services.xserver.displayManager.sessionCommands = \u0026#39;\u0026#39; ${lib.getBin pkgs.xrandr}/bin/xrandr --setprovideroutputsource 1 0 \u0026#39;\u0026#39;; ... systemd.services.dlm = { description = \u0026#34;DisplayLink Manager Service\u0026#34;; ... }; }; } /etc/X11/xorg.conf.d/. externallyConfiguredDrivers. xrandr --setprovideroutputsource. Every single piece of this module is X11 plumbing. The laptop profile defaults to Wayland. KDE Plasma 6 on Wayland ignores xorg.conf.d entirely, doesn\u0026rsquo;t use xrandr for output management, and doesn\u0026rsquo;t load the Xorg modesetting driver at all.\nDisplayLink on Wayland is an unresolved upstream problem. Synaptics has been working on it, but it\u0026rsquo;s not production-ready in 2026, and nothing in this NixOS module moves that needle. The community workaround is forcing the session back to X11, which trades away Wayland features/improvements in exchange for getting a dock working — a dock that, by the way, introduces CPU overhead for every frame rendered, because all of that framebuffer compression happens on the host CPU.\nWell\u0026hellip; crap. The hardware problem may not be something with a software solution afterall. I\u0026rsquo;m as shocked as you are.\nActually Solving It My requirements aren\u0026rsquo;t too complicated:\nHDMI 2.0 output (the TESmart KVM does 4:4:4 chroma at 4K@60Hz; HDMI 1.4 won\u0026rsquo;t do it) Gigabit Ethernet 65W+ Power Delivery (T14 and X1 Carbon charging) USB-C host connection DP Alt Mode — explicitly not DisplayLink (see everything above) The easiest way to verify #5 before buying is lsusb output from someone who already owns the dock. That is a level of planning and thoroughness that I am clearly not going to start doing now.\nThe Plugable USBC-7IN1E landed at the top of the shortlist. Plugable explicitly documents 4K@60Hz HDMI support and maintains a USB-C Alt Mode explainer that makes the architecture clear. Around $40–45, driverless, and they explicitly list Linux compatibility. The Anker 555 (A8383) is a credible alternative in the same price range, though Anker doesn\u0026rsquo;t officially support Linux (it works in practice). The Targus DOCK430USZ and refurbished Lenovo 40AY0090US are both solid options if you want something closer to enterprise-grade, at roughly $60–100.\nThe Arch Linux forums have a useful thread on T14 + USB-C + 4K that confirms the chroma subsampling behavior and validates that native DP Alt Mode docks just work without touching the kernel or xorg config.\nNote the naming pattern: the dock I don\u0026rsquo;t want is the Hybrid USB-C with USB-A dock. The dock I do want is the Universal USB-C Dock, the USB-C 7-in-1, the straightforward naming that doesn\u0026rsquo;t require a compatibility matrix. \u0026ldquo;Hybrid\u0026rdquo; means \u0026ldquo;we needed it to work with USB-A hosts, so we put a DisplayLink chip inside.\u0026rdquo; That\u0026rsquo;s a real engineering constraint Lenovo solved reasonably well — it\u0026rsquo;s just not compatible with what I\u0026rsquo;m trying to do.\nWhat I Should Have Done lsusb. That\u0026rsquo;s it. Five seconds.\n$ lsusb | grep -i lenovo Bus 002 Device 003: ID 17e9:6015 DisplayLink ThinkPad Hybrid USB-C with USB-A Dock 17e9 is DisplayLink. The word \u0026ldquo;Hybrid\u0026rdquo; is in the name. Everything else that followed — the Thunderbolt hypothesis, the bolt daemon, the speculative i915.enable_dp_mst=1, the X11-only module audit — all of it was downstream of not checking what hardware was actually present before starting to theorize.\nThe second thing I should have done: one change at a time. I added bolt and the kernel parameter together, which meant when the fix didn\u0026rsquo;t work, I had two things to unwind and no signal about which (if either) had done anything. Each hypothesis gets one change, one rebuild, one test. This is obvious advice that I ignored because I was confident in my diagnosis. That confidence was misplaced.\nThe third thing: read the module source before enabling it. nixos/modules/hardware/video/displaylink.nix has xrandr in the sessionCommands. That\u0026rsquo;s an immediate tell — xrandr doesn\u0026rsquo;t manage Wayland outputs. I would have known in thirty seconds that this path was going nowhere.\nHardware problems shouldn\u0026rsquo;t have software solutions. A DisplayLink dock on a Wayland system in 2026 is not a NixOS configuration gap; it\u0026rsquo;s an upstream compatibility gap that no amount of declarative config can paper over. The right response is a $45 dock with native Alt Mode support, not a creative workaround for a proprietary graphics protocol designed to run over USB 3.0. To be fair, I could just switch away from Wayland and I\u0026rsquo;d be fine. Apparently I\u0026rsquo;ve decided that I want the small dopamine hit from buying something shiny instead.\nt14.nix is back to its original five lines. Nothing changed. The new dock is in the mail.\n","permalink":"https://asphaltbuffet.com/posts/2026/05/nixos-undocked/","summary":"\u003cp\u003eDocks are basically plug-and-play, right? So if the monitor isn\u0026rsquo;t working, there must be a setting that\u0026rsquo;s off somewhere. This will be easy.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e(narrator voice: it would not, in fact, be easy)\u003c/em\u003e\u003c/p\u003e","title":"Two Hours of NixOS Config I Didn't Need"},{"content":"When I want to know how well my HVAC system is working, smart thermostats and ad-hoc checks don\u0026rsquo;t get me as far as I\u0026rsquo;d like. That means it\u0026rsquo;s time to create an entire system to monitor that information and present it graphically. This is normal, right?\nRequirements Overall, I didn\u0026rsquo;t want to spend much money on this. Yes, time is money, blah blah blah. Learning new tech offsets the cost of time, and it\u0026rsquo;s a better use of late-night chill time than playing a game anyway. Plus, spoilers\u0026hellip; there are graphs. That broad overview gave me the following criteria:\nDon\u0026rsquo;t buy much extra stuff. Keep it less than $20 spending. Easy to set up and keep running. The initial scale is one sensor. Don\u0026rsquo;t optimize until later, if ever. Choices, Choices, Choices Selecting the brains I have a box of microcontrollers in my project area. First, I was going to use a BeagleBoard that I got ~10 years ago at the Embedded Linux Conference. However, I could not find a power adapter for it, so after 20 minutes of searching, I nixed that idea.\nI considered using a raspberry pi, but even though I have a couple spares, I didn\u0026rsquo;t want that level of overkill to get a couple temp readings sent to the cloud.\nOh, but the handful of Arduino Ethernet boards that have PoE? Yes, please. It takes care of my need to plug into power (sort of, we\u0026rsquo;ll cover this later). 32k of memory should be fine (yeah, we\u0026rsquo;ll touch on this later too). The temp probes that I have will work fine with this and require minimal breadboard work.\nSelecting the sensors This took no time. I needed something I could insert into holes in our ductwork that our HVAC guy cut several months ago. I wanted to use these since it meant that it would get readings from the same place they do for efficiency checks, and I could do less permanent damage to our HVAC system.\nI also had two DS18B20 sensors laying around from a forgotten project. They are waterproof with long enough cords to work well around the furnace. As a bonus, they use a digital signal so I could use a couple of them on the same input on the arduino without worrying about collisions. This would be really handy later.\nSelecting the backend tooling I wanted to use MQTT for sensor communication so that I wasn\u0026rsquo;t creating my own protocol. There are a lot of alternatives that I never even considered using. Why? I know people who use MQTT at work. The availability of resources to sanity-check my work meets my goals for #2 really well.\nDatabase and visualization were chosen via similar criteria. I\u0026rsquo;ve used Grafana a lot recently to create dashboards, and there\u0026rsquo;s a free cloud version that gets me through proof-of-concept. Same deal with Timescale. The additional benefit of these technologies is that I can spin up instances at home for free later if I want to keep this party going. Or, I can decide to pay for the fully-managed versions in the cloud. This is a problem for future me and I\u0026rsquo;m good for now with what I have.\nSensor Development Wiring the Parts Together The sensor hardware itself was planned to be as plug-and-play as possible. I added an LED later to see flashy lights. It adds very little value since I am not spending much time in the room where this lives. The LED functionality isn\u0026rsquo;t even in the code I walk through below as it\u0026rsquo;s there as a debug tool more than anything, but I may update that source code later if I decide to leave it in.\nSensor Programming I started with the sensor ready and no real design. Was this a mistake? Yes. Would I do it the same next time? Also, yes.\nHonestly, the project software is fairly simple so skipping the design meant that now I need to redo a few things to get my data structured better and clean up a few edge cases for failures. It works as is, but it\u0026rsquo;s not pretty. Warts will be highlighted as we go. Grab the popcorn and follow along on the journey!\nInitially, I had planned to set up TinyGo and program the boards that way. I have not used TinyGo before. As of this post, I still have not used TinyGo. Arduino provides a web editor for creating sketches and installing them on boards with a minimal tool download. This was easy. See #2 criteria for information on why I went this route.\nI tackled the sensor programming in the following order:\nGet temp readings working Get the network connection working Get MQTT communication working Send temp readings via MQTT Reading in temps Fortunately, there\u0026rsquo;s already an arduino library for the sensors I used. I only needed to include OneWire.h and DallasTemperature.h for the software to function. I found a reference on wiring up multiple sensors that worked right away. In an hour or so, I had the following:\n// OneWire - Version: Latest #include \u0026lt;OneWire.h\u0026gt; // DallasTemperature - Version: Latest #include \u0026lt;DallasTemperature.h\u0026gt; #include \u0026lt;SPI.h\u0026gt; #include \u0026lt;Ethernet.h\u0026gt; // hardware config #define ONE_WIRE_BUS 2 OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(\u0026amp;oneWire); // take reading every 60s const long interval = 6000; unsigned long previousMillis = 0; int count = 0; void setup() { // Useful for debugging purposes Serial.begin(9600); while (!Serial) { ; // wait for serial port to connect. Needed for native USB port only } sensors.begin(); } void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis \u0026gt;= interval) { Serial.print(\u0026#34;Sensor loop #\u0026#34;); Serial.println(count); // save last time message sent previousMillis = currentMillis; // Get temp(s) sensors.requestTemperatures(); int temp_count = sensors.getDeviceCount(); for (int i = 0; i \u0026lt; temp_count; i++) { Serial.print(\u0026#34;Reading temp #\u0026#34;); Serial.print(i); Serial.print(\u0026#34;: \u0026#34;); Serial.println(sensors.getTempCByIndex(i)); } Serial.println(); count++; } } Running this code, I was getting two readings every minute\u0026hellip; oops. No, every 6 seconds. It\u0026rsquo;s helpful to remember that arduino tracks \u0026ldquo;time\u0026rdquo; in milliseconds. My math was off, but it was fine here. I fixed that and moved to the next area. The next area was not connecting to the network. Instead, I distracted myself by tweaking the output.\nJSON I realized that parsing a message with unstructured text would be a pain. I had not done design, remember, but I was getting a tickle in my head where I knew that moment would arrive soon. I substituted some long, solid engineering planning with a spur-of-the-moment design that later would prove to be insufficient.\nNobody wants to write JSON with print statements though. No, you don\u0026rsquo;t. Stop. There\u0026rsquo;s a library for that too. I still have more than half my program space left on the board at this point; no worries. Plus, ArduinoJson has a nice example I could borrow from.\nI can just include ArduinoJson.h and ArduinoJson.hpp to create a serializable variable for json. This would be awesome when I got around to adding MQTT. But I didn\u0026rsquo;t know that yet. I was just going to print it to the console. It just needed a few tweaks to the code.\nFirst, add some const char * variables for the json keys. Doing this statically means that the JSON object can be smaller since the library will link to those instead of trying to store them separately.\nconst char readingKey[] = \u0026#34;reading_idx\u0026#34;; const char tempsKey[] = \u0026#34;temps\u0026#34;; Next, create the JSON document and populate it. BUT\u0026hellip; this is static, so I needed to use a tool (from the same place that created the library) to calculate the size I\u0026rsquo;d need. This worked well. I did it wrong later, but it worked well. Eventually, I added more strings and some were dynamic. I never updated this value. It will probably have memory issues at some point. Sorry, future-me!\n// Use arduinojson.org/v6/assistant to compute the capacity. StaticJsonDocument\u0026lt;48\u0026gt; doc; doc[nameKey] = name; doc[readingKey] = count; JsonArray data = doc.createNestedArray(tempsKey); int temp_count = sensors.getDeviceCount(); for (int i = 0; i \u0026lt; temp_count; i++) { Serial.print(\u0026#34;Reading temp #\u0026#34;); Serial.println(i); data.add(sensors.getTempCByIndex(i)); } serializeJson(doc, Serial); Networking (oops, also everything else) It\u0026rsquo;s a very good idea to change one thing at a time. I am quite capable of identifying that as a best practice. I am not as skilled when I need to actually follow my own advice. When I added network support. I simultaneously added MQTT as well. I lucked out that none of it is that difficult or complicated. It worked on the third or fourth try. Most issues were with me doing dumb things when I tried to verify communication; not in the actual sensor code itself.\nThe final code (which is running on my sensor right now) is as follows:\n```arduino // ArduinoJson - Version: Latest #include \u0026lt;ArduinoJson.h\u0026gt; #include \u0026lt;ArduinoJson.hpp\u0026gt; // OneWire - Version: Latest #include \u0026lt;OneWire.h\u0026gt; // DallasTemperature - Version: Latest #include \u0026lt;DallasTemperature.h\u0026gt; // ArduinoMqttClient - Version: Latest #include \u0026lt;ArduinoMqttClient.h\u0026gt; #include \u0026lt;SPI.h\u0026gt; #include \u0026lt;Ethernet.h\u0026gt; // hardware config #define ONE_WIRE_BUS 2 OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(\u0026amp;oneWire); // Set your MAC address byte mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x95, 0x0F }; const char name[] = \u0026#34;90:a2:da:00:95:0f\u0026#34;; const char nameKey[] = \u0026#34;name\u0026#34;; const char readingKey[] = \u0026#34;reading_idx\u0026#34;; const char tempsKey[] = \u0026#34;temps\u0026#34;; const char broker[] = \u0026#34;test.mosquitto.org\u0026#34;; int port = 1883; const char dataTopic[] = \u0026#34;Hotpants/data\u0026#34;; // take reading every 60s const long interval = 60000; unsigned long previousMillis = 0; int count = 0; // Ethernet and MQTT related objects EthernetClient ethClient; MqttClient mqttClient(ethClient); void setup() { // Useful for debugging purposes Serial.begin(9600); while (!Serial) { ; // wait for serial port to connect. Needed for native USB port only } // Start the ethernet connection (DHCP) Serial.print(\u0026#34;Attempting to connect to network\u0026#34;); while (Ethernet.begin(mac) == 0) { // failed, retry Serial.print(\u0026#34;.\u0026#34;); delay(3000); } Serial.print(\u0026#34;You\u0026#39;re connected to the network. IP address = \u0026#34;); Serial.println(Ethernet.localIP()); Serial.println(); // Attempt to connect to the server Serial.print(\u0026#34;Attempting to connect to MQTT broker: \u0026#34;); Serial.println(broker); if (mqttClient.connect(broker, port)) { Serial.println(\u0026#34;Connection established\u0026#34;); Serial.println(); } else { Serial.print(\u0026#34;MQTT connection failed. Error code = \u0026#34;); Serial.println(mqttClient.connectError()); // block if unable to establish connection while (1); } sensors.begin(); } void loop() { // This is needed at the top of the loop! mqttClient.poll(); unsigned long currentMillis = millis(); if (currentMillis - previousMillis \u0026gt;= interval) { // save last time message sent previousMillis = currentMillis; // Get temp(s) sensors.requestTemperatures(); // Use arduinojson.org/v6/assistant to compute the capacity. StaticJsonDocument\u0026lt;48\u0026gt; doc; doc[nameKey] = name; doc[readingKey] = count; JsonArray data = doc.createNestedArray(tempsKey); int temp_count = sensors.getDeviceCount(); for (int i = 0; i \u0026lt; temp_count; i++) { Serial.print(\u0026#34;Reading temp #\u0026#34;); Serial.println(i); data.add(sensors.getTempCByIndex(i)); } Serial.print(\u0026#34;Sending message. Topic = \u0026#34;); Serial.println(dataTopic); serializeJson(doc, Serial); // send message mqttClient.beginMessage(dataTopic); serializeJson(doc, mqttClient); mqttClient.endMessage(); Serial.println(); count = (count + 1) % 128; } } There are a few issues that I need to address.\nIf the MQTT connection dies. You have to manually restart the sensor. This is not ideal. The mac address, broker, topic, etc. are all hardcoded in the sketch. This is annoying if I want to change anything. I have to disconnect the sensor from the furnace area, bring to my desk, reprogram and take it back. I have some small microSD cards to use and create a config to read in some values, but that\u0026rsquo;s for a later enhancement. I\u0026rsquo;m running at about 90% program space used. Adding other functionality may require some optimization. I never send any information about the sensor itself via MQTT. This would be helpful for later use (mac address, sensor type, etc.) The biggest problem\u0026hellip; I\u0026rsquo;m sending the temps in Celcius. Normally fine, but I want to see the HVAC Evaporator temp delta in Fahrenheit. I convert this later, but that\u0026rsquo;s dumb. I will change this very soon. It\u0026rsquo;s annoying, but I am taking readings if the furnace is on or off. I don\u0026rsquo;t really care what the readings are when it\u0026rsquo;s off. They are meaningless. So I will need to add in a different mechanism to detect if the furnace is turned on and send readings then. I have a few ideas on how to do this but haven\u0026rsquo;t done any further work than that. Backend Development MQTT Broker I started off by using test.mosquitto.org and realized it works fine for what I\u0026rsquo;m doing now. I will eventually change this to be hosted locally, but not now. It works well enough for who it\u0026rsquo;s for (that\u0026rsquo;s me, btw).\nTimescale I was going to do this locally. I installed postgresql on my linux server. I\u0026rsquo;m not including what I did for that because I haven\u0026rsquo;t used it at all. I created a Timescale Cloud account and which is free for 30 days. In a month, I\u0026rsquo;ll check to see how much that would cost me to avoid maintaining it locally. The setup with their instructions had me up and running within 20 minutes. A+ rating from me so far.\nGrafana Another free trial instance that is sufficient for what I need in a proof-of-concept. I will probably use a local instance here eventually, but it\u0026rsquo;s working as is for now.\nService Development: The Glue (aka shortshorts) I briefly looked to see if there was a handy service that would take my MQTT data and magically put it into Timescale. No luck. Some things sorta do that. But I haven\u0026rsquo;t done any projects in Go that have networking, async communication, or contexts. Let\u0026rsquo;s roll up our sleeves and figure out 50% of the battle GI Joe was talking about.\nAs a brief aside\u0026hellip; The name \u0026ldquo;shortshorts\u0026rdquo; has very little meaning to this project. I needed a name for the MQTT topic and had chosen \u0026ldquo;Hotpants\u0026rdquo; as it seemed funny at 1am when I was developing that part. I didn\u0026rsquo;t want the topic and this service to be named the same, but I wanted them to share a theme. Nothing further; no hidden meanings.\nMy first pass at a go application to stitch this all together was simplistic. It connected to MQTT and printed the sensor data to the console. There was no lipstick on this pig. I pulled the majority of the code from a couple examples found on GitHub and elsewhere.\nOnce I had that working, I needed to get it into Timescale. Now I must tell you how much I love Timescale for their documentation and examples. I could take almost everything I needed from their Go Tutorial.\nI had everything crammed into main.go and it worked. I didn\u0026rsquo;t want to share that code with anyone at that point though. Time to make this more like a service, I thought. Hours passed. I ate a Thanksgiving meal and the leftovers. I had a hot mess of packages and channels; partially working, but nothing looked right. Time to use a lifeline and call a friend. One hour with Jack, and it looked really good. There are still warts, but they are self-inflicted and I\u0026rsquo;m ok with them for now.\nApplication Code Highlights All of the go application code is available on GitHub.\nThe application flow starts in main (this should not be the interesting part of what I\u0026rsquo;m saying). I set up some contexts to help everything shut down nicely when it\u0026rsquo;s time, and make all the needed connections before I split off a thread for processing data. That all gets wrapped up with some shutdown logic and we have a decent core to build upon.\nfunc main() { ctx, cancel := context.WithCancel(context.Background()) tsdb, dstream, client := start(ctx) go processLoop(ctx, dstream, tsdb) servicemanager.WaitShutdown(func() { shutdown(cancel, tsdb, client) }) } The processing function loops forever until the context completes. Otherwise, it is reading data from the MQTT data stream channel.\nfunc processLoop(ctx context.Context, ds chan [2]string, tsdb *timescalewrapper.Database) { for { select { case \u0026lt;-ctx.Done(): return case d := \u0026lt;-ds: logger.Info(\u0026#34;received sensor data\u0026#34;, zap.String(\u0026#34;topic\u0026#34;, d[0]), zap.String(\u0026#34;payload\u0026#34;, d[1])) var reading timescalewrapper.SensorData err := json.Unmarshal([]byte(d[1]), \u0026amp;reading) if err != nil { logger.Error(\u0026#34;unmarshalling payload\u0026#34;, zap.Error(err), zap.String(\u0026#34;payload\u0026#34;, d[1])) } logger.Debug(\u0026#34;unmarshalling payload\u0026#34;, zap.Any(\u0026#34;reading\u0026#34;, reading)) err = tsdb.InsertData(reading) if err != nil { logger.Error(\u0026#34;inserting data\u0026#34;, zap.Error(err), zap.Any(\u0026#34;reading\u0026#34;, reading)) } } } } I also ended up using viper to pull configuration from a file. I could have used environmental variables or CLI arguments; I may still end up with either of those. Putting that in a simple YAML document is easy and works well. No more putting sensitive data into my source code. There\u0026rsquo;s no validation here and it feels like a kludge. It\u0026rsquo;s a TODO for later.\nfunc readConfig() string { viper.SetConfigName(\u0026#34;config\u0026#34;) viper.SetConfigType(\u0026#34;yaml\u0026#34;) viper.AddConfigPath(\u0026#34;.\u0026#34;) err := viper.ReadInConfig() if err != nil { logger.Panic(\u0026#34;reading config file\u0026#34;, zap.Error(err)) } var sb strings.Builder sb.WriteString(\u0026#34;postgres://\u0026#34;) sb.WriteString(viper.GetString(\u0026#34;timescale.user\u0026#34;)) sb.WriteString(\u0026#34;:\u0026#34;) sb.WriteString(viper.GetString(\u0026#34;timescale.password\u0026#34;)) sb.WriteString(\u0026#34;@\u0026#34;) sb.WriteString(viper.GetString(\u0026#34;timescale.host\u0026#34;)) sb.WriteString(\u0026#34;:\u0026#34;) sb.WriteString(viper.GetString(\u0026#34;timescale.port\u0026#34;)) sb.WriteString(\u0026#34;/\u0026#34;) sb.WriteString(viper.GetString(\u0026#34;timescale.database\u0026#34;)) sb.WriteString(\u0026#34;?sslmode=\u0026#34;) sb.WriteString(viper.GetString(\u0026#34;timescale.sslmode\u0026#34;)) return sb.String() } In the MQTT package, I set up my connection and create a subscription to the sensor data topic. This is where I populate the channel used elsewhere for data streaming.\nf := func(client mqtt.Client, msg mqtt.Message) { datastream \u0026lt;- [2]string{msg.Topic(), string(msg.Payload())} } if token := cli.Subscribe(topic, byte(qos), f); token.Wait() \u0026amp;\u0026amp; token.Error() != nil { logger.Error(\u0026#34;error subscribing to topic\u0026#34;, zap.Error(token.Error())) return nil, token.Error() } Finally, putting the data into the database is fairly straightforward. I have a lot of the structure hardcoded and may want to make this a little more dynamic later to help with reducing the coupling between what I develop on the sensor and how that data gets into Timescale. That\u0026rsquo;s a low priority item, though, as I don\u0026rsquo;t expect to see a significant amount of churn here. There are only so many ways to send the readings I\u0026rsquo;m gathering. I could plan for additional complexity, but that violates at least two of my project criteria. YAGNI indeed.\n// InsertData inserts the data into the database. func (db *Database) InsertData(reading SensorData) error { queryInsertData := `INSERT INTO conditions (time, mac, temp_delta, raw_temp0, raw_temp1) VALUES ($1, $2, $3, $4, $5)` _, err := db.pool.Exec(context.Background(), queryInsertData, time.Now(), reading.Mac, reading.Temps[1]-reading.Temps[0], reading.Temps[0], reading.Temps[1]) if err != nil { return fmt.Errorf(\u0026#34;inserting data: %w\u0026#34;, err) } return nil } Results Now, Faithful Reader, if you\u0026rsquo;ve made it this far\u0026hellip; what does this actually look like?\n{\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:\u0026#34;2022-11-27T20:49:37.475-0500\u0026#34;,\u0026#34;caller\u0026#34;:\u0026#34;mqttwrapper/mqtt.go:43\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Connected to broker\u0026#34;,\u0026#34;broker\u0026#34;:\u0026#34;tcp://test.mosquitto.org:1883\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:\u0026#34;2022-11-27T20:49:37.565-0500\u0026#34;,\u0026#34;caller\u0026#34;:\u0026#34;mqttwrapper/mqtt.go:65\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;successfully subscribed\u0026#34;,\u0026#34;topic\u0026#34;:\u0026#34;Hotpants/data\u0026#34;,\u0026#34;broker\u0026#34;:\u0026#34;tcp://test.mosquitto.org:1883\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:\u0026#34;2022-11-27T20:56:52.395-0500\u0026#34;,\u0026#34;caller\u0026#34;:\u0026#34;shortshorts/main.go:77\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;received sensor data\u0026#34;,\u0026#34;topic\u0026#34;:\u0026#34;Hotpants/data\u0026#34;,\u0026#34;payload\u0026#34;:\u0026#34;{\\\u0026#34;name\\\u0026#34;:\\\u0026#34;90:a2:da:00:95:0f\\\u0026#34;,\\\u0026#34;reading_idx\\\u0026#34;:0,\\\u0026#34;temps\\\u0026#34;:[21.3125,20.6875]}\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:\u0026#34;2022-11-27T20:57:52.409-0500\u0026#34;,\u0026#34;caller\u0026#34;:\u0026#34;shortshorts/main.go:77\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;received sensor data\u0026#34;,\u0026#34;topic\u0026#34;:\u0026#34;Hotpants/data\u0026#34;,\u0026#34;payload\u0026#34;:\u0026#34;{\\\u0026#34;name\\\u0026#34;:\\\u0026#34;90:a2:da:00:95:0f\\\u0026#34;,\\\u0026#34;reading_idx\\\u0026#34;:1,\\\u0026#34;temps\\\u0026#34;:[21.25,20.625]}\u0026#34;} The logging fills up my terminal nicely and gives me an indication that things are working or not. I\u0026rsquo;ve found that the sensor stops reporting after a day or two. Power cycling brings it back online and I should really look into why that happens. I\u0026rsquo;m fairly certain it\u0026rsquo;s a memory issue on the sensor or a failure to recover from a dead MQTT connection.\nThe Grafana dashboard works well and shows me the information I was hoping to see. I believe my temperature probe for the HVAC return is improperly placed and needs to be adjusted. It never really increases when the system is running. It\u0026rsquo;s likely giving a reading of the basement ambient temp instead of air temps inside the return vent. I\u0026rsquo;ll tweak it a bit while monitoring the readings to check further. The other possibility is that my system is completely hosed and I need to call the HVAC company for service.\nI wonder what they\u0026rsquo;d say if I send them this dashboard output as the reason for my service request?\n","permalink":"https://asphaltbuffet.com/posts/2022/11/hvac-monitoring/","summary":"\u003cp\u003eWhen I want to know \u003ca href=\"https://sumzim.com/supply-vs-return-vents\"\u003ehow well my HVAC system is working\u003c/a\u003e, smart thermostats and ad-hoc checks don\u0026rsquo;t get me as far as I\u0026rsquo;d like. That means it\u0026rsquo;s time to create an entire system to monitor that information and present it graphically. This is normal, right?\u003c/p\u003e\n\u003cfigure class=\"align-center \"\u003e\n    \u003cimg loading=\"lazy\" src=\"in-situ.jpg#center\"\n         alt=\"HVAC sensor installed in-situ\"/\u003e \n\u003c/figure\u003e\n\n\u003ch2 id=\"requirements\"\u003eRequirements\u003c/h2\u003e\n\u003cp\u003eOverall, I didn\u0026rsquo;t want to spend much money on this. Yes, time is money, blah blah blah. Learning new tech offsets the cost of time, and it\u0026rsquo;s a better use of late-night chill time than playing a game anyway. Plus, spoilers\u0026hellip; there are graphs. That broad overview gave me the following criteria:\u003c/p\u003e","title":"Monitoring my HVAC"},{"content":"Running some static code scanning on my rendered site with Sonarcloud, I found some potential security concerns with how I was loading the mermaid javascript.\nCooling off those hotspots! An article on Bits and Pieces had additional information on how to use SRI functionality in modern browsers. I used srihash.org to calculate the hash for the mermaid js file. After that, it was easy enough to modify layouts/partials/extend_head.html to include the integrity check:\n{{ if .Page.Store.Get \u0026#34;hasMermaid\u0026#34; }} \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/mermaid@9.1.3/dist/mermaid.min.js\u0026#34; integrity=\u0026#34;sha384-LnGjpNDrP4cp7MIk4CpRa/lPNclrf839ryYVFx1T1mPSV3RGAZ7nlBa7pqcyGY/K\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; mermaid.initialize({ startOnLoad: true }); \u0026lt;/script\u0026gt; {{ end }} I also switched to using a specific version of the js package so that the hash would have a static file. It\u0026rsquo;s good to keep that control over which versions of scripts are running, so there are no complaints here.\nAn Important Detail The crossorigin=\u0026quot;anonymous\u0026quot; part is crucial to making this effective.\nWithout a crossorigin attribute, the browser will choose to \u0026lsquo;fail-open\u0026rsquo; which means it will load the resource as if the integrity attribute was not set, effectively losing all the security SRI brings in the first place.\nsrihash.org ","permalink":"https://asphaltbuffet.com/posts/2022/07/js-integrity/","summary":"\u003cp\u003eRunning some static code scanning on my rendered site with \u003ca href=\"https://sonarcloud.io/\"\u003eSonarcloud\u003c/a\u003e, I found some potential security concerns with how I was loading the mermaid javascript.\u003c/p\u003e","title":"SRI: Fixing Security Warnings"},{"content":"Here is the setup I\u0026rsquo;ve been using in Obsidian since late March 2022. The default plugins and configuration would provide most of the functionality I want. However, I tweaked a few things to add more value than my Bullet Journal could provide.\nPlugins I use quite a few plugins and have most of them configured uniquely. There are a few negatives to this approach:\nload time for obsidian is slow1 keeping track of plugins is a bit of a pain must manually synchronize/manage installed plugins2 That said, I\u0026rsquo;m happy with the current configuration.\nAdmonition I\u0026rsquo;ve added a few custom admonition types.\nThese get used for daily/weekly/monthly notes when organizing my to-do lists. They also are a way to wrap information in correspondence tracking so that I can highlight important details. While not absolutely necessary, they are helpful and make the journaling look better organized.\nAdvanced Tables I use advanced tables so that I can be lazier when writing up information in journals. It auto-sizes the header to match the width of cell content. I used to do this manually by copy-pasta with Prettier, but now I keep that all in the journal entry.\nCalendar I use this plugin extensively, multiple times a day. Not only does it give an easy way to move through daily logging, but it also makes it easier to create periodic entries for weeks and months. While keyboard shortcuts can get me similar behavior, some days, I just want to use the mouse.\nThe only change that I made to the settings for this plugin was to show week numbers in a separate column. I use weekly notes to track my finished tasks for use in status meetings and other times when I want to skim through past accomplishments.\nDataview Rarely used by itself, this is necessary for templates, heatmaps, and task lists. I enabled javascript queries to allow some other plugin functionality, but did not otherwise make any changes.\nDynamic Table of Contents I have a few ToCs scattered throughout my journal. I generate most index lists in Dataview, so this is more of a nice-to-have. It is much more helpful when using Obsidian to draft other documents. I can keep the ToC updated automatically, then export that markdown file into another application without fiddling with some other editor\u0026rsquo;s ToC generation options.\nFootnote Shortcut An easy way to create/edit/return from footnotes. I set my hotkey (done manually) to be CMD + SHIFT + 6. It\u0026rsquo;s the same shortcut as noted in the plugin documentation. Their reasoning made sense to me, and I didn\u0026rsquo;t want to think about it much more.\nHeatmap Calendar The only heatmaps in my journal are two yearly trackers on my year pages. They aren\u0026rsquo;t really important or even that useful, but they look nice, so I keep this plugin around. If it gets updated to show monthly heatmaps, I have a few more items I\u0026rsquo;d like to track at that granularity.\nThere\u0026rsquo;s no configuration for this plugin, but I did change the date format for it to work since my daily entries do not follow the YYYY-MM-DD format.\nIcon Folder It gives me visual reminders of folder contents and nothing else. Not really needed, but pretty, so it stays around.\nIcon Shortcodes Most headers in my daily notes have an icon for recognition and visual appeal. Not required, but if I remove it now, I\u0026rsquo;d need to remove all the references from previous entries.\nLinter I configured this plugin more than most others. I have it set to lint when I save (CTRL + S) a file. I also configured it to ignore my template folder as the linter should not modify those files while template fields are present. I don\u0026rsquo;t remember which settings I\u0026rsquo;ve tweaked for linting. I sync this setting in github, so my style remains consistent across instances.\nNatural Language Dates Rarely used, this plugin is most likely to get removed from my set-up at some point. I keep it around at the moment so that I don\u0026rsquo;t have to think when I want to link to @tomorrow or @yesterday.\nI updated the date format to match my periodic note format and configured it to add the dates as a link.\nNote Refactor Helpful when I feel inclined to move a bunch of notes around and need to split out content.3 Less valuable in day-to-day operations. Another candidate for pruning soon.\nObsidian Git Configuring this was easy enough, but setting up git was more of a hassle. I usually do my development in Linux, so for Windows machines, I had to set up git with proper credentials to enable access to the private repo where I store my journal. Beyond that effort, I have the hostname set separately for each instance\u0026rsquo;s commit message to track where information was created.\nI recommend setting it up to backup after X minutes from the last file change. The inherent delay has bitten me a few times when switching between machines where files don\u0026rsquo;t quite get synced, and I need to resolve conflicts manually.\nPeriodic Notes Daily notes are the backbone of my journaling experience. Matching the format of my physical bullet journal, I have tweaked many settings here and used templates to maintain consistency. The change that most affects other plugins is my date format, YYYY.MM.DD - ddd.\nSmart Typography Keeping my style consistent is crucial, and linting doesn\u0026rsquo;t cover all aspects of text encoding. This auto-converts punctuation and symbols to a standard type and cleans up my entries overall. Unfortunately, this does not apply retroactively to pre-existing text.\nStyle Settings Highly recommended when using the Blue Topaz theme. I changed the default font and dataview list icons (Pacman was cute, but not what I wanted to see).\nTasks This is used on every daily/weekly/monthly file and many others in my collections. I use a global task filter of #task to give the option of non-task checkboxes when I need to create an untracked checklist.\nTemplater Most templates that I use are for periodic notes. However, there are also some templates for correspondence tracking and one-on-one meetings. I toyed with the idea of using Obsidian to create blog entries, but directly using Hugo\u0026rsquo;s archetypes ended up being a better solution.\nCore Settings There are a few settings that I\u0026rsquo;ve changed scattered throughout the core plugins and main application. Of note, page preview is invaluable to peek at contents for reference without the need to switch between files while writing.\nI also show backlinks by default to ease movement throughout the journal.\nSince I am using periodic notes, I have the Daily notes core plugin disabled. I also have Workspaces disabled. I found that it did not play well with backups due to plugin version mismatches.\nHotkeys I have changed the default/unset hotkeys for the following:\nCTRL + SHIFT + A Advanced Tables: Open table controls toolbar CTRL + SHIFT + B Backlinks: Toggle backlinks in document CTRL + ALT + N Create note in new pane CTRL + SHIFT + 6 Footnote Shortcut: Insert and Navigate Footnote CTRL + \u0026lt;space\u0026gt; Periodic Notes: Open daily note CTRL + M Periodic Notes: Open monthly note \u0026lt;blank\u0026gt; Periodic Notes: Open weekly note CTRL + Y Periodic Notes: Open yearly note CTRL + T Tasks: Create or edit task CTRL + ENTER Tasks: Toggle task done CTRL + SHIFT + D Templates: Insert current date \u0026lt;blank\u0026gt; Toggle checkbox status Theme I tried out a few different options in the beginning. Blue Topaz won out as it gives a nice minimal feel without being too austere. Furthermore, it plays well with my plugins and has cohesiveness for all the journal parts.\nIt can take up to a minute to load the application\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nI have to remember to check for updates when switching between computers (e.g., work, personal desktop, personal laptop). Usually, when I update one, I add a task to touch the others.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nThis doesn\u0026rsquo;t happen much after I got everything set up in the first week or two.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://asphaltbuffet.com/posts/2022/07/obsidian-setup/","summary":"\u003cp\u003eHere is the setup I\u0026rsquo;ve been using in \u003ca href=\"https://obsidian.md/\"\u003eObsidian\u003c/a\u003e since late March 2022. The default plugins and configuration would provide most of the functionality I want. However, I tweaked a few things to add more value than my \u003ca href=\"https://bulletjournal.com/\"\u003eBullet Journal\u003c/a\u003e could provide.\u003c/p\u003e","title":"Obsidian Configuration"},{"content":"It took three days after finishing the work to display Mermaid diagrams (using shortcodes) to discover an easier method. Let\u0026rsquo;s talk a bit about custom markdown rendering!\nI don\u0026rsquo;t remember the actual search that led me to the unanswered discussion on the PaperMod GitHub page. No, that\u0026rsquo;s not entirely true. I do remember how I found this. It was in my unclosed tabs as I cleaned up my browser after the initial implementation. What, you don\u0026rsquo;t have to routinely go back and close a couple (dozen) tabs following the completion of a task? Regardless, this is where you find me, dear reader.\nIn any case\u0026hellip;\nMissed Connections One of the tabs caught my eye, and I read further; maybe I could add something helpful since I just do that same thing (they were trying to display Mermaid in Hugo). Maybe I could get something else useful from the discussion. Oh, here\u0026rsquo;s that foreshadowing again.\nI was interested that they were trying to do the same thing but going at it a different way; they also linked to some sources that I hadn\u0026rsquo;t found in my first web search foray. And I learned something. It\u0026rsquo;s relatively easy to write some functionality that uses code fences like the shortcode I was already doing. My ability to remember how to write three backticks is much better than remembering the format and syntax of shortcodes so far.\nYou Had Me at ```mermaid Create layouts/_markup/render-codeblock-mermaid.html with this content:\n\u0026lt;div class=\u0026#34;mermaid\u0026#34;\u0026gt; {{- .Inner | safeHTML }} \u0026lt;/div\u0026gt; {{ .Page.Store.Set \u0026#34;hasMermaid\u0026#34; true }} We lose the justification (more on why that\u0026rsquo;s ok later). However, the last line is a real nice touch. This ensures that a page variable is set that can be used later to load the mermaid rendering javascript.\nWith that addition/adjustment, now I only have to put the following text in a markdown file, and it\u0026rsquo;s loaded just fine. Javascript is loaded as required; no frontmatter adjustments are needed.\n```mermaid graph LR A{do you need a panflute?} --\u0026gt;|yes| B[no you don\u0026#39;t] B --\u0026gt; C[no panflute] A --\u0026gt;|no| C ``` graph LR A{do you need a panflute?} --\u003e|yes| B[no you don't] B --\u003e C[no panflute] A --\u003e|no| C And yet\u0026hellip; Ok, so you might want this centered. You could write different markdown renderers and use something like mermaid-left or mermaid-center, but that feels bad, and I believe you might need to feel a little dirty for thinking about it. Just create a shortcode for centering inner content and call it a day. That example isn\u0026rsquo;t here. I may make that one later. I\u0026rsquo;m not sure at this point how much I really want/need it, so we\u0026rsquo;ll see if I get there or not.\n","permalink":"https://asphaltbuffet.com/posts/2022/06/mermaid-hugo-v2/","summary":"\u003cp\u003eIt took three days after \u003ca href=\"https://asphaltbuffet.com/posts/2022/06/mermaid-hugo/\"\u003efinishing the work\u003c/a\u003e to display \u003ca href=\"https://mermaid-js.github.io/mermaid/\"\u003eMermaid\u003c/a\u003e diagrams (using shortcodes) to discover an easier method. Let\u0026rsquo;s talk a bit about custom markdown rendering!\u003c/p\u003e","title":"Mermaid + Hugo v2: Better, Faster"},{"content":"I learned about Mermaid in 2021 while trying to find a way to better see changes in diagrams at work. Being able to look through a revision history of images was tolerable but only slightly better than the proverbial needle and the unexplained presence of a nearby haystack.\nBut Mermaid resolves my complaints by rendering text/code into fairly nice-looking diagrams and visualizations. Some are too plain for my liking (looking at you, state diagram), but they convey information graphically. That\u0026rsquo;s all I\u0026rsquo;m really looking to get here.\ntl;dr\nComplicating It All; Let\u0026rsquo;s Make a Website Mermaid files are just fine when used in a README (GitHub supports Mermaid already in its markdown) or project documentation. If I want to also post them on here, we have a problem.\n```mermaid graph LR A{do you need a panflute?} --\u0026gt;|yes| B[no you don\u0026#39;t] B --\u0026gt; C[no panflute] A --\u0026gt;|no| C ``` Ok, an inconvenience, at the least. Hugo doesn\u0026rsquo;t know about Mermaid. Not in the slightest. I guess it\u0026rsquo;s time to figure out how to get that working.\nAdding Mermaid to Hugo One option is to change my theme to something that has Mermaid built-in. Docsy is an example of this, but that isn\u0026rsquo;t something I want to consider for the site right now. I could also compile the image with Mermaid\u0026rsquo;s live editor (or use their CLI locally), but again, the intent is to keep everything together. Cats and dogs; living together.\nWhat I really want is a shortcode like {{\u0026lt; code [...] \u0026gt;}} that will let me do it inline. Following the directions from satoru.dev, I expected that I\u0026rsquo;d found the perfect example and implementation. Nope. Does nothing.\nI checked to make sure I had spelled things correctly and placed them in the right place\u0026hellip; Oh. Rereading the article, I see that I needed to put the shortcode in layouts/shortcodes/mermaid.html, but where the site had said,\nFind somewhere in your theme that\u0026rsquo;s suitable for adding a \u0026lt;script\u0026gt; [\u0026hellip;]\nI had not really done the \u0026lsquo;suitable\u0026rsquo; part of it \u0026ndash; and would continue to fail at this part. Clues on the Hugo Discourse, RTFM, RTF(Theme)M, and investigation of the layouts/partial/extended_footer.html file got me a bit closer. Nope. RTFM again and look at the example theme with Mermaid included\u0026hellip; and a little bit at the end of Alexandra Souly\u0026rsquo;s article.\nI am convinced at this point that I have fully grokked the situation. I am a god amongst men (actually, no, my self-esteem would never get there\u0026hellip;). Update. Refresh.\nFrak.\nTime to look at the raw page source. Hmm, there isn\u0026rsquo;t one iota of reference to the mermaid.min.js script. Time for an equivalent form of print statement debugging. Adding TESTING_FOOTER right after the script should do the trick. And its absence is deafening. Some debugging, some tea, and a last-ditch effort to look for any bugs in the theme that would cause this, and I see it.\nI want you to know that I could have deleted all of this. I could have. Do you remember that foreshadowing about failing to find a suitable place for things?\n2022-06-24 00:38:05 EDT in blog on  main [⇡!?] ➜ tree layouts layouts └── shortcodes ├── mermaid.html └── partials ├── comments.html └── extended_footer.html Yes, I failed to pay attention and created layouts/shortcodes/partials instead of layouts/partials. The first resource was correct, and I have it working.\ngraph LR A{do you need a panflute?} --\u003e|yes| B[no you don't] B --\u003e C[no panflute] A --\u003e|no| C That\u0026rsquo;s great. Seriously. I\u0026rsquo;m happy now and tired and a little thirsty. This should take the typical person a few minutes to get this working.\ntl;dr First, create a shortcode in layouts/shortcodes/mermaid.html. I added some extra sugar here so that I can center it and make it look just right.\n\u0026lt;div class=\u0026#34;mermaid\u0026#34; style=\u0026#34;text-align: {{ .Get 0 }}\u0026#34;\u0026gt; {{.Inner}} \u0026lt;/div\u0026gt; Then update layouts/partials/extended_footer.html to call the script that will look for the mermaid class and render it. If the file doesn\u0026rsquo;t already exist, you can create it or copy the example from themes/layouts/partials/extended_footer.html (which is a file that only contains a comment saying that you should create the file).\n{{ if (.Params.mermaid) }} \u0026lt;!-- MermaidJS support --\u0026gt; \u0026lt;script async src=\u0026#34;https://unpkg.com/mermaid/dist/mermaid.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; {{ end }} This is wrapped in a check for frontmatter on the page that lets you choose which pages get that loaded. Generally a good idea to only load on pages that need it. Saves a bit of loading time and data transfer. We\u0026rsquo;re doing this all to have a zippy site, right? So the last setup bit is to update the page frontmatter.\ntitle: \u0026#34;Mermaid + Hugo\u0026#34; author: \u0026#34;Ben\u0026#34; tags: - mermaid mermaid: true That\u0026rsquo;s it. You can use it as so:\n```mermaid graph LR A{do you need a panflute?} --\u0026gt;|yes| B[no you don\u0026#39;t] B --\u0026gt; C[no panflute] A --\u0026gt;|no| C ``` \u0026lt;div class=\u0026#34;mermaid\u0026#34; style=\u0026#34;text-align: center\u0026#34;\u0026gt; graph LR A{do you need a panflute?} --\u0026gt;|yes| B[no you don\u0026#39;t] B --\u0026gt; C[no panflute] A --\u0026gt;|no| C \u0026lt;/div\u0026gt; ","permalink":"https://asphaltbuffet.com/posts/2022/06/mermaid-hugo/","summary":"\u003cp\u003eI learned about \u003ca href=\"https://mermaid-js.github.io/mermaid/\"\u003eMermaid\u003c/a\u003e in 2021 while trying to find a way to better see changes in diagrams at work. Being able to look through a revision history of images was tolerable but only slightly better than the proverbial needle and the unexplained presence of a nearby haystack.\u003c/p\u003e\n\u003cp\u003eBut Mermaid resolves my complaints by rendering text/code into fairly nice-looking diagrams and visualizations. Some are too plain for my liking (looking at you, state diagram), but they convey information graphically. That\u0026rsquo;s all I\u0026rsquo;m really looking to get here.\u003c/p\u003e","title":"Mermaid + Hugo"},{"content":"Time for a new project.\nThe idea of an application that would help me track all my stuff has been rattling around in the noggin for quite a while. At one point, I\u0026rsquo;d hoped that maybe I could get Google Assistant to give some verbal means to tell me where I put that #2 Phillips screwdriver. A couple days of poking at IFTTT later\u0026hellip; idea abandoned.\nBut now, it came back (I still don\u0026rsquo;t know where that screwdriver is) and my thought is this would be a great way to expand my knowledge/practice of go, and databases, and REST and APIs and\u0026hellip; well, there\u0026rsquo;s a lot of things I can poke at here.\nFirst Steps Rather than jumping right into the code, I brought up the old daily journal app (see Obsidian) and created a new collection (see Bullet Journaling). For the really wild part, I stopped with code once I had the basic core repo set up and building. Did that take half a day? Yes. Did I stop immediately without going any further? Mostly. I count this as a huge success though. Figuring out a better name after I set up the repo and now I need to do some conversion\u0026hellip; well, it was 90% success at least.\nDesign So, back in my journal, I created a high-level summary of the project and then a page to start listing out my subcommands. It feels a bit like code, but the main benefit is that I\u0026rsquo;m rounding out what I want this thing to look like when using it. So far, very good results. I have iterated a few times on how the command structure looks and had to make some decisions about what kind of information I would want to include. There are notes on first deliverable functionality, ideas for the future, and it\u0026rsquo;s saving a lot of time.\nAn interesting discovery after doing some of this work is that it highlights the usefulness of a flowchart or state diagram of some sort. A means to visualize where application flow would happen and help define test cases and expectations. It also feeds into user documentation nicely too.\nOutput Currently, the document looks like this:\n--- date created: Sunday, June 19th, 2022 12:09:41 pm date modified: Wednesday, June 22nd, 2022 12:53:45 am title: subcommands --- # subcommands %%TODO: rename ‘name’ to ‘thing’%% ## `store [name] in [location]` - adds item \u0026#39;name\u0026#39; into storage \u0026#39;location\u0026#39; ## `use [name] from [location]` - pull item \u0026#39;name\u0026#39; out of storage \u0026#39;location\u0026#39; temporarily and track in ephemeral place while in use. - Intended to be relevant when calling the \u0026#39;cleanup\u0026#39; subcommand. ## `find [name]` - return list of storage locations that contain an item called \u0026#39;name\u0026#39; ## `rename [name]-[id] to [name]` - change the name of specific item - id would use a unique (but easy to input) identifier in the event multiple things have same name - TODO: should each item have a unique name? - TODO: should application tell user what the unique name is when they add something to tracking? ## `cleanup` - Lists of all items that have been ‘used’ but not `put-away` - include storage locations to assist in returning them to right place ## `put-away [name] (in \u0026lt;location\u0026gt;)` - remove item from ephemeral storage - adding a different location than previously recorded would be equivalent to \u0026#39;put-away\u0026#39; and \u0026#39;move\u0026#39; commands ## `move [name] from [location] to [location]` - change storage location for that item ## `remove [name] from [location]` - stop tracking specified item - TODO: does this permanently delete the item or just flag as removed? ## `borrow [name] from [person]` - add item to a special location for borrowed items - TODO: ability to add item to a storage location? - TODO: is this something that should be tracked with location like other items but a flag to indicate borrowed? ## `lend [name] to [person]` - take out of storage location (like use) - set flag that it is now loaned - TODO: ability to set return date? - TODO: autocomplete people from history? - TODO: add contact info for a person? ## `return [name]` - functionally very similar to put away - removes borrow/loaned flags - TODO: add ability to change location here too? ## `organize` - Group items (like with like) and generate report that highlights discrepancies. - TODO: may need some metadata to help determine which things are “alike” ## `places` - list out all storage locations ## `inventory` - Generate a list of all tracked items, locations, quantities, and status (loaned/borrowed) ","permalink":"https://asphaltbuffet.com/posts/2022/06/designing-a-side-project/","summary":"Time for a new project. Design before development; new territory for a non-work effort\u0026hellip;","title":"Designing a Side Project: Wherehouse"},{"content":"While I’ve blogged before, there is a need to have a common place to put development updates, tricks learned, and simply to show off when I think I’m being particularly clever.\nSo exists this place to serve that purpose.\n","permalink":"https://asphaltbuffet.com/posts/2022/06/beginnings/","summary":"First post and all that\u0026hellip;","title":"Beginnings"},{"content":"tl;dr I am Ben, and I make things.\nCredits Icon/Logo: Ary Prasetyo hugo-notice: Nicolas Martignoni ","permalink":"https://asphaltbuffet.com/about/","summary":"\u003ch2 id=\"tldr\"\u003etl;dr\u003c/h2\u003e\n\u003cp\u003eI am Ben, and I make things.\u003c/p\u003e\n\u003ch2 id=\"credits\"\u003eCredits\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eIcon/Logo: \u003ca href=\"https://thenounproject.com/arypst/\"\u003eAry Prasetyo\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003ehugo-notice: \u003ca href=\"https://github.com/martignoni\"\u003eNicolas Martignoni\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"About"}]