me like nix
1{ pkgs, lib, config, ... }:
2
3let
4 cfg = config.pi;
5
6 # Workaround from https://github.com/NixOS/nixos-hardware/blob/master/raspberry-pi/4/apply-overlays-dtmerge.nix
7 deviceTree_overlay = _final: prev: {
8 deviceTree = {
9 applyOverlays = prev.callPackage ./overlays/apply-overlays-dtmerge.nix { };
10 compileDTS = prev.deviceTree.compileDTS;
11 };
12 };
13
14 # Custom libcamera with Raspberry Pi IPA/pipeline support
15 libcamera-rpi = pkgs.libcamera.overrideAttrs (old: {
16 mesonFlags = (old.mesonFlags or [ ]) ++ [
17 "-Dipas=rpi/vc4,rpi/pisp"
18 "-Dpipelines=rpi/vc4,rpi/pisp"
19 ];
20 });
21
22 # rpicam-apps using custom libcamera
23 rpicam-apps = pkgs.stdenv.mkDerivation rec {
24 pname = "rpicam-apps";
25 version = "1.11.1";
26
27 src = pkgs.fetchFromGitHub {
28 owner = "raspberrypi";
29 repo = "rpicam-apps";
30 rev = "v${version}";
31 hash = "sha256-hVoKbvWFeramPkHuibJwUgFOPS9v588+K8828a1fNnA=";
32 };
33
34 nativeBuildInputs = with pkgs; [
35 meson
36 ninja
37 pkg-config
38 ];
39
40 buildInputs = [
41 libcamera-rpi
42 pkgs.libdrm
43 pkgs.libexif
44 pkgs.libjpeg
45 pkgs.libpng
46 pkgs.libtiff
47 pkgs.boost
48 pkgs.ffmpeg
49 ];
50
51 mesonFlags = [
52 "-Denable_libav=enabled"
53 "-Denable_drm=enabled"
54 "-Denable_egl=disabled"
55 "-Denable_qt=disabled"
56 "-Denable_opencv=disabled"
57 "-Denable_tflite=disabled"
58 "-Denable_hailo=disabled"
59 ];
60
61 meta = with lib; {
62 description = "Raspberry Pi camera applications";
63 homepage = "https://github.com/raspberrypi/rpicam-apps";
64 license = licenses.bsd2;
65 platforms = [ "aarch64-linux" ];
66 };
67 };
68in
69{
70 options.pi = {
71 streamName = lib.mkOption {
72 type = lib.types.str;
73 description = "Name of the camera stream";
74 };
75
76 resolution = {
77 width = lib.mkOption {
78 type = lib.types.int;
79 default = 1920;
80 description = "Camera resolution width";
81 };
82 height = lib.mkOption {
83 type = lib.types.int;
84 default = 1080;
85 description = "Camera resolution height";
86 };
87 };
88
89 framerate = lib.mkOption {
90 type = lib.types.int;
91 default = 30;
92 description = "Camera framerate";
93 };
94
95 deviceTreeFilter = lib.mkOption {
96 type = lib.types.str;
97 description = "Device tree filter pattern";
98 };
99
100 deviceTreeCompatible = lib.mkOption {
101 type = lib.types.str;
102 description = "Device tree compatible string (e.g., brcm,bcm2711)";
103 };
104
105 gpuMem = lib.mkOption {
106 type = lib.types.int;
107 default = 256;
108 description = "GPU memory allocation in MB";
109 };
110
111 flipCamera = lib.mkOption {
112 type = lib.types.bool;
113 default = false;
114 description = "Flip camera image vertically and horizontally (180 degree rotation)";
115 };
116
117 };
118
119 config = {
120 nix.settings.trusted-users = [ "sean" ];
121
122 # Pre-generated SSH host key for agenix decryption (shared across all Pis)
123 services.openssh.hostKeys = [
124 {
125 path = "/etc/ssh/ssh_host_ed25519_key";
126 type = "ed25519";
127 }
128 ];
129
130 environment.etc."ssh/ssh_host_ed25519_key" = {
131 source = /home/sean/nixos-config/secrets/pi_host_key;
132 mode = "0600";
133 };
134
135 # Agenix configuration - use Nix store path directly so the key is available
136 # before the etc activation script runs (agenix activates before etc)
137 age.identityPaths = [ "${/home/sean/nixos-config/secrets/pi_host_key}" ];
138 age.secrets.wifi = {
139 file = ../../secrets/wifi.age;
140 mode = "0444";
141 };
142
143 # WiFi configuration using wpa_supplicant with agenix credentials
144 networking.wireless = {
145 enable = true;
146 secretsFile = config.age.secrets.wifi.path;
147 networks."GL-MT6000-6a6".pskRaw = "ext:WIFI_PSK";
148 };
149
150 # Enable DHCP for ethernet
151 networking.useDHCP = true;
152 # Add device tree overlay for dtmerge support
153 nixpkgs.overlays = [ deviceTree_overlay ];
154
155 # Disable ZFS which isn't supported on Pi
156 boot.supportedFilesystems = lib.mkForce [ "vfat" "ext4" ];
157
158 # Pi kernel lacks device-mapper, so use legacy initrd (not systemd)
159 boot.initrd.systemd.enable = false;
160
161 # Enable SSH for headless setup
162 services.openssh = {
163 enable = true;
164 settings = {
165 PasswordAuthentication = false;
166 PermitRootLogin = "no";
167 };
168 };
169
170 # User config
171 users.users.sean = {
172 isNormalUser = true;
173 extraGroups = [ "wheel" "video" ];
174 openssh.authorizedKeys.keys = [
175 "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCIqgZ7kedxo+mOW7YG73Vp3zel3h180y3GKvHtRsXfGlpIIvRDy7pgCBQ4AGXYD4y78URQmFohYSAPqCPOPaWcU2un3XG9KvCzEsHmsbskPonitUmCiKvrKkb6oW4jCBtd7AEtBn+AiajAQFtPZ7NN2Df3AmTypvR6Irg7R+nxnfc9NTIHmGvxSDyWcbb4pguL20sctUSqGL6xGh8q/bqhdOThSimM+z9bEUNxK/5rPhwkNniMrp4pJcUrUiAh5/4DiRFG6KT+oeg+/myoz/Z1sPvAs7u/8JDQI4RshRD8Hu0oTkRBN6Hxj478q2SXbeBUZlD6IdjP3RhGpmSecoDdtWqKbpuV3eVRtQtba3KL86GBeV/bugaOdJ1Aud+1SOFJreAAuvxzMMKT+cdQZk6oOPP148DA/No+mDm/2S43lcdCXh79wA6YRAmKQ8jmZxTCtPutrvuZK1rguvvUlEoG/vhdNHh7eDa4Td07V6bjCRPUl8qk/e4M0E3pwsTlZc="
176 ];
177 };
178
179 # Allow sudo without password for wheel group
180 security.sudo.wheelNeedsPassword = false;
181
182 # go2rtc for camera streaming to Home Assistant
183 services.go2rtc = {
184 enable = true;
185 settings = {
186 ffmpeg.bin = "${pkgs.ffmpeg}/bin/ffmpeg";
187 streams = {
188 "${cfg.streamName}" = "exec:${rpicam-apps}/bin/rpicam-vid -t 0 --width ${toString cfg.resolution.width} --height ${toString cfg.resolution.height} --framerate ${toString cfg.framerate} --codec h264 --inline${lib.optionalString cfg.flipCamera " --vflip --hflip"} -o -";
189 };
190 };
191 };
192
193 # udev rule to give video group access to DMA heap devices (required for libcamera)
194 services.udev.extraRules = ''
195 SUBSYSTEM=="dma_heap", GROUP="video", MODE="0660"
196 '';
197
198 # Override go2rtc systemd service to run as root for camera access
199 systemd.services.go2rtc.serviceConfig = {
200 User = lib.mkForce "root";
201 };
202
203 # Camera and system tools
204 environment.systemPackages = [
205 pkgs.ffmpeg
206 pkgs.libraspberrypi
207 libcamera-rpi
208 rpicam-apps
209 pkgs.v4l-utils
210 ];
211
212 # Device tree configuration for Pi Camera v3 (IMX708)
213 hardware.deviceTree.filter = cfg.deviceTreeFilter;
214 hardware.deviceTree.overlays = [
215 {
216 name = "imx708-overlay";
217 dtsText = ''
218 // SPDX-License-Identifier: GPL-2.0-only
219 // Definitions for IMX708 camera module on VC I2C bus
220 /dts-v1/;
221 /plugin/;
222
223 /{
224 compatible = "${cfg.deviceTreeCompatible}";
225
226 fragment@0 {
227 target = <&i2c0if>;
228 __overlay__ {
229 status = "okay";
230 };
231 };
232
233 clk_frag: fragment@1 {
234 target = <&cam1_clk>;
235 __overlay__ {
236 status = "okay";
237 clock-frequency = <24000000>;
238 };
239 };
240
241 fragment@2 {
242 target = <&i2c0mux>;
243 __overlay__ {
244 status = "okay";
245 };
246 };
247
248 reg_frag: fragment@3 {
249 target = <&cam1_reg>;
250 cam_reg: __overlay__ {
251 startup-delay-us = <70000>;
252 off-on-delay-us = <30000>;
253 regulator-min-microvolt = <2700000>;
254 regulator-max-microvolt = <2700000>;
255 };
256 };
257
258 i2c_frag: fragment@100 {
259 target = <&i2c_csi_dsi>;
260 __overlay__ {
261 #address-cells = <1>;
262 #size-cells = <0>;
263 status = "okay";
264
265 // IMX708 sensor configuration (from imx708.dtsi)
266 cam_node: imx708@1a {
267 compatible = "sony,imx708";
268 reg = <0x1a>;
269 status = "okay";
270
271 clocks = <&cam1_clk>;
272 clock-names = "inclk";
273
274 vana1-supply = <&cam1_reg>;
275 vana2-supply = <&cam_dummy_reg>;
276 vdig-supply = <&cam_dummy_reg>;
277 vddl-supply = <&cam_dummy_reg>;
278
279 rotation = <180>;
280 orientation = <2>;
281
282 port {
283 cam_endpoint: endpoint {
284 clock-lanes = <0>;
285 data-lanes = <1 2>;
286 clock-noncontinuous;
287 link-frequencies =
288 /bits/ 64 <450000000>;
289 };
290 };
291 };
292
293 // VCM (autofocus motor) configuration
294 vcm_node: dw9817@c {
295 compatible = "dongwoon,dw9817-vcm";
296 reg = <0x0c>;
297 status = "okay";
298 VDD-supply = <&cam1_reg>;
299 };
300 };
301 };
302
303 csi_frag: fragment@101 {
304 target = <&csi1>;
305 csi: __overlay__ {
306 status = "okay";
307 brcm,media-controller;
308
309 port {
310 csi_ep: endpoint {
311 remote-endpoint = <&cam_endpoint>;
312 clock-lanes = <0>;
313 data-lanes = <1 2>;
314 clock-noncontinuous;
315 };
316 };
317 };
318 };
319
320 __overrides__ {
321 rotation = <&cam_node>,"rotation:0";
322 orientation = <&cam_node>,"orientation:0";
323 media-controller = <&csi>,"brcm,media-controller?";
324 cam0 = <&i2c_frag>, "target:0=",<&i2c_csi_dsi0>,
325 <&csi_frag>, "target:0=",<&csi0>,
326 <&clk_frag>, "target:0=",<&cam0_clk>,
327 <®_frag>, "target:0=",<&cam0_reg>,
328 <&cam_node>, "clocks:0=",<&cam0_clk>,
329 <&cam_node>, "vana1-supply:0=",<&cam0_reg>,
330 <&vcm_node>, "VDD-supply:0=",<&cam0_reg>;
331 vcm = <&vcm_node>, "status",
332 <0>, "=4";
333 link-frequency = <&cam_endpoint>,"link-frequencies#0";
334 };
335 };
336
337 &cam_endpoint {
338 remote-endpoint = <&csi_ep>;
339 };
340 '';
341 }
342 ];
343
344 # Raspberry Pi firmware for camera
345 hardware.enableRedistributableFirmware = true;
346
347 # Add camera config and overlays to firmware partition
348 sdImage.populateFirmwareCommands = lib.mkAfter ''
349 chmod u+w ./firmware/config.txt
350 cat >> ./firmware/config.txt << EOF
351
352# Camera support - Pi Camera v3 (IMX708)
353gpu_mem=${toString cfg.gpuMem}
354EOF
355
356 # Copy device tree overlays for camera auto-detect
357 if [ -d ${pkgs.raspberrypifw}/share/raspberrypi/boot/overlays ]; then
358 cp -r ${pkgs.raspberrypifw}/share/raspberrypi/boot/overlays ./firmware/
359 fi
360 '';
361
362 # Firewall
363 networking.firewall.allowedTCPPorts = [
364 22 # SSH
365 1984 # go2rtc API
366 8554 # RTSP
367 ];
368 };
369}