Repository navigation
Expand file tree
/
Copy pathindex.xml
More file actions
1599 lines (1599 loc) · 276 KB
/
Copy pathindex.xml
File metadata and controls
1599 lines (1599 loc) · 276 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>asphaltbuffet dev blog – asphaltbuffet dev blog</title><link>https://asphaltbuffet.com/</link><description>Recent content on asphaltbuffet dev blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><atom:link href="https://asphaltbuffet.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Automated Fixes for My Failures</title><link>https://asphaltbuffet.com/blog/jj-background-fetch/</link><pubDate>Tue, 19 May 2026 20:29:52 -0400</pubDate><guid>https://asphaltbuffet.com/blog/jj-background-fetch/</guid><description>
<p>I constantly open a repo in my terminal and start making changes and then rebase at some point when I realize that I never ran <code>jj git fetch</code> and now I have divergent changes, conflicts, etc. At some point a reasonable person stops repeating the action because they learn from their mistakes. To me, it seems like a decent reason to automate the problem away.</p>
<p>I might as well do some overengineering it while I&rsquo;m at it.</p>
<p>The concept was fairly simple: run <code>jj git fetch --all-remotes</code> in every jujutsu repo under <code>~/dev/</code> on a schedule, without me involved. A systemd user timer felt like the right shape — it runs independently of any shell session, survives reboots, and integrates with system logging out of the box. The implementation ended up being a justafiable excuse to use <code>pkgs.writeShellApplication</code>, which I like using whenever I can (I don&rsquo;t know why). It&rsquo;s good tho. I&rsquo;m going to tell you about it.</p>
<h2>The Timer Is the Easy Part<span class="hx:absolute hx:-mt-20" id="the-timer-is-the-easy-part"></span>
<a href="#the-timer-is-the-easy-part" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>systemd user timers in home-manager are not bad. Two blocks in <code>home/modules/jj/default.nix</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">systemd</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">jj-git-fetch</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">Unit</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">Description</span> <span class="o">=</span> <span class="s2">&#34;Fetch all jj repos under ~/dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">After</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;network-online.target&#34;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"> <span class="n">Service</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">Type</span> <span class="o">=</span> <span class="s2">&#34;oneshot&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">ExecStart</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">${</span><span class="n">fetchScript</span><span class="si">}</span><span class="s2">/bin/jj-git-fetch-all&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">systemd</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">timers</span><span class="o">.</span><span class="n">jj-git-fetch</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">Unit</span><span class="o">.</span><span class="n">Description</span> <span class="o">=</span> <span class="s2">&#34;Periodic jj git fetch for all repos under ~/dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">Timer</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">OnBootSec</span> <span class="o">=</span> <span class="s2">&#34;5m&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">OnUnitActiveSec</span> <span class="o">=</span> <span class="s2">&#34;15m&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">Persistent</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"> <span class="n">Install</span><span class="o">.</span><span class="n">WantedBy</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;timers.target&#34;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p><code>OnBootSec = &quot;5m&quot;</code> gives the network a moment to exist before trying to do something. <code>OnUnitActiveSec = &quot;15m&quot;</code> fires every fifteen minutes after that. <code>Persistent = true</code> is probably not absolutely necessary in this case — if the laptop was asleep at the scheduled time, the timer will catch up on next boot (since this runs every 15 minutes, it&rsquo;s not really saving us much here).</p>
<p><code>fetchScript</code> is a <code>let</code> binding pointing at the shell script package, which we&rsquo;ll get to. <em>Spoilers&hellip;</em></p>
<h2>Look at my package!<span class="hx:absolute hx:-mt-20" id="look-at-my-package"></span>
<a href="#look-at-my-package" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>So, <a href="https://nixos.org/manual/nixpkgs/stable/#trivial-builder-writeShellScript"target="_blank" rel="noopener">writeShellScript</a> is a file, but <a href="https://nixos.org/manual/nixpkgs/stable/#trivial-builder-writeShellApplication"target="_blank" rel="noopener">writeShellApplication</a> is a package. Here&rsquo;s where things get interesting. Nix gives you two primitives for packaging shell scripts:</p>
<p><code>pkgs.writeShellScript</code> produces a single derivation containing a script file. That&rsquo;s it. You get a path you can reference. You&rsquo;re responsible for making sure every tool the script calls is available at runtime, either by hard-coding store paths or hoping the environment has them. There&rsquo;s no build-time validation. If the script has a syntax error or calls a tool that isn&rsquo;t in <code>PATH</code>, you find out when the script fails at runtime. Change your path later and forget about the script? Yeah, you get an error whenever that script runs&hellip; yay!</p>
<p><code>pkgs.writeShellApplication</code> produces a proper package with a <code>bin/</code> subdirectory. When you build it, Nix automatically:</p>
<ol>
<li>Prepends <code>set -euo pipefail</code> — so the script fails fast on errors, unset variables, and broken pipes without you remembering to do that</li>
<li>Runs <code>shellcheck</code> at <em>build time</em> — if your script has a bug, <code>nix build</code> fails, not the systemd unit three days later</li>
<li>Manages <code>runtimeInputs</code> — tools listed there are on <code>$PATH</code> inside the script, resolved to their store paths automatically, no hard-coding needed</li>
</ol>
<p>The <code>runtimeInputs</code> pattern is quite pleasant. Instead of writing <code>${pkgs.fd}/bin/fd</code> and <code>${pkgs.jujutsu}/bin/jj</code> inline, you declare them:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="c1"># home/modules/jj/fetch-script.nix</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="n">pkgs</span><span class="p">}:</span>
</span></span><span class="line"><span class="cl"><span class="n">pkgs</span><span class="o">.</span><span class="n">writeShellApplication</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;jj-git-fetch-all&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">runtimeInputs</span> <span class="o">=</span> <span class="k">with</span> <span class="n">pkgs</span><span class="p">;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="n">curl</span>
</span></span><span class="line"><span class="cl"> <span class="n">fd</span>
</span></span><span class="line"><span class="cl"> <span class="n">systemd</span>
</span></span><span class="line"><span class="cl"> <span class="n">jujutsu</span>
</span></span><span class="line"><span class="cl"> <span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="n">text</span> <span class="o">=</span> <span class="nb">builtins</span><span class="o">.</span><span class="n">readFile</span> <span class="sr">./jj-git-fetch.sh</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>And then the script itself just calls <code>fd</code>, <code>jj</code>, <code>curl</code>, <code>systemd-cat</code> — short names, no store paths, the way you&rsquo;d write a script for any system (and makes importing scripts really seamless). The derivation takes care of resolving them.</p>
<p>I split the package definition into its own file (<code>fetch-script.nix</code>) and referenced it from <code>default.nix</code> via <code>import ./fetch-script.nix {inherit pkgs;}</code>. Simple, not <code>callPackage</code> — the package only needs <code>pkgs</code>, so pulling in the full overlay machinery would be overkill.</p>
<h2>Finding my point (and some repos)<span class="hx:absolute hx:-mt-20" id="finding-my-point-and-some-repos"></span>
<a href="#finding-my-point-and-some-repos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The script uses <code>fd</code> to discover the appropriate repos by looking for <code>.jj/</code> directories:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">repos</span><span class="o">=</span><span class="k">$(</span>fd --type d --hidden --no-ignore <span class="s1">&#39;^\.jj$&#39;</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/dev&#34;</span> 2&gt;/dev/null <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r jjdir<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> dirname <span class="s2">&#34;</span><span class="nv">$jjdir</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p><code>fd '^\.jj$'</code> finds directories named exactly <code>.jj</code> (regex anchored at both ends), anywhere under <code>~/dev/</code>, including nested paths. <code>dirname</code> walks each result up one level to get the repo root. This handles repos at any depth without knowing the naming convention in advance — a flat <code>~/dev/foo/</code>, a grouped <code>~/dev/work/client/project/</code>, whatever.</p>
<p>The alternative would be a glob like <code>~/dev/*/</code>, which only finds repos one level deep and relies on a consistent directory structure.</p>
<p>I could probably collapse the whole discovery-and-fetch loop into a single <code>fd</code> invocation using <code>-x</code> to run <code>jj git fetch</code> once per result — something like <code>fd --type d --hidden --no-ignore '^\.jj$' &quot;$HOME/dev&quot; -x jj --repository '{//}' git fetch --all-remotes</code>. Cleaner, no <code>while</code> loop, no intermediate variable. I didn&rsquo;t feel like debugging <code>fd</code>&rsquo;s placeholder syntax at the time, and the loop works fine. I&rsquo;m not really doing this to learn something today.</p>
<h2>The Network Check: Good Enough<span class="hx:absolute hx:-mt-20" id="the-network-check-good-enough"></span>
<a href="#the-network-check-good-enough" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before doing anything, the script checks whether GitHub is reachable:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> ! curl --silent --head --fail --max-time <span class="m">5</span> https://github.com &gt; /dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;jj-git-fetch: network unavailable or github unreachable, skipping&#34;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> systemd-cat -t jj-git-fetch -p info
</span></span><span class="line"><span class="cl"> <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Note <code>exit 0</code> — the unit exits clean when the network is down. That&rsquo;s intentional. A failed unit shows up as failed in <code>systemctl --user status jj-git-fetch</code>, and I don&rsquo;t want to stare at red text every morning just because the service fired before the VPN connected. If GitHub is unreachable, the right move is to quietly skip and try again in fifteen minutes.</p>
<p>The check is specifically for GitHub and not a generic connectivity check (pinging 8.8.8.8, say) because fetching is only useful if the remote is reachable. A working LAN connection with no internet access would pass a generic ping and then immediately fail on every <code>jj git fetch</code>. This way the check and the work are testing the same thing.</p>
<h2>Per-Repo Logging and Failure Tracking<span class="hx:absolute hx:-mt-20" id="per-repo-logging-and-failure-tracking"></span>
<a href="#per-repo-logging-and-failure-tracking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">failed</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> -r repo<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;jj-git-fetch: fetching </span><span class="nv">$repo</span><span class="s2">&#34;</span> <span class="p">|</span> systemd-cat -t jj-git-fetch -p info
</span></span><span class="line"><span class="cl"> <span class="k">if</span> ! jj --repository <span class="s2">&#34;</span><span class="nv">$repo</span><span class="s2">&#34;</span> git fetch --all-remotes 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> systemd-cat -t jj-git-fetch -p info<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;jj-git-fetch: FAILED for </span><span class="nv">$repo</span><span class="s2">&#34;</span> <span class="p">|</span> systemd-cat -t jj-git-fetch -p err
</span></span><span class="line"><span class="cl"> <span class="nv">failed</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"> <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span> <span class="o">&lt;&lt;&lt;</span> <span class="s2">&#34;</span><span class="nv">$repos</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">exit</span> <span class="s2">&#34;</span><span class="nv">$failed</span><span class="s2">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Each repo gets its own log lines at <code>info</code> priority. Failures are logged at <code>err</code> priority, which means they show up more prominently in <code>journalctl --user -t jj-git-fetch -p err</code>. The script continues through all repos regardless — a failure in one shouldn&rsquo;t skip the others. If any repo failed, the overall exit code is nonzero, which marks the systemd unit as failed and makes the problem visible in <code>systemctl --user status</code>. Yes, this could get noisy. No, I don&rsquo;t care right now.</p>
<p><code>journalctl --user -t jj-git-fetch</code> gives a clean view of just this service&rsquo;s output, timestamped, across all runs.</p>
<h2>The 1Password Detour<span class="hx:absolute hx:-mt-20" id="the-1password-detour"></span>
<a href="#the-1password-detour" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In an earlier attempt, I tried a pre-check: <code>op whoami</code>, to see if 1Password was authenticated before attempting fetches. The idea was that I use SSH keys managed by 1Password&rsquo;s SSH agent, so if 1Password wasn&rsquo;t running, the fetch would fail anyway.</p>
<p>Not quite the right approach though: <code>op whoami</code> returns nonzero unless the 1Password desktop app is fully signed in, but even when executing <code>jj git fetch</code> manually was fine, the check failed. I just got rid of the check and figured if a fetch fails because auth isn&rsquo;t available, it can log the failure per-repo and move on. That&rsquo;s a better signal than a blanket early exit based on a condition that wasn&rsquo;t actually relevant.</p>
<h2>It Works<span class="hx:absolute hx:-mt-20" id="it-works"></span>
<a href="#it-works" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ systemctl --user list-timers --all
</span></span><span class="line"><span class="cl">NEXT LEFT LAST PASSED UNIT
</span></span><span class="line"><span class="cl">Mon 2026-05-19 20:02:47 EDT 13min Mon 2026-05-19 19:47:47 EDT 1min ago jj-git-fetch.timer</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ journalctl --user -t jj-git-fetch
</span></span><span class="line"><span class="cl">May <span class="m">19</span> 19:47:47 wendigo jj-git-fetch<span class="o">[</span>12053<span class="o">]</span>: jj-git-fetch: fetching /home/grue/dev/blog
</span></span><span class="line"><span class="cl">May <span class="m">19</span> 19:47:49 wendigo jj-git-fetch<span class="o">[</span>12053<span class="o">]</span>: jj-git-fetch: fetching /home/grue/dev/nix-config
</span></span><span class="line"><span class="cl">May <span class="m">19</span> 19:47:52 wendigo jj-git-fetch<span class="o">[</span>12053<span class="o">]</span>: jj-git-fetch: fetching /home/grue/dev/wherehouse</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Repos fetched, no intervention required, logs readable via <code>journalctl</code>. The timer fires, does its thing, and disappears. Next run in thirteen minutes.</p></description></item><item><title>Rooting around in .local/bin</title><link>https://asphaltbuffet.com/blog/nixos-local-bin-path/</link><pubDate>Tue, 12 May 2026 00:00:00 -0400</pubDate><guid>https://asphaltbuffet.com/blog/nixos-local-bin-path/</guid><description>
<p>Claude Code&rsquo;s <code>/doctor</code> command told me <code>~/.local/bin</code> wasn&rsquo;t in my <code>PATH</code>. That&rsquo;s where it installs its native binary. Simple fix, right?</p>
<p>The symptom was easy to reproduce: open a new shell, run <code>echo $PATH</code>, and <code>~/.local/bin</code> is nowhere in there. Claude Code was installed fine; it just couldn&rsquo;t find itself.</p>
<p>I&rsquo;d just make sure that the path was configured in my <code>.zshrc</code> and all would be good. Since it&rsquo;s NixOS, the obvious fix from home-manager was with <code>home.sessionPath</code>. One line in <code>home/roles/base.nix</code>, rebuild, done:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">home</span><span class="o">.</span><span class="n">sessionPath</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;$HOME/.local/bin&#34;</span> <span class="p">];</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Tried it. Rebuilt. Ran <code>exec zsh</code>. Opened Claude Code. Ran <code>/doctor</code>. Still broken.</p>
<h2>In Which I Learn How NixOS Boots a Shell<span class="hx:absolute hx:-mt-20" id="in-which-i-learn-how-nixos-boots-a-shell"></span>
<a href="#in-which-i-learn-how-nixos-boots-a-shell" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Ok, so NixOS generates a script called <code>set-environment</code> at build time. You can find the one currently running your shell at something like <code>/nix/store/&lt;hash&gt;-set-environment</code>. It gets sourced via <code>/etc/zshenv</code> on every shell start, and it does this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.nix-profile/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>A hard <code>export PATH=</code>. Not <code>PATH=&quot;something:$PATH&quot;</code>. A fresh path every time. Usually a good thing to prevent cruft building up, but now important stuff was missing.</p>
<p>home-manager&rsquo;s <code>hm-session-vars.sh</code> — which <strong>does</strong> correctly add <code>~/.local/bin</code> — runs <em>before</em> <code>/etc/zshenv</code>. So it sets the path, and then NixOS immediately resets it. The entry is right there in <code>hm-session-vars.sh</code> if you go looking:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin</span><span class="si">${</span><span class="nv">PATH</span><span class="p">:+:</span><span class="si">}</span><span class="nv">$PATH</span><span class="s2">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Was this documented anywhere obvious? I mean, <em>probably</em>, but I didn&rsquo;t do a lot of investigation.</p>
<h2>The Actual Fix<span class="hx:absolute hx:-mt-20" id="the-actual-fix"></span>
<a href="#the-actual-fix" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Searching online got me to a better option pretty quickly: NixOS has <a href="https://search.nixos.org/options?query=localBinInPath"target="_blank" rel="noopener"><code>environment.localBinInPath</code></a>. Setting it to <code>true</code> injects <code>~/.local/bin</code> directly into the <code>set-environment</code> script, so it survives the reset.</p>
<p>One line in <code>nixos/profiles/base.nix</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">environment</span><span class="o">.</span><span class="n">localBinInPath</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Rebuild. New shell. <code>~/.local/bin</code> is in <code>$PATH</code>. Claude Code&rsquo;s <code>/doctor</code> command goes quiet. Mission accomplished.</p>
<h2>Onions and Parfaits<span class="hx:absolute hx:-mt-20" id="onions-and-parfaits"></span>
<a href="#onions-and-parfaits" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There are layers here I didn&rsquo;t consider, and the lesson isn&rsquo;t subtle: on NixOS, if something isn&rsquo;t in the <code>PATH</code> that <code>set-environment</code> builds, it isn&rsquo;t in your <code>PATH</code>. Full stop. home-manager&rsquo;s <code>sessionPath</code>, <code>sessionVariables</code>, anything in <code>hm-session-vars.sh</code> — all of it happens <em>before</em> the hard reset. Anything you want to survive has to come from the NixOS level.</p>
<p>Was <code>home.sessionPath</code> the wrong tool? Yes. Would I have tried it first anyway? Also, yes.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="nv">$PATH</span> <span class="p">|</span> tr <span class="s1">&#39;:&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> grep <span class="nb">local</span>
</span></span><span class="line"><span class="cl">/home/grue/.local/bin</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Good enough.</p></description></item><item><title>Two Hours of NixOS Config I Didn't Need</title><link>https://asphaltbuffet.com/blog/nixos-undocked/</link><pubDate>Mon, 04 May 2026 22:34:26 -0400</pubDate><guid>https://asphaltbuffet.com/blog/nixos-undocked/</guid><description>
<p>Docks are basically plug-and-play, right? So if the monitor isn&rsquo;t working, there must be a setting that&rsquo;s off somewhere. This will be easy.</p>
<p><em>(narrator voice: it would not, in fact, be easy)</em></p>
<p>The 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&rsquo;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.</p>
<p>The correct diagnosis would have taken five seconds. I did not find it in five seconds.</p>
<h2>The Reasonable Wrong Answer<span class="hx:absolute hx:-mt-20" id="the-reasonable-wrong-answer"></span>
<a href="#the-reasonable-wrong-answer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>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 <code>bolt</code> daemon handles this, and the security level on most systems defaults to <code>user</code> or <code>secure</code>, which means unrecognized devices are blocked until approved. USB peripherals (keyboard, mouse) work because they&rsquo;re enumerated through the USB controller, not the Thunderbolt controller. DisplayPort Alt Mode video goes through Thunderbolt. If <code>bolt</code> isn&rsquo;t running, or if the device hasn&rsquo;t been authorized, it would look exactly like what I saw.</p>
<p>Turns out, that reasoning is entirely correct. For a Thunderbolt dock.</p>
<p>My T14-specific NixOS profile (<code>nixos/profiles/laptop/t14.nix</code>) looked like this before I touched it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span><span class="n">inputs</span><span class="o">,</span> <span class="o">...</span><span class="p">}:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">imports</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="sr">./.</span>
</span></span><span class="line"><span class="cl"> <span class="sr">./power.nix</span>
</span></span><span class="line"><span class="cl"> <span class="n">inputs</span><span class="o">.</span><span class="n">nixos-hardware</span><span class="o">.</span><span class="n">nixosModules</span><span class="o">.</span><span class="n">lenovo-thinkpad-t14-intel-gen1</span>
</span></span><span class="line"><span class="cl"> <span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Sensible. Minimal. I made it worse:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">services</span><span class="o">.</span><span class="n">hardware</span><span class="o">.</span><span class="n">bolt</span><span class="o">.</span><span class="n">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">boot</span><span class="o">.</span><span class="n">kernelParams</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;i915.enable_dp_mst=1&#34;</span><span class="p">];</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>The <code>bolt</code> line enables the Thunderbolt daemon and, because KDE Plasma ships <code>plasma-thunderbolt</code>, you get a little system tray widget to authorize devices. The <code>i915.enable_dp_mst=1</code> 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&rsquo;t hurt. (I was correct here, it didn&rsquo;t hurt. More precisely, the only thing it accomplished was &ldquo;not hurting&rdquo;).</p>
<p>Build was clean. <code>nh os build</code> added <code>bolt 0.9.8</code> and <code>plasma-thunderbolt 6.6.4</code>. After <code>just switch</code> and a reboot:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ systemctl status bolt
</span></span><span class="line"><span class="cl">● bolt.service - Thunderbolt system service
</span></span><span class="line"><span class="cl"> Active: active <span class="o">(</span>running<span class="o">)</span>
</span></span><span class="line"><span class="cl"> ...
</span></span><span class="line"><span class="cl">May <span class="m">05</span> 17:59:24 wendigo boltd<span class="o">[</span>5619<span class="o">]</span>: udev: found <span class="m">0</span> domain
</span></span><span class="line"><span class="cl">May <span class="m">05</span> 17:59:24 wendigo boltd<span class="o">[</span>5619<span class="o">]</span>: udev: enumerating devices
</span></span><span class="line"><span class="cl">May <span class="m">05</span> 17:59:44 wendigo boltd<span class="o">[</span>5619<span class="o">]</span>: power: setting force_power to OFF</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ boltctl list
</span></span><span class="line"><span class="cl"><span class="c1"># (no output)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p><code>udev: found 0 domain</code>. The kernel isn&rsquo;t exposing any Thunderbolt domains at all. <code>boltctl list</code> is silent. The dock isn&rsquo;t showing up because, from the kernel&rsquo;s perspective, there is no Thunderbolt controller talking to anything. This wasn&rsquo;t a configuration problem; the hardware wasn&rsquo;t what I assumed it was.</p>
<p>I reverted both changes and went to look at what was actually there.</p>
<h2>lsusb. Obviously. Run lsusb &ldquo;First&rdquo;<span class="hx:absolute hx:-mt-20" id="lsusb-obviously-run-lsusb-first"></span>
<a href="#lsusb-obviously-run-lsusb-first" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The diagnostic I should have started with:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ sudo dmesg <span class="p">|</span> rg -i <span class="s2">&#34;thunderbolt|usb4|tbt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> 0.015200<span class="o">]</span> ACPI: SSDT 0x00000000A652D000 001F15 <span class="o">(</span>v02 LENOVO WHL_Tbt_ ...<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> 6.155008<span class="o">]</span> ACPI: bus <span class="nb">type</span> thunderbolt registered</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Only ACPI table entries — the firmware knows Thunderbolt exists as a bus type, but no actual controller was probed, no domain registered. Then:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ sudo dmesg <span class="p">|</span> rg -i <span class="s2">&#34;i915|drm|displaylink&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> 4.119714<span class="o">]</span> usb 2-2.2: New USB device found, <span class="nv">idVendor</span><span class="o">=</span>17e9, <span class="nv">idProduct</span><span class="o">=</span>6015, <span class="nv">bcdDevice</span><span class="o">=</span>31.04
</span></span><span class="line"><span class="cl"><span class="o">[</span> 4.119719<span class="o">]</span> usb 2-2.2: Manufacturer: DisplayLink</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Hmm&hellip; <code>Manufacturer: DisplayLink</code>. Let&rsquo;s just dig in there a little deeper:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ lsusb
</span></span><span class="line"><span class="cl">Bus <span class="m">002</span> Device 003: ID 17e9:6015 DisplayLink ThinkPad Hybrid USB-C with USB-A Dock</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>There&rsquo;s the dock&rsquo;s full product name: <strong>ThinkPad Hybrid USB-C with USB-A Dock</strong>. The word &ldquo;Hybrid&rdquo; is doing a lot of work there, and I&rsquo;d completely missed it. Lenovo designed this dock to connect to hosts via either USB-C <em>or</em> USB-A — because USB-A has no DisplayPort Alt Mode capability at all, they couldn&rsquo;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&rsquo;t care whether the host connected via USB-C or USB-A; it just needs USB bandwidth.</p>
<p>The architecture difference matters enormously for driver requirements:</p>
<table>
<thead>
<tr>
<th>Architecture</th>
<th>How video works</th>
<th>Driver required</th>
</tr>
</thead>
<tbody>
<tr>
<td>DP Alt Mode</td>
<td>GPU drives display directly via repurposed USB-C lanes</td>
<td>None (native i915)</td>
</tr>
<tr>
<td>Thunderbolt 3/4</td>
<td>DP signal tunneled through the TB controller</td>
<td>None (bolt for auth)</td>
</tr>
<tr>
<td>DisplayLink</td>
<td>CPU compresses framebuffer, sent over USB to the dock chip</td>
<td>Proprietary kernel module + userspace daemon</td>
</tr>
</tbody>
</table>
<p>A native DP Alt Mode dock just works on Linux. A Thunderbolt dock needs <code>bolt</code> for authorization but otherwise just works. A DisplayLink dock needs a proprietary kernel module (<code>evdi</code>) and a closed-source userspace daemon from Synaptics. I had the wrong dock for the thing I wanted — zero-config Linux compatibility.</p>
<p>Was this clear from the product name? In retrospect, absolutely. Would I have noticed it without the <code>lsusb</code> output slapping me in the face? Shmaybe?</p>
<h2>In Which I Briefly Try to Make DisplayLink Work<span class="hx:absolute hx:-mt-20" id="in-which-i-briefly-try-to-make-displaylink-work"></span>
<a href="#in-which-i-briefly-try-to-make-displaylink-work" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Look, I knew this was unlikely to go well &ndash; but I already had several browser tabs open.</p>
<p>The <a href="https://www.synaptics.com/products/displaylink-usb-graphics-software-ubuntu-62"target="_blank" rel="noopener">DisplayLink Linux package</a> lives in nixpkgs under <code>pkgs/os-specific/linux/displaylink/default.nix</code>. It uses <code>requireFile</code> because Synaptics&rsquo;s license prohibits redistribution — you have to manually download the installer and <code>nix-store --add-fixed</code> it yourself. The NixOS module lives at <code>nixos/modules/hardware/video/displaylink.nix</code>. I should have read it before trying anything. I skipped reading it before trying anything.</p>
<p>Easy enough to activate: <code>services.xserver.videoDrivers = [&quot;displaylink&quot;]</code>. The module keys off that list to decide whether to load <code>evdi</code> and start the <code>dlm.service</code> daemon.</p>
<p>After finding the correct option name, I actually opened the module source. Here&rsquo;s the relevant piece:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="k">let</span>
</span></span><span class="line"><span class="cl"> <span class="n">enabled</span> <span class="o">=</span> <span class="n">lib</span><span class="o">.</span><span class="n">elem</span> <span class="s2">&#34;displaylink&#34;</span> <span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">xserver</span><span class="o">.</span><span class="n">videoDrivers</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">in</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">config</span> <span class="o">=</span> <span class="n">lib</span><span class="o">.</span><span class="n">mkIf</span> <span class="n">enabled</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">boot</span><span class="o">.</span><span class="n">extraModulePackages</span> <span class="o">=</span> <span class="p">[</span> <span class="n">evdi</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="n">boot</span><span class="o">.</span><span class="n">kernelModules</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;evdi&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="n">services</span><span class="o">.</span><span class="n">xserver</span><span class="o">.</span><span class="n">externallyConfiguredDrivers</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;displaylink&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="n">environment</span><span class="o">.</span><span class="n">etc</span><span class="o">.</span><span class="s2">&#34;X11/xorg.conf.d/40-displaylink.conf&#34;</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1"> Section &#34;OutputClass&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1"> Identifier &#34;DisplayLink&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1"> MatchDriver &#34;evdi&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1"> Driver &#34;modesetting&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1"> ...
</span></span></span><span class="line"><span class="cl"><span class="s1"> EndSection
</span></span></span><span class="line"><span class="cl"><span class="s1"> &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">services</span><span class="o">.</span><span class="n">xserver</span><span class="o">.</span><span class="n">displayManager</span><span class="o">.</span><span class="n">sessionCommands</span> <span class="o">=</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1"> </span><span class="si">${</span><span class="n">lib</span><span class="o">.</span><span class="n">getBin</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">xrandr</span><span class="si">}</span><span class="s1">/bin/xrandr --setprovideroutputsource 1 0
</span></span></span><span class="line"><span class="cl"><span class="s1"> &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="o">...</span>
</span></span><span class="line"><span class="cl"> <span class="n">systemd</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">dlm</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;DisplayLink Manager Service&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="o">...</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p><code>/etc/X11/xorg.conf.d/</code>. <code>externallyConfiguredDrivers</code>. <code>xrandr --setprovideroutputsource</code>. Every single piece of this module is X11 plumbing. The laptop profile defaults to Wayland. KDE Plasma 6 on Wayland ignores <code>xorg.conf.d</code> entirely, doesn&rsquo;t use <code>xrandr</code> for output management, and doesn&rsquo;t load the Xorg <code>modesetting</code> driver at all.</p>
<p>DisplayLink on Wayland is an unresolved upstream problem. Synaptics has been working on it, but it&rsquo;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.</p>
<p>Well&hellip; crap. The hardware problem may not be something with a software solution afterall. I&rsquo;m as shocked as you are.</p>
<h2>Actually Solving It<span class="hx:absolute hx:-mt-20" id="actually-solving-it"></span>
<a href="#actually-solving-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>My requirements aren&rsquo;t too complicated:</p>
<ol>
<li>HDMI 2.0 output (the TESmart KVM does 4:4:4 chroma at 4K@60Hz; HDMI 1.4 won&rsquo;t do it)</li>
<li>Gigabit Ethernet</li>
<li>65W+ Power Delivery (T14 and X1 Carbon charging)</li>
<li>USB-C host connection</li>
<li><strong>DP Alt Mode — explicitly not DisplayLink</strong> (see everything above)</li>
</ol>
<p>The easiest way to verify #5 before buying is <code>lsusb</code> 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.</p>
<p>The Plugable <a href="https://www.amazon.com/Plugable-USB-Multiport-Adapter-Ethernet/dp/B08NXV94QM"target="_blank" rel="noopener">USBC-7IN1E</a> landed at the top of the shortlist. Plugable explicitly documents <a href="https://kb.plugable.com/does-the-usbc-7in1e-support-hdmi-displays-at-4k-60hz"target="_blank" rel="noopener">4K@60Hz HDMI support</a> and maintains a <a href="https://kb.plugable.com/understanding-usb-c-alt-mode"target="_blank" rel="noopener">USB-C Alt Mode explainer</a> that makes the architecture clear. Around $40–45, driverless, and they explicitly list Linux compatibility. The <a href="https://www.amazon.com/Anker-PowerExpand-Adapter-Delivery-Ethernet/dp/B087QZVQJX"target="_blank" rel="noopener">Anker 555 (A8383)</a> is a credible alternative in the same price range, though Anker doesn&rsquo;t officially support Linux (it works in practice). The <a href="https://www.amazon.com/Targus-Multi-Function-DisplayPort-Docking-DOCK430USZ/dp/B07X441JKQ"target="_blank" rel="noopener">Targus DOCK430USZ</a> and refurbished <a href="https://www.amazon.com/Lenovo-ThinkPad-Universal-USB-C-Dock/dp/B09DNL4C9J"target="_blank" rel="noopener">Lenovo 40AY0090US</a> are both solid options if you want something closer to enterprise-grade, at roughly $60–100.</p>
<p>The Arch Linux forums have <a href="https://bbs.archlinux.org/viewtopic.php?id=259408"target="_blank" rel="noopener">a useful thread on T14 + USB-C + 4K</a> that confirms the chroma subsampling behavior and validates that native DP Alt Mode docks just work without touching the kernel or xorg config.</p>
<p>Note the naming pattern: the dock I <strong>don&rsquo;t</strong> want is the <em>Hybrid</em> USB-C with USB-A dock. The dock I <strong>do</strong> want is the <em>Universal</em> USB-C Dock, the <em>USB-C 7-in-1</em>, the straightforward naming that doesn&rsquo;t require a compatibility matrix. &ldquo;Hybrid&rdquo; means &ldquo;we needed it to work with USB-A hosts, so we put a DisplayLink chip inside.&rdquo; That&rsquo;s a real engineering constraint Lenovo solved reasonably well — it&rsquo;s just not compatible with what I&rsquo;m trying to do.</p>
<h2>What I Should Have Done<span class="hx:absolute hx:-mt-20" id="what-i-should-have-done"></span>
<a href="#what-i-should-have-done" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><code>lsusb</code>. That&rsquo;s it. Five seconds.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ lsusb <span class="p">|</span> grep -i lenovo
</span></span><span class="line"><span class="cl">Bus <span class="m">002</span> Device 003: ID 17e9:6015 DisplayLink ThinkPad Hybrid USB-C with USB-A Dock</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p><code>17e9</code> is DisplayLink. The word &ldquo;Hybrid&rdquo; is in the name. Everything else that followed — the Thunderbolt hypothesis, the <code>bolt</code> daemon, the speculative <code>i915.enable_dp_mst=1</code>, the X11-only module audit — all of it was downstream of not checking what hardware was actually present before starting to theorize.</p>
<p>The second thing I should have done: one change at a time. I added <code>bolt</code> and the kernel parameter together, which meant when the fix didn&rsquo;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.</p>
<p>The third thing: read the module source before enabling it. <code>nixos/modules/hardware/video/displaylink.nix</code> has <code>xrandr</code> in the <code>sessionCommands</code>. That&rsquo;s an immediate tell — <code>xrandr</code> doesn&rsquo;t manage Wayland outputs. I would have known in thirty seconds that this path was going nowhere.</p>
<p>Hardware problems shouldn&rsquo;t have software solutions. A DisplayLink dock on a Wayland system in 2026 is not a NixOS configuration gap; it&rsquo;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&rsquo;d be fine. Apparently I&rsquo;ve decided that I want the small dopamine hit from buying something shiny instead.</p>
<p><code>t14.nix</code> is back to its original five lines. Nothing changed. The new dock is in the mail.</p></description></item><item><title>Monitoring my HVAC</title><link>https://asphaltbuffet.com/blog/hvac-monitoring/</link><pubDate>Sun, 27 Nov 2022 00:00:00 +0000</pubDate><guid>https://asphaltbuffet.com/blog/hvac-monitoring/</guid><description>
<p>When I want to know <a href="https://sumzim.com/supply-vs-return-vents"target="_blank" rel="noopener">how well my HVAC system is working</a>, smart thermostats and ad-hoc checks don&rsquo;t get me as far as I&rsquo;d like. That means it&rsquo;s time to create an entire system to monitor that information and present it graphically. This is normal, right?</p>
<figure><img src="https://asphaltbuffet.com/blog/hvac-monitoring/in-situ.jpg"
alt="HVAC sensor installed in-situ">
</figure>
<h2>Requirements<span class="hx:absolute hx:-mt-20" id="requirements"></span>
<a href="#requirements" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Overall, I didn&rsquo;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&rsquo;s a better use of late-night chill time than playing a game anyway. Plus, spoilers&hellip; there are graphs. That broad overview gave me the following criteria:</p>
<ol>
<li>Don&rsquo;t buy much extra stuff. Keep it less than $20 spending.</li>
<li>Easy to set up and keep running.</li>
<li>The initial scale is one sensor. Don&rsquo;t optimize until later, if ever.</li>
</ol>
<h2>Choices, Choices, Choices<span class="hx:absolute hx:-mt-20" id="choices-choices-choices"></span>
<a href="#choices-choices-choices" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Selecting the brains<span class="hx:absolute hx:-mt-20" id="selecting-the-brains"></span>
<a href="#selecting-the-brains" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I have a box of microcontrollers in my project area. First, I was going to use a <a href="https://www.beagleboard.org/"target="_blank" rel="noopener">BeagleBoard</a> that I got ~10 years ago at the <a href="https://web.archive.org/web/20221101/https://events17.linuxfoundation.org/events/archive/2013/embedded-linux-conference"target="_blank" rel="noopener">Embedded Linux Conference</a>. However, I could not find a power adapter for it, so after 20 minutes of searching, I nixed that idea.</p>
<p>I considered using a <a href="https://www.raspberrypi.org/"target="_blank" rel="noopener">raspberry pi</a>, but even though I have a couple spares, I didn&rsquo;t want that level of overkill to get a couple temp readings sent to the cloud.</p>
<p>Oh, but the handful of <a href="https://docs.arduino.cc/retired/boards/arduino-ethernet-rev3-with-poe"target="_blank" rel="noopener">Arduino Ethernet boards that have PoE</a>? Yes, please. It takes care of my need to plug into power (sort of, we&rsquo;ll cover this later). 32k of memory should be fine (yeah, we&rsquo;ll touch on this later too). The temp probes that I have will work fine with this and require minimal breadboard work.</p>
<h3>Selecting the sensors<span class="hx:absolute hx:-mt-20" id="selecting-the-sensors"></span>
<a href="#selecting-the-sensors" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>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.</p>
<p>I also had two <a href="https://web.archive.org/web/20221101/https://datasheets.maximintegrated.com/en/ds/DS18B20.pdf"target="_blank" rel="noopener">DS18B20</a> 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.</p>
<h3>Selecting the backend tooling<span class="hx:absolute hx:-mt-20" id="selecting-the-backend-tooling"></span>
<a href="#selecting-the-backend-tooling" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I wanted to use <a href="https://mqtt.org/"target="_blank" rel="noopener">MQTT</a> for sensor communication so that I wasn&rsquo;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.</p>
<p>Database and visualization were chosen via similar criteria. I&rsquo;ve used <a href="https://grafana.com/"target="_blank" rel="noopener">Grafana</a> a lot recently to create dashboards, and there&rsquo;s a free cloud version that gets me through proof-of-concept. Same deal with <a href="https://www.timescale.com/"target="_blank" rel="noopener">Timescale</a>. 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&rsquo;m good for now with what I have.</p>
<h2>Sensor Development<span class="hx:absolute hx:-mt-20" id="sensor-development"></span>
<a href="#sensor-development" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Wiring the Parts Together<span class="hx:absolute hx:-mt-20" id="wiring-the-parts-together"></span>
<a href="#wiring-the-parts-together" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><figure><img src="https://asphaltbuffet.com/blog/hvac-monitoring/hotpants-breadboard.png"
alt="Sensor wired on breadboard">
</figure>
<p>The sensor hardware itself was planned to be as plug-and-play as possible. I <a href="https://create.arduino.cc/projecthub/karmette/basic-led-setup-for-beginners-0a124a"target="_blank" rel="noopener">added an LED</a> 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&rsquo;t even in the code I walk through below as it&rsquo;s there as a debug tool more than anything, but I may update that source code later if I decide to leave it in.</p>
<figure><img src="https://asphaltbuffet.com/blog/hvac-monitoring/blink.gif"
alt="LED blinking on the sensor board">
</figure>
<h3>Sensor Programming<span class="hx:absolute hx:-mt-20" id="sensor-programming"></span>
<a href="#sensor-programming" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>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.</p>
<p>Honestly, 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&rsquo;s not pretty. Warts will be highlighted as we go. Grab the popcorn and follow along on the journey!</p>
<p>Initially, I had planned to set up <a href="https://tinygo.org/"target="_blank" rel="noopener">TinyGo</a> 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.</p>
<p>I tackled the sensor programming in the following order:</p>
<ol>
<li>Get temp readings working</li>
<li>Get the network connection working</li>
<li>Get MQTT communication working</li>
<li>Send temp readings via MQTT</li>
</ol>
<h3>Reading in temps<span class="hx:absolute hx:-mt-20" id="reading-in-temps"></span>
<a href="#reading-in-temps" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Fortunately, there&rsquo;s already an Arduino library for the sensors I used. I only needed to include <code>OneWire.h</code> and <code>DallasTemperature.h</code> for the software to function. I found a reference on <a href="https://raspberryautomation.com/connect-multiple-ds18b20-temperature-sensors-to-a-raspberry-pi"target="_blank" rel="noopener">wiring up multiple sensors</a> that worked right away. In an hour or so, I had the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-arduino" data-lang="arduino"><span class="line"><span class="cl"><span class="c1">// OneWire - Version: Latest
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;OneWire.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// DallasTemperature - Version: Latest
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;DallasTemperature.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;SPI.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;Ethernet.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// hardware config
</span></span></span><span class="line"><span class="cl"><span class="cp">#define ONE_WIRE_BUS 2
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">OneWire</span> <span class="nf">oneWire</span><span class="p">(</span><span class="n">ONE_WIRE_BUS</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">DallasTemperature</span> <span class="nf">sensors</span><span class="p">(</span><span class="o">&amp;</span><span class="n">oneWire</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// take reading every 60s
</span></span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">long</span> <span class="n">interval</span> <span class="o">=</span> <span class="mi">6000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">previousMillis</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Useful for debugging purposes
</span></span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">begin</span><span class="p">(</span><span class="mi">9600</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="nc">Serial</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="p">;</span> <span class="c1">// wait for serial port to connect. Needed for native USB port only
</span></span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">sensors</span><span class="p">.</span><span class="nf">begin</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">currentMillis</span> <span class="o">=</span> <span class="nf">millis</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">currentMillis</span> <span class="o">-</span> <span class="n">previousMillis</span> <span class="o">&gt;=</span> <span class="n">interval</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Sensor loop #&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="n">count</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// save last time message sent
</span></span></span><span class="line"><span class="cl"> <span class="n">previousMillis</span> <span class="o">=</span> <span class="n">currentMillis</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Get temp(s)
</span></span></span><span class="line"><span class="cl"> <span class="n">sensors</span><span class="p">.</span><span class="n">requestTemperatures</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">temp_count</span> <span class="o">=</span> <span class="n">sensors</span><span class="p">.</span><span class="n">getDeviceCount</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">temp_count</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Reading temp #&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;: &#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="n">sensors</span><span class="p">.</span><span class="n">getTempCByIndex</span><span class="p">(</span><span class="n">i</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">count</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Running this code, I was getting two readings every minute&hellip; oops. No, every 6 seconds. It&rsquo;s helpful to remember that Arduino tracks &ldquo;time&rdquo; 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.</p>
<h3>JSON<span class="hx:absolute hx:-mt-20" id="json"></span>
<a href="#json" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>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.</p>
<p>Nobody wants to write JSON with print statements though. No, you don&rsquo;t. Stop. There&rsquo;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 <a href="https://arduinojson.org/v6/example/generator/"target="_blank" rel="noopener">example</a> I could borrow from.</p>
<p>I can just include <code>ArduinoJson.h</code> and <code>ArduinoJson.hpp</code> to create a serializable variable for json. This would be awesome when I got around to adding MQTT. But I didn&rsquo;t know that yet. I was just going to print it to the console. It just needed a few tweaks to the code.</p>
<p>First, add some <code>const char *</code> 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.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-arduino" data-lang="arduino"><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span> <span class="n">readingKey</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;reading_idx&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span> <span class="n">tempsKey</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;temps&#34;</span><span class="p">;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>Next, create the JSON document and populate it. BUT&hellip; this is static, so I needed to use a <a href="https://arduinojson.org/v6/assistant"target="_blank" rel="noopener">tool</a> (from the same place that created the library) to calculate the size I&rsquo;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!</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-arduino" data-lang="arduino"><span class="line"><span class="cl"><span class="c1">// Use arduinojson.org/v6/assistant to compute the capacity.
</span></span></span><span class="line"><span class="cl"><span class="n">StaticJsonDocument</span><span class="o">&lt;</span><span class="mi">48</span><span class="o">&gt;</span> <span class="n">doc</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span><span class="p">[</span><span class="n">nameKey</span><span class="p">]</span> <span class="o">=</span> <span class="n">name</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span><span class="p">[</span><span class="n">readingKey</span><span class="p">]</span> <span class="o">=</span> <span class="n">count</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">JsonArray</span> <span class="n">data</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="n">createNestedArray</span><span class="p">(</span><span class="n">tempsKey</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">temp_count</span> <span class="o">=</span> <span class="n">sensors</span><span class="p">.</span><span class="n">getDeviceCount</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">temp_count</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Reading temp #&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">data</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">sensors</span><span class="p">.</span><span class="n">getTempCByIndex</span><span class="p">(</span><span class="n">i</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">serializeJson</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="nc">Serial</span><span class="p">);</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<h3>Networking (oops, also everything else)<span class="hx:absolute hx:-mt-20" id="networking-oops-also-everything-else"></span>
<a href="#networking-oops-also-everything-else" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>It&rsquo;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.</p>
<p>The final code (which is running on my sensor right now) is as follows:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-arduino" data-lang="arduino"><span class="line"><span class="cl"><span class="err">```</span><span class="n">arduino</span>
</span></span><span class="line"><span class="cl"><span class="c1">// ArduinoJson - Version: Latest
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;ArduinoJson.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;ArduinoJson.hpp&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// OneWire - Version: Latest
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;OneWire.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// DallasTemperature - Version: Latest
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;DallasTemperature.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// ArduinoMqttClient - Version: Latest
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;ArduinoMqttClient.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;SPI.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;Ethernet.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// hardware config
</span></span></span><span class="line"><span class="cl"><span class="cp">#define ONE_WIRE_BUS 2
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">OneWire</span> <span class="n">oneWire</span><span class="p">(</span><span class="n">ONE_WIRE_BUS</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">DallasTemperature</span> <span class="nf">sensors</span><span class="p">(</span><span class="o">&amp;</span><span class="n">oneWire</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Set your MAC address
</span></span></span><span class="line"><span class="cl"><span class="kt">byte</span> <span class="n">mac</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="mh">0x90</span><span class="p">,</span> <span class="mh">0xA2</span><span class="p">,</span> <span class="mh">0xDA</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x95</span><span class="p">,</span> <span class="mh">0x0F</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span> <span class="n">name</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;90:a2:da:00:95:0f&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span> <span class="n">nameKey</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;name&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span> <span class="n">readingKey</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;reading_idx&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span> <span class="n">tempsKey</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;temps&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span> <span class="n">broker</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;test.mosquitto.org&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">port</span> <span class="o">=</span> <span class="mi">1883</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">char</span> <span class="n">dataTopic</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;Hotpants/data&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// take reading every 60s
</span></span></span><span class="line"><span class="cl"><span class="k">const</span> <span class="kt">long</span> <span class="n">interval</span> <span class="o">=</span> <span class="mi">60000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">previousMillis</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Ethernet and MQTT related objects
</span></span></span><span class="line"><span class="cl"><span class="nc">EthernetClient</span> <span class="n">ethClient</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">MqttClient</span> <span class="nf">mqttClient</span><span class="p">(</span><span class="n">ethClient</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Useful for debugging purposes
</span></span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">begin</span><span class="p">(</span><span class="mi">9600</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="nc">Serial</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="p">;</span> <span class="c1">// wait for serial port to connect. Needed for native USB port only
</span></span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Start the ethernet connection (DHCP)
</span></span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Attempting to connect to network&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="k">while</span> <span class="p">(</span><span class="nc">Ethernet</span><span class="p">.</span><span class="nf">begin</span><span class="p">(</span><span class="n">mac</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// failed, retry
</span></span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;.&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nf">delay</span><span class="p">(</span><span class="mi">3000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;You&#39;re connected to the network. IP address = &#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="nc">Ethernet</span><span class="p">.</span><span class="nf">localIP</span><span class="p">());</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Attempt to connect to the server
</span></span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Attempting to connect to MQTT broker: &#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="n">broker</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">mqttClient</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">broker</span><span class="p">,</span> <span class="n">port</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="s">&#34;Connection established&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">else</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;MQTT connection failed. Error code = &#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="n">mqttClient</span><span class="p">.</span><span class="n">connectError</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// block if unable to establish connection
</span></span></span><span class="line"><span class="cl"> <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">sensors</span><span class="p">.</span><span class="nf">begin</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// This is needed at the top of the loop!
</span></span></span><span class="line"><span class="cl"> <span class="n">mqttClient</span><span class="p">.</span><span class="n">poll</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">currentMillis</span> <span class="o">=</span> <span class="nf">millis</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="n">currentMillis</span> <span class="o">-</span> <span class="n">previousMillis</span> <span class="o">&gt;=</span> <span class="n">interval</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// save last time message sent
</span></span></span><span class="line"><span class="cl"> <span class="n">previousMillis</span> <span class="o">=</span> <span class="n">currentMillis</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Get temp(s)
</span></span></span><span class="line"><span class="cl"> <span class="n">sensors</span><span class="p">.</span><span class="n">requestTemperatures</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Use arduinojson.org/v6/assistant to compute the capacity.
</span></span></span><span class="line"><span class="cl"> <span class="n">StaticJsonDocument</span><span class="o">&lt;</span><span class="mi">48</span><span class="o">&gt;</span> <span class="n">doc</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">doc</span><span class="p">[</span><span class="n">nameKey</span><span class="p">]</span> <span class="o">=</span> <span class="n">name</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="n">doc</span><span class="p">[</span><span class="n">readingKey</span><span class="p">]</span> <span class="o">=</span> <span class="n">count</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">JsonArray</span> <span class="n">data</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="n">createNestedArray</span><span class="p">(</span><span class="n">tempsKey</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kt">int</span> <span class="n">temp_count</span> <span class="o">=</span> <span class="n">sensors</span><span class="p">.</span><span class="n">getDeviceCount</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">temp_count</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Reading temp #&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">data</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">sensors</span><span class="p">.</span><span class="n">getTempCByIndex</span><span class="p">(</span><span class="n">i</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Sending message. Topic = &#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">(</span><span class="n">dataTopic</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="n">serializeJson</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="nc">Serial</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// send message
</span></span></span><span class="line"><span class="cl"> <span class="n">mqttClient</span><span class="p">.</span><span class="n">beginMessage</span><span class="p">(</span><span class="n">dataTopic</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="n">serializeJson</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">mqttClient</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="n">mqttClient</span><span class="p">.</span><span class="n">endMessage</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nc">Serial</span><span class="p">.</span><span class="nf">println</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="n">count</span> <span class="o">=</span> <span class="p">(</span><span class="n">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">%</span> <span class="mi">128</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>There are a few issues that I need to address.</p>
<ul>
<li>If the MQTT connection dies. You have to manually restart the sensor. This is not ideal.</li>
<li>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&rsquo;s for a later enhancement.</li>
<li>I&rsquo;m running at about 90% program space used. Adding other functionality may require some optimization.</li>
<li>I never send any information about the sensor itself via MQTT. This would be helpful for later use (mac address, sensor type, etc.)</li>
<li>The biggest problem&hellip; I&rsquo;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&rsquo;s dumb. I will change this very soon.</li>
<li>It&rsquo;s annoying, but I am taking readings if the furnace is on or off. I don&rsquo;t really care what the readings are when it&rsquo;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 <a href="https://web.archive.org/web/20221101/https://www.pes-psrc.org/kb/published/reports/Use%20of%20Hall%20Effect%20Sensors%20for%20Protection%20and%20Monitoring%20Applications%20-%20March%202018.pdf"target="_blank" rel="noopener">few ideas</a> on how to do this but haven&rsquo;t done any further work than that.</li>
</ul>
<h2>Backend Development<span class="hx:absolute hx:-mt-20" id="backend-development"></span>
<a href="#backend-development" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>MQTT Broker<span class="hx:absolute hx:-mt-20" id="mqtt-broker"></span>
<a href="#mqtt-broker" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I started off by using <code>test.mosquitto.org</code> and realized it works fine for what I&rsquo;m doing now. I will eventually change this to be hosted locally, but not now. It works well enough for who it&rsquo;s for (that&rsquo;s me, btw).</p>
<h3>Timescale<span class="hx:absolute hx:-mt-20" id="timescale"></span>
<a href="#timescale" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I was going to do this locally. I installed postgresql on my linux server. I&rsquo;m not including what I did for that because I haven&rsquo;t used it at all. I created a Timescale Cloud account and which is free for 30 days. In a month, I&rsquo;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.</p>
<h3>Grafana<span class="hx:absolute hx:-mt-20" id="grafana"></span>
<a href="#grafana" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>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&rsquo;s working as is for now.</p>
<h2>Service Development: The Glue (aka shortshorts)<span class="hx:absolute hx:-mt-20" id="service-development-the-glue-aka-shortshorts"></span>
<a href="#service-development-the-glue-aka-shortshorts" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>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 <strong>sorta</strong> do that. But I haven&rsquo;t done any projects in Go that have networking, async communication, or contexts. Let&rsquo;s roll up our sleeves and figure out 50% of the battle GI Joe was talking about.</p>
<p>As a brief aside&hellip; The name &ldquo;shortshorts&rdquo; has very little meaning to this project. I needed a name for the MQTT topic and had chosen &ldquo;Hotpants&rdquo; as it seemed funny at 1am when I was developing that part. I didn&rsquo;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.</p>
<p>My 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 <a href="https://github.com/CloudMQTT/go-mqtt-example/blob/22e6befea8ed6005090dfdce063aa7bd93e2279b/main.go"target="_blank" rel="noopener">GitHub</a> and <a href="https://levelup.gitconnected.com/how-to-use-mqtt-with-go-89c617915774"target="_blank" rel="noopener">elsewhere</a>.</p>
<p>Once 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 <a href="https://docs.timescale.com/timescaledb/latest/quick-start/golang/"target="_blank" rel="noopener">Go Tutorial</a>.</p>
<p>I had everything crammed into <code>main.go</code> and it worked. I didn&rsquo;t want to share that code with anyone at that point though. Time to <a href="https://github.com/gbeletti/service-golang"target="_blank" rel="noopener">make this more like a service</a>, 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&rsquo;m ok with them for now.</p>
<h3>Application Code Highlights<span class="hx:absolute hx:-mt-20" id="application-code-highlights"></span>
<a href="#application-code-highlights" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>All of the go application code is <a href="https://github.com/asphaltbuffet/shortshorts"target="_blank" rel="noopener">available on GitHub</a>.</p>
<p>The application flow starts in main (this should not be the interesting part of what I&rsquo;m saying). I set up some contexts to help everything shut down nicely when it&rsquo;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.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">cancel</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nf">WithCancel</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">tsdb</span><span class="p">,</span><span class="w"> </span><span class="nx">dstream</span><span class="p">,</span><span class="w"> </span><span class="nx">client</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">start</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">go</span><span class="w"> </span><span class="nf">processLoop</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">dstream</span><span class="p">,</span><span class="w"> </span><span class="nx">tsdb</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">servicemanager</span><span class="p">.</span><span class="nf">WaitShutdown</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nf">shutdown</span><span class="p">(</span><span class="nx">cancel</span><span class="p">,</span><span class="w"> </span><span class="nx">tsdb</span><span class="p">,</span><span class="w"> </span><span class="nx">client</span><span class="p">)</span><span class="w"> </span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>The processing function loops forever until the context completes. Otherwise, it is reading data from the MQTT data stream channel.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">processLoop</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">ds</span><span class="w"> </span><span class="kd">chan</span><span class="w"> </span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">tsdb</span><span class="w"> </span><span class="o">*</span><span class="nx">timescalewrapper</span><span class="p">.</span><span class="nx">Database</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="nx">d</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ds</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;received sensor data&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">String</span><span class="p">(</span><span class="s">&#34;topic&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">d</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">String</span><span class="p">(</span><span class="s">&#34;payload&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">d</span><span class="p">[</span><span class="mi">1</span><span class="p">]))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">reading</span><span class="w"> </span><span class="nx">timescalewrapper</span><span class="p">.</span><span class="nx">SensorData</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">json</span><span class="p">.</span><span class="nf">Unmarshal</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">d</span><span class="p">[</span><span class="mi">1</span><span class="p">]),</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">reading</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;unmarshalling payload&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">err</span><span class="p">),</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">String</span><span class="p">(</span><span class="s">&#34;payload&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">d</span><span class="p">[</span><span class="mi">1</span><span class="p">]))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nf">Debug</span><span class="p">(</span><span class="s">&#34;unmarshalling payload&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">Any</span><span class="p">(</span><span class="s">&#34;reading&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">reading</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">tsdb</span><span class="p">.</span><span class="nf">InsertData</span><span class="p">(</span><span class="nx">reading</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;inserting data&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">err</span><span class="p">),</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">Any</span><span class="p">(</span><span class="s">&#34;reading&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">reading</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>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&rsquo;s no validation here and it feels like a kludge. It&rsquo;s a TODO for later.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">readConfig</span><span class="p">()</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">viper</span><span class="p">.</span><span class="nf">SetConfigName</span><span class="p">(</span><span class="s">&#34;config&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">viper</span><span class="p">.</span><span class="nf">SetConfigType</span><span class="p">(</span><span class="s">&#34;yaml&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">viper</span><span class="p">.</span><span class="nf">AddConfigPath</span><span class="p">(</span><span class="s">&#34;.&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">viper</span><span class="p">.</span><span class="nf">ReadInConfig</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nf">Panic</span><span class="p">(</span><span class="s">&#34;reading config file&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">err</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">sb</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nx">Builder</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;postgres://&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">viper</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="s">&#34;timescale.user&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;:&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">viper</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="s">&#34;timescale.password&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;@&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">viper</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="s">&#34;timescale.host&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;:&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">viper</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="s">&#34;timescale.port&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">viper</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="s">&#34;timescale.database&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;?sslmode=&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">viper</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="s">&#34;timescale.sslmode&#34;</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">sb</span><span class="p">.</span><span class="nf">String</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>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.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="nx">f</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">client</span><span class="w"> </span><span class="nx">mqtt</span><span class="p">.</span><span class="nx">Client</span><span class="p">,</span><span class="w"> </span><span class="nx">msg</span><span class="w"> </span><span class="nx">mqtt</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">datastream</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="nx">msg</span><span class="p">.</span><span class="nf">Topic</span><span class="p">(),</span><span class="w"> </span><span class="nb">string</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nf">Payload</span><span class="p">())}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">token</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">cli</span><span class="p">.</span><span class="nf">Subscribe</span><span class="p">(</span><span class="nx">topic</span><span class="p">,</span><span class="w"> </span><span class="nb">byte</span><span class="p">(</span><span class="nx">qos</span><span class="p">),</span><span class="w"> </span><span class="nx">f</span><span class="p">);</span><span class="w"> </span><span class="nx">token</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">token</span><span class="p">.</span><span class="nf">Error</span><span class="p">()</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;error subscribing to topic&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">zap</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">token</span><span class="p">.</span><span class="nf">Error</span><span class="p">()))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w"> </span><span class="nx">token</span><span class="p">.</span><span class="nf">Error</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>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&rsquo;s a low priority item, though, as I don&rsquo;t expect to see a significant amount of churn here. There are only so many ways to send the readings I&rsquo;m gathering. I could plan for additional complexity, but that violates at least two of my project criteria. YAGNI indeed.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-golang" data-lang="golang"><span class="line"><span class="cl"><span class="c1">// InsertData inserts the data into the database.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">db</span><span class="w"> </span><span class="o">*</span><span class="nx">Database</span><span class="p">)</span><span class="w"> </span><span class="nf">InsertData</span><span class="p">(</span><span class="nx">reading</span><span class="w"> </span><span class="nx">SensorData</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">queryInsertData</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">`INSERT INTO conditions (time, mac, temp_delta, raw_temp0, raw_temp1) VALUES ($1, $2, $3, $4, $5)`</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">db</span><span class="p">.</span><span class="nx">pool</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span><span class="w"> </span><span class="nx">queryInsertData</span><span class="p">,</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">(),</span><span class="w"> </span><span class="nx">reading</span><span class="p">.</span><span class="nx">Mac</span><span class="p">,</span><span class="w"> </span><span class="nx">reading</span><span class="p">.</span><span class="nx">Temps</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">-</span><span class="nx">reading</span><span class="p">.</span><span class="nx">Temps</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span><span class="w"> </span><span class="nx">reading</span><span class="p">.</span><span class="nx">Temps</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span><span class="w"> </span><span class="nx">reading</span><span class="p">.</span><span class="nx">Temps</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;inserting data: %w&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<h2>Results<span class="hx:absolute hx:-mt-20" id="results"></span>
<a href="#results" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now, Faithful Reader, if you&rsquo;ve made it this far&hellip; what does this actually look like?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">
<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;level&#34;</span><span class="p">:</span><span class="s2">&#34;info&#34;</span><span class="p">,</span><span class="nt">&#34;ts&#34;</span><span class="p">:</span><span class="s2">&#34;2022-11-27T20:49:37.475-0500&#34;</span><span class="p">,</span><span class="nt">&#34;caller&#34;</span><span class="p">:</span><span class="s2">&#34;mqttwrapper/mqtt.go:43&#34;</span><span class="p">,</span><span class="nt">&#34;msg&#34;</span><span class="p">:</span><span class="s2">&#34;Connected to broker&#34;</span><span class="p">,</span><span class="nt">&#34;broker&#34;</span><span class="p">:</span><span class="s2">&#34;tcp://test.mosquitto.org:1883&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;level&#34;</span><span class="p">:</span><span class="s2">&#34;info&#34;</span><span class="p">,</span><span class="nt">&#34;ts&#34;</span><span class="p">:</span><span class="s2">&#34;2022-11-27T20:49:37.565-0500&#34;</span><span class="p">,</span><span class="nt">&#34;caller&#34;</span><span class="p">:</span><span class="s2">&#34;mqttwrapper/mqtt.go:65&#34;</span><span class="p">,</span><span class="nt">&#34;msg&#34;</span><span class="p">:</span><span class="s2">&#34;successfully subscribed&#34;</span><span class="p">,</span><span class="nt">&#34;topic&#34;</span><span class="p">:</span><span class="s2">&#34;Hotpants/data&#34;</span><span class="p">,</span><span class="nt">&#34;broker&#34;</span><span class="p">:</span><span class="s2">&#34;tcp://test.mosquitto.org:1883&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;level&#34;</span><span class="p">:</span><span class="s2">&#34;info&#34;</span><span class="p">,</span><span class="nt">&#34;ts&#34;</span><span class="p">:</span><span class="s2">&#34;2022-11-27T20:56:52.395-0500&#34;</span><span class="p">,</span><span class="nt">&#34;caller&#34;</span><span class="p">:</span><span class="s2">&#34;shortshorts/main.go:77&#34;</span><span class="p">,</span><span class="nt">&#34;msg&#34;</span><span class="p">:</span><span class="s2">&#34;received sensor data&#34;</span><span class="p">,</span><span class="nt">&#34;topic&#34;</span><span class="p">:</span><span class="s2">&#34;Hotpants/data&#34;</span><span class="p">,</span><span class="nt">&#34;payload&#34;</span><span class="p">:</span><span class="s2">&#34;{\&#34;name\&#34;:\&#34;90:a2:da:00:95:0f\&#34;,\&#34;reading_idx\&#34;:0,\&#34;temps\&#34;:[21.3125,20.6875]}&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;level&#34;</span><span class="p">:</span><span class="s2">&#34;info&#34;</span><span class="p">,</span><span class="nt">&#34;ts&#34;</span><span class="p">:</span><span class="s2">&#34;2022-11-27T20:57:52.409-0500&#34;</span><span class="p">,</span><span class="nt">&#34;caller&#34;</span><span class="p">:</span><span class="s2">&#34;shortshorts/main.go:77&#34;</span><span class="p">,</span><span class="nt">&#34;msg&#34;</span><span class="p">:</span><span class="s2">&#34;received sensor data&#34;</span><span class="p">,</span><span class="nt">&#34;topic&#34;</span><span class="p">:</span><span class="s2">&#34;Hotpants/data&#34;</span><span class="p">,</span><span class="nt">&#34;payload&#34;</span><span class="p">:</span><span class="s2">&#34;{\&#34;name\&#34;:\&#34;90:a2:da:00:95:0f\&#34;,\&#34;reading_idx\&#34;:1,\&#34;temps\&#34;:[21.25,20.625]}&#34;</span><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
aria-label="Copy code"
data-copied-label="Copied!"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>
</div>
<p>The logging fills up my terminal nicely and gives me an indication that things are working or not. I&rsquo;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&rsquo;m fairly certain it&rsquo;s a memory issue on the sensor or a failure to recover from a dead MQTT connection.</p>
<figure><img src="https://asphaltbuffet.com/blog/hvac-monitoring/grafana.png"
alt="Grafana dashboard showing HVAC sensor data">
</figure>
<p>The 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&rsquo;s likely giving a reading of the basement ambient temp instead of air temps inside the return vent. I&rsquo;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.</p>
<p>I wonder what they&rsquo;d say if I send them this dashboard output as the reason for my service request?</p></description></item><item><title>SRI: Fixing Security Warnings</title><link>https://asphaltbuffet.com/blog/js-integrity/</link><pubDate>Sun, 17 Jul 2022 00:00:00 +0000</pubDate><guid>https://asphaltbuffet.com/blog/js-integrity/</guid><description>