{ self, cfg, pkgs, lib, ... }: let createMlatService = (name: feedConfig: let uuidFile = pkgs.writeTextFile { name = "${name}-uuid"; text = "${feedConfig.uuid}"; }; options = [ "--input-type" feedConfig.mlat.input.type "--no-udp" "--input-connect" "${feedConfig.mlat.input.host}:${toString feedConfig.mlat.input.port}" "--server" feedConfig.mlat.server "--user" feedConfig.username "--lat" cfg.latitude "--lon" cfg.longitude "--alt" (toString cfg.altitude) "--uuid-file" "${uuidFile}" ] ++ ( if feedConfig.mlat.privacy then [ "--privacy" ] else [ ] ) ++ ( map (result: "--result ${result}") feedConfig.mlat.results ); in { enable = feedConfig.mlat.enable; description = "${name} MLAT client."; wants = [ "network.target" ]; after = [ "network.target" ]; unitConfig = { Documentation = "https://github.com/makrsmark/mlat-client/"; }; serviceConfig = { Type = "simple"; User = "${name}"; RuntimeDirectory = "${name}-mlat"; RuntimeDirectoryMode = "0755"; ExecStart = "${self.packages.${pkgs.system}.adsbfi-mlat-client}/bin/mlat-client ${lib.concatStringsSep " " options}"; SyslogIdentifier = "${name}-mlat"; Restart = "on-failure"; RestartSec = 30; Nice = -1; }; wantedBy = [ "multi-user.target" ]; } ); createFeedService = (name: feedConfig: let uuidFile = pkgs.writeTextFile { name = "${name}-uuid"; text = "${feedConfig.uuid}"; }; connectors = map ( connector: "--net-connector ${connector}" ) ( map ( connector: ( lib.concatStringsSep "," ( [ connector.primary.host (toString connector.primary.port) connector.protocol ] ++ ( if connector.silentFail then [ "silent_fail" ] else [ ] ) ++ ( if connector.secondary != null then [ connector.secondary.host (toString connector.secondary.port) ] else [ ] ) ) ) ) feedConfig.feed.connectors ); options = [ "--quiet" "--net" "--net-only" "--write-json" "%t/${name}-feed" "--net-beast-reduce-interval 0.5" "--net-heartbeat 60" "--net-ro-size 1280" "--net-ro-interval 0.2" "--net-ro-port 0" "--net-sbs-port 0" "--net-bi-port ${toString feedConfig.feed.beastInputPort}" "--net-bo-port 0" "--net-ri-port 0" "--write-json-every 1" "--lat" cfg.latitude "--lon" cfg.longitude "--max-range 450" "--json-location-accuracy 2" "--range-outline-hours 24" "--uuid-file" "${uuidFile}" ] ++ connectors; in { enable = feedConfig.feed.enable; description = "ADSB.fi feeder."; wants = [ "network.target" ]; after = [ "network.target" ]; unitConfig = { Documentation = "https://github.com/widehopf/readsb/"; StartLimitIntervalSec = 1; }; serviceConfig = { Type = "simple"; User = "${name}"; RuntimeDirectory = "${name}-feed"; RuntimeDirectoryMode = "0755"; ExecStart = "${self.packages.${pkgs.system}.adsbfi-readsb}/bin/readsb ${lib.concatStringsSep " " options}"; SyslogIdentifier = "${name}-feed"; Restart = "on-failure"; RestartSec = 30; StartLimitBurst = 100; Nice = -1; }; wantedBy = [ "multi-user.target" ]; } ); in lib.mkIf cfg.enable { assertions = [ { assertion = cfg.dump1090.device.type == "rtlsdr"; message = "not set up for anything but rtlsdr yet"; } { assertion = !(cfg.dump1090.device.type == "rtlsdr" && cfg.dump1090.device.rtlsdr.serial != null && cfg.dump1090.device.rtlsdr.index != null); message = "rtlsdr device cannot be selected by both serial and index"; } { assertion = !(!cfg.dump978.enable && cfg.skyaware978.enable); message = "The skyaware978 service cannot be enabled if the dump978 service is disabled."; } ]; boot.blacklistedKernelModules = [ "dvb_usb_rtl28xxu" ]; services.udev.packages = [ pkgs.rtl-sdr ]; users.groups.plugdev = { }; users.groups.dump1090 = { }; users.users.dump1090 = { isSystemUser = true; group = "dump1090"; extraGroups = [ "plugdev" ]; }; users.groups.piaware = { }; users.users.piaware = { isSystemUser = true; group = "piaware"; }; systemd.services."dump1090-fa" = let rtlsdrOptions = if cfg.dump1090.device.type == "rtlsdr" then ( if cfg.dump1090.device.rtlsdr.serial != null then [ "--device-index" cfg.dump1090.device.rtlsdr.serial ] else [ ] ) ++ ( if cfg.dump1090.device.rtlsdr.index != null then [ "--device-index" (toString cfg.dump1090.device.rtlsdr.index) ] else [ ] ) ++ ( if cfg.dump1090.device.rtlsdr.enableAgc then [ "--enable-agc" ] else [ ] ) ++ ( if cfg.dump1090.device.rtlsdr.frequencyCorrection != null then [ "--ppm" (toString cfg.dump1090.device.rtlsdr.frequencyCorrection) ] else [ ] ) ++ ( if cfg.dump1090.device.rtlsdr.directSamplingMode != null then [ "--direct" (toString cfg.dump1090.device.rtlsdr.directSamplingMode) ] else [ ] ) else [ ]; adaptiveDynamicRangeOptions = if cfg.dump1090.adaptiveDynamicRange.enable then [ "--adaptive-range" ] ++ ( if cfg.dump1090.adaptiveDynamicRange.target != null then [ "--adaptive-range-target" (toString cfg.dump1090.adaptiveDynamicRange.target) ] else [ ] ) ++ ( if cfg.dump1090.adaptiveDynamicRange.alpha != null then [ "--adaptive-range-alpha" (toString cfg.dump1090.adaptiveDynamicRange.alpha) ] else [ ] ) ++ ( if cfg.dump1090.adaptiveDynamicRange.percentile != null then [ "--adaptive-range-percentile" (toString cfg.dump1090.adaptiveDynamicRange.percentile) ] else [ ] ) ++ ( if cfg.dump1090.adaptiveDynamicRange.changeDelay != null then [ "--adaptive-range-change-delay" (toString cfg.dump1090.adaptiveDynamicRange.changeDelay) ] else [ ] ) ++ ( if cfg.dump1090.adaptiveDynamicRange.scanDelay != null then [ "--adaptive-range-scan-delay" (toString cfg.dump1090.adaptiveDynamicRange.scanDelay) ] else [ ] ) ++ ( if cfg.dump1090.adaptiveDynamicRange.rescanDelay != null then [ "--adaptive-range-rescan-delay" (toString cfg.dump1090.adaptiveDynamicRange.rescanDelay) ] else [ ] ) else [ ]; options = [ "--quiet" "--device-type" cfg.dump1090.device.type ] ++ rtlsdrOptions ++ adaptiveDynamicRangeOptions ++ ( if cfg.dump1090.device.gain != null then [ "--gain" (toString cfg.dump1090.device.gain) ] else [ ] ) ++ ( if cfg.dump1090.errorCorrection then [ "--fix" ] else [ ] ) ++ [ "--lat" cfg.latitude "--lon" cfg.longitude ] ++ ( if cfg.dump1090.maxRange != null then [ "--max-range" (toString cfg.dump1090.maxRange) ] else [ ] ) ++ ( if cfg.dump1090.network.raw.input.enable then [ "--net-ri-port" (lib.concatStringsSep "," (map toString cfg.dump1090.network.raw.input.ports)) ] else [ ] ) ++ ( if cfg.dump1090.network.raw.output.enable then [ "--net-ro-port" (lib.concatStringsSep "," (map toString cfg.dump1090.network.raw.output.ports)) ] else [ ] ) ++ ( if cfg.dump1090.network.raw.output.size != null then [ "--net-ro-size" (toString cfg.dump1090.network.raw.output.size) ] else [ ] ) ++ ( if cfg.dump1090.network.raw.output.interval != null then [ "--net-ro-interval" (toString cfg.dump1090.network.raw.output.interval) ] else [ ] ) ++ ( if cfg.dump1090.network.baseStation.enable then [ "--net-sbs-port" (lib.concatStringsSep "," (map toString cfg.dump1090.network.baseStation.ports)) ] else [ ] ) ++ ( if cfg.dump1090.network.beast.input.enable then [ "--net-bi-port" (lib.concatStringsSep "," (map toString cfg.dump1090.network.beast.input.ports)) ] else [ ] ) ++ ( if cfg.dump1090.network.beast.output.enable then [ "--net-bo-port" (lib.concatStringsSep "," (map toString cfg.dump1090.network.beast.output.ports)) ] else [ ] ) ++ [ "--json-location-accuracy" (toString cfg.dump1090.jsonLocationAccuracy) "--write-json" "%t/dump1090-fa" ] ++ cfg.dump1090.extraOptions; in { enable = cfg.dump1090.enable; description = "dump1090 ADS-B receiver (FlightAware customization)"; wants = [ "network.target" ]; after = [ "network.target" ]; unitConfig = { Documentation = "https://flightaware.com/adsb/piaware/"; }; serviceConfig = { Type = "simple"; User = "dump1090"; RuntimeDirectory = "dump1090-fa"; RuntimeDirectoryMode = "0755"; ExecStart = "${self.packages.${pkgs.system}.dump1090-fa}/bin/dump1090 ${lib.concatStringsSep " " options}"; SyslogIdentifier = "dump1090-fa"; Restart = "on-failure"; RestartSec = 30; RestartPreventExitStatus = "64"; Nice = -5; }; wantedBy = [ "multi-user.target" ]; }; users.groups.skyaware = { }; users.users.skyaware = { isSystemUser = true; group = "skyaware"; }; systemd.services."skyaware978" = { enable = cfg.skyaware978.enable; description = "skyaware978 ADS-B UAT web display"; wants = [ "network.target" ]; after = [ "network.target" "dump978-fa.service" ]; unitConfig = { Documentation = "https://flightaware.com/adsb/piaware/"; }; serviceConfig = { Type = "simple"; User = "skyaware"; RuntimeDirectory = "skyaware978"; RuntimeDirectoryMode = "0755"; ExecStart = "${self.packages.${pkgs.system}.dump978-fa}/bin/skyaware978 --lat ${cfg.latitude} --lon ${cfg.longitude} --json-dir %t/skyaware978"; SyslogIdentifier = "skyaware978"; Restart = "on-failure"; RestartSec = 30; RestartPreventExitStatus = "64"; }; wantedBy = [ "dump978-fa.service" ]; }; users.groups.adsbexchange = { }; users.users.adsbexchange = { isSystemUser = true; group = "adsbexchange"; }; systemd.services."adsbexchange-mlat" = let uuidFile = pkgs.writeTextFile { name = "adsbx-uuid"; text = "${cfg.adsbexchange.uuid}"; }; options = [ "--input-type" cfg.adsbexchange.mlat.input.type "--no-udp" "--input-connect" "${cfg.adsbexchange.mlat.input.host}:${toString cfg.adsbexchange.mlat.input.port}" "--server" cfg.adsbexchange.mlat.server "--user" cfg.adsbexchange.username "--lat" cfg.latitude "--lon" cfg.longitude "--alt" (toString cfg.altitude) "--uuid-file" "${uuidFile}" ] ++ ( if cfg.adsbexchange.mlat.privacy then [ "--privacy" ] else [ ] ) ++ ( map (result: "--result ${result}") cfg.adsbexchange.mlat.results ); in { enable = cfg.adsbexchange.mlat.enable; description = "ADS-B Exchange MLAT client."; wants = [ "network.target" ]; after = [ "network.target" ]; unitConfig = { Documentation = "https://github.com/makrsmark/mlat-client/"; }; serviceConfig = { Type = "simple"; User = "adsbexchange"; RuntimeDirectory = "adsbexchange-mlat"; RuntimeDirectoryMode = "0755"; ExecStart = "${self.packages.${pkgs.system}.adsbfi-mlat-client}/bin/mlat-client ${lib.concatStringsSep " " options}"; SyslogIdentifier = "adsbexchange-mlat"; Restart = "on-failure"; RestartSec = 30; Nice = -1; }; wantedBy = [ "multi-user.target" ]; }; systemd.services."adsbexchange-feed" = let uuidFile = pkgs.writeTextFile { name = "adsbx-uuid"; text = "${cfg.adsbexchange.uuid}"; }; options = [ "--quiet" "--net" "--net-only" "--write-json" "%t/adsbexchange-feed" "--net-beast-reduce-interval 0.5" "--net-connector feed1.adsbexchange.com,30004,beast_reduce_out,feed2.adsbexchange.com,64004" "--net-heartbeat 60" "--net-ro-size 1280" "--net-ro-interval 0.2" "--net-ro-port 0" "--net-sbs-port 0" "--net-bi-port ${toString cfg.adsbexchange.feed.beastInputPort}" "--net-bo-port 0" "--net-ri-port 0" "--write-json-every 1" "--lat" cfg.latitude "--lon" cfg.longitude "--max-range 450" "--json-location-accuracy 2" "--range-outline-hours 24" "--uuid-file" "${uuidFile}" "--net-connector 127.0.0.1,30978,uat_in,silent_fail" "--net-connector 127.0.0.1,30005,beast_in,silent_fail" ]; in { enable = cfg.adsbexchange.feed.enable; description = "ADS-B Exchange feeder."; wants = [ "network.target" ]; after = [ "network.target" ]; unitConfig = { Documentation = "https://github.com/wiedehopf/readsb/"; StartLimitIntervalSec = 1; }; serviceConfig = { Type = "simple"; User = "adsbexchange"; RuntimeDirectory = "adsbexchange-feed"; RuntimeDirectoryMode = "0755"; ExecStart = "${self.packages.${pkgs.system}.adsbfi-readsb}/bin/readsb ${lib.concatStringsSep " " options}"; SyslogIdentifier = "adsbexchange-feed"; Restart = "on-failure"; RestartSec = 30; StartLimitBurst = 100; Nice = -1; }; wantedBy = [ "multi-user.target" ]; }; ## ## ADSB.fi ## users.groups.adsbfi = { }; users.users.adsbfi = { isSystemUser = true; group = "adsbexchange"; }; systemd.services."adsbfi-mlat" = let uuidFile = pkgs.writeTextFile { name = "adsbfi-uuid"; text = "${cfg.adsbfi.uuid}"; }; options = [ "--input-type" cfg.adsbfi.mlat.input.type "--no-udp" "--input-connect" "${cfg.adsbfi.mlat.input.host}:${toString cfg.adsbfi.mlat.input.port}" "--server" cfg.adsbfi.mlat.server "--user" cfg.adsbfi.username "--lat" cfg.latitude "--lon" cfg.longitude "--alt" (toString cfg.altitude) "--uuid-file" "${uuidFile}" ] ++ ( if cfg.adsbfi.mlat.privacy then [ "--privacy" ] else [ ] ) ++ ( map (result: "--result ${result}") cfg.adsbfi.mlat.results ); in { enable = cfg.adsbfi.mlat.enable; description = "ADSB.fi MLAT client."; wants = [ "network.target" ]; after = [ "network.target" ]; unitConfig = { Documentation = "https://github.com/makrsmark/mlat-client/"; }; serviceConfig = { Type = "simple"; User = "adsbfi"; RuntimeDirectory = "adsbfi-mlat"; RuntimeDirectoryMode = "0755"; ExecStart = "${self.packages.${pkgs.system}.adsbfi-mlat-client}/bin/mlat-client ${lib.concatStringsSep " " options}"; SyslogIdentifier = "adsbfi-mlat"; Restart = "on-failure"; RestartSec = 30; Nice = -1; }; wantedBy = [ "multi-user.target" ]; }; systemd.services."adsbfi-feed" = let uuidFile = pkgs.writeTextFile { name = "adsbfi-uuid"; text = "${cfg.adsbfi.uuid}"; }; connectors = map ( connector: "--net-connector ${connector}" ) ( map ( connector: ( lib.concatStringsSep "," ( [ connector.primary.host (toString connector.primary.port) connector.protocol ] ++ ( if connector.silentFail then [ "silent_fail" ] else [ ] ) ++ ( if connector.secondary != null then [ connector.secondary.host (toString connector.secondary.port) ] else [ ] ) ) ) ) cfg.adsbfi.feed.connectors ); options = [ "--quiet" "--net" "--net-only" "--write-json" "%t/adsbfi-feed" "--net-beast-reduce-interval 0.5" "--net-heartbeat 60" "--net-ro-size 1280" "--net-ro-interval 0.2" "--net-ro-port 0" "--net-sbs-port 0" "--net-bi-port ${toString cfg.adsbfi.feed.beastInputPort}" "--net-bo-port 0" "--net-ri-port 0" "--write-json-every 1" "--lat" cfg.latitude "--lon" cfg.longitude "--max-range 450" "--json-location-accuracy 2" "--range-outline-hours 24" "--uuid-file" "${uuidFile}" ] ++ connectors; in { enable = cfg.adsbfi.feed.enable; description = "ADSB.fi feeder."; wants = [ "network.target" ]; after = [ "network.target" ]; unitConfig = { Documentation = "https://github.com/widehopf/readsb/"; StartLimitIntervalSec = 1; }; serviceConfig = { Type = "simple"; User = "adsbfi"; RuntimeDirectory = "adsbfi-feed"; RuntimeDirectoryMode = "0755"; ExecStart = "${self.packages.${pkgs.system}.adsbfi-readsb}/bin/readsb ${lib.concatStringsSep " " options}"; SyslogIdentifier = "adsbfi-feed"; Restart = "on-failure"; RestartSec = 30; StartLimitBurst = 100; Nice = -1; }; wantedBy = [ "multi-user.target" ]; }; ## ## theairtraffic.com ## users.groups.theairtraffic = { }; users.users.theairtraffic = { isSystemUser = true; group = "theairtraffic"; }; systemd.services."theairtraffic-mlat" = createMlatService "theairtraffic" cfg.theairtraffic; systemd.services."theairtraffic-feed" = createFeedService "theairtraffic" cfg.theairtraffic; systemd.tmpfiles.rules = [ "d /var/cache/piaware 0755 piaware piaware - -" ]; systemd.services."piaware" = let config = pkgs.writeTextFile { name = "piaware.conf"; text = '' # https://flightaware.com/adsb/piaware/advanced_configuration allow-auto-updates no allow-manual-updates no allow-mlat ${if cfg.piaware.allowMLAT then "yes" else "no"} allow-modeac ${if cfg.piaware.allowModeAC then "yes" else "no"} feeder-id ${cfg.piaware.feederId} ''; }; in { enable = cfg.piaware.enable; description = "FlightAware ADS-B uploader"; wants = [ "network-online.target" ]; after = [ "dump1090-fa.service" "network-online.target" "time-sync.target" ]; unitConfig = { Documentation = "https://flightaware.com/adsb/piaware/"; }; serviceConfig = { Type = "simple"; User = "piaware"; RuntimeDirectory = "piaware"; ExecStart = "${self.packages.${pkgs.system}.piaware}/bin/piaware -p %t/piaware/piaware.pid -plainlog -configfile ${config} -statusfile %t/piaware/status.json"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; Restart = "on-failure"; RestartPreventExitStatus = "4 6"; }; wantedBy = [ "multi-user.target" ]; reloadTriggers = [ "/etc/piaware.conf" ]; }; services.nginx = { enable = true; virtualHosts = { "_" = { root = self.packages.${pkgs.system}.dump1090-fa.html; locations = { "=/status.json" = { alias = "/run/piaware/status.json"; extraConfig = '' add_header Access-Control-Allow-Origin *; ''; }; "/data/" = { alias = "/run/dump1090-fa/"; extraConfig = '' add_header Access-Control-Allow-Origin *; ''; }; "/data-978/" = { root = "/run/skyaware978/"; extraConfig = '' add_header Access-Control-Allow-Origin *; ''; }; }; }; }; }; }