mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-10-31 03:51:58 +01:00
634 lines
26 KiB
Nix
634 lines
26 KiB
Nix
# Copyright © 2025-26 l5yth & contributors
|
|
# Licensed under the Apache License, Version 2.0 (see LICENSE)
|
|
{
|
|
description = "PotatoMesh - A federated, Meshtastic-powered node dashboard";
|
|
|
|
inputs = {
|
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
flake-utils.url = "github:numtide/flake-utils";
|
|
};
|
|
|
|
outputs = { self, nixpkgs, flake-utils }:
|
|
flake-utils.lib.eachDefaultSystem (system:
|
|
let
|
|
# nixpkgs marks rns unfree (the Reticulum License limits fields of use).
|
|
# The flake accepts that one license, for every user, so the ingestor
|
|
# and PROTOCOL=reticulum evaluate (SPEC DP8).
|
|
pkgs = import nixpkgs {
|
|
inherit system;
|
|
config.allowUnfreePredicate = pkg: nixpkgs.lib.getName pkg == "rns";
|
|
};
|
|
|
|
# Ruby 3.4, the version the web image ships and CI tests (SPEC DP5),
|
|
# with a Bundler built against it rather than nixpkgs' default Ruby.
|
|
ruby = pkgs.ruby_3_4;
|
|
bundler = pkgs.bundler.override { inherit ruby; };
|
|
|
|
# Python environment for the ingestor; rns runs PROTOCOL=reticulum.
|
|
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
|
meshtastic
|
|
protobuf
|
|
requests
|
|
rns
|
|
]);
|
|
|
|
# Web app wrapper script
|
|
webApp = pkgs.writeShellApplication {
|
|
name = "potato-mesh-web";
|
|
runtimeInputs = [ ruby bundler pkgs.sqlite pkgs.git pkgs.gnumake pkgs.gcc ];
|
|
text = ''
|
|
if [ -n "''${XDG_DATA_HOME:-}" ]; then
|
|
BASEDIR="$XDG_DATA_HOME"
|
|
else
|
|
BASEDIR="$HOME/.local/share/potato-mesh"
|
|
fi
|
|
WORKDIR="$BASEDIR/web"
|
|
mkdir -p "$WORKDIR"
|
|
|
|
# Copy app files if not present or outdated
|
|
APP_SRC="${./web}"
|
|
DATA_SRC="${./data}"
|
|
if [ ! -f "$WORKDIR/.installed" ] || [ "$APP_SRC" != "$(cat "$WORKDIR/.src_path" 2>/dev/null)" ]; then
|
|
# Copy web app
|
|
cp -rT "$APP_SRC" "$WORKDIR/"
|
|
chmod -R u+w "$WORKDIR"
|
|
# Copy data directory (contains SQL schemas)
|
|
mkdir -p "$BASEDIR/data"
|
|
cp -rT "$DATA_SRC" "$BASEDIR/data/"
|
|
chmod -R u+w "$BASEDIR/data"
|
|
echo "$APP_SRC" > "$WORKDIR/.src_path"
|
|
rm -f "$WORKDIR/.installed"
|
|
fi
|
|
|
|
cd "$WORKDIR"
|
|
|
|
# Install gems if needed
|
|
if [ ! -f ".installed" ]; then
|
|
bundle config set --local path 'vendor/bundle'
|
|
bundle install
|
|
touch .installed
|
|
fi
|
|
|
|
exec bundle exec ruby app.rb -p "''${PORT:-41447}" -o "''${HOST:-0.0.0.0}"
|
|
'';
|
|
};
|
|
|
|
# Ingestor wrapper script
|
|
ingestor = pkgs.writeShellApplication {
|
|
name = "potato-mesh-ingestor";
|
|
runtimeInputs = [ pythonEnv ];
|
|
text = ''
|
|
# The ingestor needs to run from parent directory with data/ folder
|
|
if [ -n "''${XDG_DATA_HOME:-}" ]; then
|
|
BASEDIR="$XDG_DATA_HOME"
|
|
else
|
|
BASEDIR="$HOME/.local/share/potato-mesh"
|
|
fi
|
|
if [ ! -d "$BASEDIR/data" ]; then
|
|
mkdir -p "$BASEDIR"
|
|
cp -rT "${./data}" "$BASEDIR/data/"
|
|
chmod -R u+w "$BASEDIR/data"
|
|
fi
|
|
cd "$BASEDIR"
|
|
exec python -m data.mesh
|
|
'';
|
|
};
|
|
|
|
in {
|
|
packages = {
|
|
web = webApp;
|
|
ingestor = ingestor;
|
|
default = webApp;
|
|
};
|
|
|
|
apps = {
|
|
web = {
|
|
type = "app";
|
|
program = "${webApp}/bin/potato-mesh-web";
|
|
};
|
|
ingestor = {
|
|
type = "app";
|
|
program = "${ingestor}/bin/potato-mesh-ingestor";
|
|
};
|
|
default = self.apps.${system}.web;
|
|
};
|
|
|
|
devShells.default = pkgs.mkShell {
|
|
buildInputs = [
|
|
ruby
|
|
bundler
|
|
pythonEnv
|
|
pkgs.sqlite
|
|
];
|
|
|
|
shellHook = ''
|
|
echo "PotatoMesh development shell"
|
|
echo " - Ruby: $(ruby --version)"
|
|
echo " - Python: $(python --version)"
|
|
echo ""
|
|
echo "To run the web app: cd web && bundle install && ./app.sh"
|
|
echo "To run the ingestor: cd data && python mesh.py"
|
|
'';
|
|
};
|
|
|
|
checks.potato-mesh-nixos = pkgs.testers.nixosTest {
|
|
name = "potato-mesh-data-dir";
|
|
nodes.machine = { lib, ... }: {
|
|
imports = [ self.nixosModules.default ];
|
|
services.potato-mesh = {
|
|
enable = true;
|
|
apiToken = "test-token";
|
|
dataDir = "/var/lib/potato-mesh";
|
|
ingestor.enable = true;
|
|
};
|
|
systemd.services.potato-mesh-ingestor.wantedBy = lib.mkForce [];
|
|
};
|
|
testScript = ''
|
|
machine.start
|
|
machine.succeed("grep -q 'XDG_DATA_HOME=/var/lib/potato-mesh' /etc/systemd/system/potato-mesh-web.service")
|
|
machine.succeed("grep -q 'XDG_DATA_HOME=/var/lib/potato-mesh' /etc/systemd/system/potato-mesh-ingestor.service")
|
|
machine.succeed("grep -q 'WorkingDirectory=/var/lib/potato-mesh' /etc/systemd/system/potato-mesh-web.service")
|
|
machine.succeed("grep -q 'WorkingDirectory=/var/lib/potato-mesh' /etc/systemd/system/potato-mesh-ingestor.service")
|
|
'';
|
|
};
|
|
}
|
|
) // {
|
|
# NixOS module
|
|
nixosModules.default = { config, lib, pkgs, ... }:
|
|
let
|
|
cfg = config.services.potato-mesh;
|
|
in {
|
|
options.services.potato-mesh = {
|
|
enable = lib.mkEnableOption "PotatoMesh web dashboard";
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = self.packages.${pkgs.system}.web;
|
|
description = "The potato-mesh web package to use";
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 41447;
|
|
description = "Port to listen on";
|
|
};
|
|
|
|
host = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "0.0.0.0";
|
|
description = "Host to bind to";
|
|
};
|
|
|
|
apiToken = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Shared secret that authorizes ingestors and API clients making POST requests. Warning: visible in nix store. Prefer apiTokenFile for production.";
|
|
};
|
|
|
|
apiTokenFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = "File containing API_TOKEN=<secret> (recommended for production)";
|
|
};
|
|
|
|
instanceDomain = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Public hostname used for metadata, federation, and generated API links";
|
|
};
|
|
|
|
siteName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "PotatoMesh Demo";
|
|
description = "Title and header displayed in the UI";
|
|
};
|
|
|
|
channel = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "#LongFast";
|
|
description = "Default channel name displayed in the UI";
|
|
};
|
|
|
|
frequency = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "915MHz";
|
|
description = "Default frequency description displayed in the UI";
|
|
};
|
|
|
|
contactLink = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "#potatomesh:dod.ngo";
|
|
description = "Chat link or Matrix alias rendered in the footer and overlays";
|
|
};
|
|
|
|
mapCenter = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "38.761944,-27.090833";
|
|
description = "Latitude and longitude that centre the map on load";
|
|
};
|
|
|
|
mapZoom = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.int;
|
|
default = null;
|
|
description = "Fixed Leaflet zoom applied on first load; disables auto-fit when provided";
|
|
};
|
|
|
|
maxDistance = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 42;
|
|
description = "Maximum distance (km) before node relationships are hidden on the map";
|
|
};
|
|
|
|
debug = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable verbose logging";
|
|
};
|
|
|
|
allowedChannels = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Comma-separated channel names the ingestor accepts";
|
|
};
|
|
|
|
hiddenChannels = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Comma-separated channel names the ingestor will ignore";
|
|
};
|
|
|
|
federation = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Announce instance and crawl peers";
|
|
};
|
|
|
|
private = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Hide chat UI, disable message APIs, and exclude hidden clients from public listings";
|
|
};
|
|
|
|
dataDir = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/var/lib/potato-mesh";
|
|
description = "Directory to store database and configuration";
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "potato-mesh";
|
|
description = "User to run the service as";
|
|
};
|
|
|
|
group = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "potato-mesh";
|
|
description = "Group to run the service as";
|
|
};
|
|
|
|
announcement = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Announcement banner text rendered above the header on every page";
|
|
};
|
|
|
|
ogImageUrl = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Absolute http(s) URL for the social preview image, replacing the generated /og-image.png";
|
|
};
|
|
|
|
ogImageNoSandbox = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Run the Chromium that renders /og-image.png without its sandbox";
|
|
};
|
|
|
|
pagesDir = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Directory of static custom-content pages served at /pages/:slug";
|
|
};
|
|
|
|
promReportIds = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Comma-separated node ids exported as per-node Prometheus gauges";
|
|
};
|
|
|
|
events = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Serve the live-update SSE stream at /api/events; when false clients poll instead";
|
|
};
|
|
|
|
meshtasticPreset = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Meshtastic radio preset for the join strip; overrides the deprecated channel option";
|
|
};
|
|
|
|
meshtasticFreq = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Meshtastic frequency for the join strip; overrides the deprecated frequency option";
|
|
};
|
|
|
|
meshcorePreset = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Meshcore radio preset; the join strip hides the Meshcore line until both Meshcore values are set";
|
|
};
|
|
|
|
meshcoreFreq = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Meshcore frequency; the join strip hides the Meshcore line until both Meshcore values are set";
|
|
};
|
|
|
|
# Ingestor options
|
|
ingestor = {
|
|
enable = lib.mkEnableOption "PotatoMesh Python ingestor";
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = self.packages.${pkgs.system}.ingestor;
|
|
description = "The potato-mesh ingestor package to use";
|
|
};
|
|
|
|
connection = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/dev/ttyACM0";
|
|
description = "Connection target: serial port, IP:port for TCP, or Bluetooth address for BLE";
|
|
};
|
|
|
|
protocol = lib.mkOption {
|
|
type = lib.types.enum [ "meshtastic" "meshcore" "reticulum" ];
|
|
default = "meshtastic";
|
|
description = "Mesh protocol the ingestor reads";
|
|
};
|
|
|
|
transport = lib.mkOption {
|
|
type = lib.types.enum [ "api" "udp" ];
|
|
default = "api";
|
|
description = "Meshtastic transport: api (serial/TCP/BLE) or udp (passive LAN multicast)";
|
|
};
|
|
|
|
nodeId = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Host node id for the ingestor heartbeat; required for transport=udp, and for protocol=reticulum when nothing on its RNS stack announces or two local identities tie";
|
|
};
|
|
|
|
channelIndex = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 0;
|
|
description = "Channel index the activity announcement is sent on; not an ingest filter";
|
|
};
|
|
|
|
energySaving = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Sleep between ingestion cycles instead of holding the connection open";
|
|
};
|
|
|
|
reticulumConfigDir = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "RNS config directory for protocol=reticulum; defaults to ~/.reticulum. Point it at the directory rnsd uses so interface filtering resolves names";
|
|
};
|
|
|
|
reticulumFreq = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Frequency shown for Reticulum nodes; overrides the value read from the RNS config. Also feeds the web footer join strip, which hides the Reticulum line until both Reticulum values are set";
|
|
};
|
|
|
|
reticulumPreset = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Radio preset shown for Reticulum nodes; overrides the value derived from the RNS config's bandwidth/spreading factor/coding rate. Also feeds the web footer join strip, which hides the Reticulum line until both Reticulum values are set";
|
|
};
|
|
|
|
reticulumInterfaces = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "RNS interfaces to ingest from; null ingests RNode interfaces only, * ingests every interface, otherwise comma-separated case-insensitive substrings of interface names";
|
|
};
|
|
|
|
primaryChannelOnly = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Ingest only channel 0. Unconditional when transport=udp; this option affects the api transport only";
|
|
};
|
|
|
|
dropViaMqtt = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Drop Meshtastic packets and nodes carrying the viaMqtt flag (relayed through an MQTT gateway)";
|
|
};
|
|
|
|
primaryChannelKey = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "AQ==";
|
|
description = "Base64 primary-channel PSK (Meshtastic default shown)";
|
|
};
|
|
|
|
primaryChannelName = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Name of channel 0, as shown by `meshtastic --info`. Required when transport=udp with primaryChannelOnly: it resolves the channel hash, and without it UDP mode drops all traffic (fail closed)";
|
|
};
|
|
|
|
meshUdpGroup = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "239.0.0.69,224.0.0.69";
|
|
description = "Comma-separated multicast groups for Meshtastic \"Mesh via UDP\". The default listens on both; set one address to listen on that group only";
|
|
};
|
|
|
|
meshUdpPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 4403;
|
|
description = "Multicast port for Meshtastic \"Mesh via UDP\"";
|
|
};
|
|
|
|
meshcoreSelfTelemetrySeconds = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 3600;
|
|
description = "Seconds between Meshcore host self-telemetry reads over the companion link (no airtime); 0 disables";
|
|
};
|
|
|
|
meshcoreTelemetryPollSeconds = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 300;
|
|
description = "Seconds between Meshcore contact telemetry polls; requires txEnabled since polling transmits. 0 disables on-air polling";
|
|
};
|
|
|
|
meshcoreTelemetryPoll24hExempt = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Comma-separated node ids exempt from the once-per-24h Meshcore contact telemetry cooldown; listed nodes are polled every round-robin rotation. Requires txEnabled";
|
|
};
|
|
|
|
txEnabled = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Allow the ingestor to transmit on the mesh. Off by default: the
|
|
ingestor is a listener unless you opt in. Enables MeshCore on-air
|
|
contact telemetry polling. Companion-link reads to your own radio
|
|
are not transmissions and are unaffected.
|
|
'';
|
|
};
|
|
|
|
txAnnounce = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Broadcast a one-line activity summary on the default channel, at
|
|
most once per 24h and never in the first 24h after start. Requires
|
|
txEnabled. Unsolicited automated traffic on a shared channel, so it
|
|
is off by default.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
users.users.${cfg.user} = {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
home = cfg.dataDir;
|
|
createHome = true;
|
|
};
|
|
|
|
users.groups.${cfg.group} = {};
|
|
|
|
systemd.services.potato-mesh-web = {
|
|
description = "PotatoMesh Web Dashboard";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" ];
|
|
|
|
environment = {
|
|
RACK_ENV = "production";
|
|
APP_ENV = "production";
|
|
PORT = toString cfg.port;
|
|
HOST = cfg.host;
|
|
SITE_NAME = cfg.siteName;
|
|
CHANNEL = cfg.channel;
|
|
FREQUENCY = cfg.frequency;
|
|
CONTACT_LINK = cfg.contactLink;
|
|
MAP_CENTER = cfg.mapCenter;
|
|
MAX_DISTANCE = toString cfg.maxDistance;
|
|
DEBUG = if cfg.debug then "1" else "0";
|
|
FEDERATION = if cfg.federation then "1" else "0";
|
|
PRIVATE = if cfg.private then "1" else "0";
|
|
XDG_DATA_HOME = cfg.dataDir;
|
|
XDG_CONFIG_HOME = "${cfg.dataDir}/config";
|
|
EVENTS = if cfg.events then "1" else "0";
|
|
} // lib.optionalAttrs (cfg.announcement != null) {
|
|
ANNOUNCEMENT = cfg.announcement;
|
|
} // lib.optionalAttrs (cfg.ogImageUrl != null) {
|
|
OG_IMAGE_URL = cfg.ogImageUrl;
|
|
} // lib.optionalAttrs cfg.ogImageNoSandbox {
|
|
OG_IMAGE_NO_SANDBOX = "1";
|
|
} // lib.optionalAttrs (cfg.pagesDir != null) {
|
|
PAGES_DIR = cfg.pagesDir;
|
|
} // lib.optionalAttrs (cfg.promReportIds != null) {
|
|
PROM_REPORT_IDS = cfg.promReportIds;
|
|
} // lib.optionalAttrs (cfg.meshtasticPreset != null) {
|
|
MESHTASTIC_PRESET = cfg.meshtasticPreset;
|
|
} // lib.optionalAttrs (cfg.meshtasticFreq != null) {
|
|
MESHTASTIC_FREQ = cfg.meshtasticFreq;
|
|
} // lib.optionalAttrs (cfg.meshcorePreset != null) {
|
|
MESHCORE_PRESET = cfg.meshcorePreset;
|
|
} // lib.optionalAttrs (cfg.meshcoreFreq != null) {
|
|
MESHCORE_FREQ = cfg.meshcoreFreq;
|
|
} // lib.optionalAttrs (cfg.ingestor.reticulumPreset != null) {
|
|
RETICULUM_PRESET = cfg.ingestor.reticulumPreset;
|
|
} // lib.optionalAttrs (cfg.ingestor.reticulumFreq != null) {
|
|
RETICULUM_FREQ = cfg.ingestor.reticulumFreq;
|
|
} // lib.optionalAttrs (cfg.instanceDomain != null) {
|
|
INSTANCE_DOMAIN = cfg.instanceDomain;
|
|
} // lib.optionalAttrs (cfg.mapZoom != null) {
|
|
MAP_ZOOM = toString cfg.mapZoom;
|
|
} // lib.optionalAttrs (cfg.apiToken != null) {
|
|
API_TOKEN = cfg.apiToken;
|
|
};
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
WorkingDirectory = cfg.dataDir;
|
|
ExecStart = "${cfg.package}/bin/potato-mesh-web";
|
|
Restart = "always";
|
|
RestartSec = 5;
|
|
} // lib.optionalAttrs (cfg.apiTokenFile != null) {
|
|
EnvironmentFile = cfg.apiTokenFile;
|
|
};
|
|
};
|
|
|
|
systemd.services.potato-mesh-ingestor = lib.mkIf cfg.ingestor.enable {
|
|
description = "PotatoMesh Python Ingestor";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" "potato-mesh-web.service" ];
|
|
requires = [ "potato-mesh-web.service" ];
|
|
|
|
environment = {
|
|
INSTANCE_DOMAIN = "http://127.0.0.1:${toString cfg.port}";
|
|
CONNECTION = cfg.ingestor.connection;
|
|
DEBUG = if cfg.debug then "1" else "0";
|
|
XDG_DATA_HOME = cfg.dataDir;
|
|
TX_ENABLED = if cfg.ingestor.txEnabled then "1" else "0";
|
|
TX_ANNOUNCE = if cfg.ingestor.txAnnounce then "1" else "0";
|
|
PROTOCOL = cfg.ingestor.protocol;
|
|
TRANSPORT = cfg.ingestor.transport;
|
|
CHANNEL_INDEX = toString cfg.ingestor.channelIndex;
|
|
ENERGY_SAVING = if cfg.ingestor.energySaving then "1" else "0";
|
|
MESHCORE_SELF_TELEMETRY_SECONDS = toString cfg.ingestor.meshcoreSelfTelemetrySeconds;
|
|
MESHCORE_TELEMETRY_POLL_SECONDS = toString cfg.ingestor.meshcoreTelemetryPollSeconds;
|
|
MESHCORE_TELEMETRY_POLL_24H_EXEMPT = cfg.ingestor.meshcoreTelemetryPoll24hExempt;
|
|
PRIMARY_CHANNEL_ONLY = if cfg.ingestor.primaryChannelOnly then "1" else "0";
|
|
DROP_VIA_MQTT = if cfg.ingestor.dropViaMqtt then "1" else "0";
|
|
PRIMARY_CHANNEL_KEY = cfg.ingestor.primaryChannelKey;
|
|
MESH_UDP_GROUP = cfg.ingestor.meshUdpGroup;
|
|
MESH_UDP_PORT = toString cfg.ingestor.meshUdpPort;
|
|
} // lib.optionalAttrs (cfg.ingestor.primaryChannelName != null) {
|
|
PRIMARY_CHANNEL_NAME = cfg.ingestor.primaryChannelName;
|
|
} // lib.optionalAttrs (cfg.ingestor.nodeId != null) {
|
|
INGESTOR_NODE_ID = cfg.ingestor.nodeId;
|
|
} // lib.optionalAttrs (cfg.ingestor.reticulumConfigDir != null) {
|
|
RETICULUM_CONFIG_DIR = cfg.ingestor.reticulumConfigDir;
|
|
} // lib.optionalAttrs (cfg.ingestor.reticulumFreq != null) {
|
|
RETICULUM_FREQ = cfg.ingestor.reticulumFreq;
|
|
} // lib.optionalAttrs (cfg.ingestor.reticulumPreset != null) {
|
|
RETICULUM_PRESET = cfg.ingestor.reticulumPreset;
|
|
} // lib.optionalAttrs (cfg.ingestor.reticulumInterfaces != null) {
|
|
RETICULUM_INTERFACES = cfg.ingestor.reticulumInterfaces;
|
|
} // lib.optionalAttrs (cfg.allowedChannels != null) {
|
|
ALLOWED_CHANNELS = cfg.allowedChannels;
|
|
} // lib.optionalAttrs (cfg.hiddenChannels != null) {
|
|
HIDDEN_CHANNELS = cfg.hiddenChannels;
|
|
} // lib.optionalAttrs (cfg.apiToken != null) {
|
|
API_TOKEN = cfg.apiToken;
|
|
};
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
WorkingDirectory = cfg.dataDir;
|
|
ExecStart = "${cfg.ingestor.package}/bin/potato-mesh-ingestor";
|
|
Restart = "always";
|
|
RestartSec = 10;
|
|
} // lib.optionalAttrs (cfg.apiTokenFile != null) {
|
|
EnvironmentFile = cfg.apiTokenFile;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|