diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 4fee9fb92e5e7..1c19a0d57a4dd 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -10,6 +10,10 @@ +- [Meshtastic](https://meshtastic.org), an open-source, off-grid, decentralised mesh network + designed to run on affordable, low-power devices. Available as [services.meshtasticd] + (#opt-services.meshtasticd.enable). + - [knot-resolver](https://www.knot-resolver.cz/) in version 6. Available as `services.knot-resolver`. A module for knot-resolver 5 was already available as `services.kresd`. - [ImmichFrame](https://immichframe.dev/), display your photos from Immich as a digital photo frame. Available as `services.immichframe`. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 430872242d57a..8d233529326e7 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1247,6 +1247,7 @@ ./services/networking/lxd-image-server.nix ./services/networking/magic-wormhole-mailbox-server.nix ./services/networking/matterbridge.nix + ./services/networking/meshtasticd.nix ./services/networking/microsocks.nix ./services/networking/mihomo.nix ./services/networking/minidlna.nix diff --git a/nixos/modules/services/networking/meshtastic.md b/nixos/modules/services/networking/meshtastic.md new file mode 100644 index 0000000000000..d16c59fe3917e --- /dev/null +++ b/nixos/modules/services/networking/meshtastic.md @@ -0,0 +1,57 @@ +# Meshtasticd {#module-services-meshtasticd} + +[Meshtasticd](https://meshtastic.org/) daemon. + +Meshtastic is an open-source, off-grid, decentralised mesh network designed to +run on affordable, low-power devices. + +Meshtastic is a project that enables you to use inexpensive LoRa radios as a +long range off-grid communication platform in areas without existing or reliable +communications infrastructure. This project is 100% community driven and open +source! + +## Quickstart {#module-services-meshtasticd-quickstart} + +A minimal configuration: + +```nix +{ + services.meshtasticd = { + enable = true; + port = 4403; + settings = { + Lora = { + Module = "auto"; + }; + Webserver = { + Port = 9443; + RootPath = pkgs.meshtastic-web; + }; + General = { + MaxNodes = 200; + MaxMessageQueue = 100; + MACAddressSource = "eth0"; + }; + }; + }; +} +``` + +By default Meshtasticd listens on all network interfaces. The example above +binds the daemon to port `4403` and the web UI to `9443`. This module +intentionally does not configure an reverse proxy for you, keeping the module +focused on the Meshtastic service itself. If you need to restrict access, use +firewall rules or put the web UI behind a reverse proxy (e.g.: Caddy, Nginx) +that binds to `127.0.0.1` and exposes only the proxy. This approach leaves proxy +choice and TLS configuration to the operator while documenting how to securely +expose the web UI when required. + +## Configuration {#module-services-meshtasticd-config} + +All available configuration directives are documented in the +[standard Meshtastic configuration file](https://github.com/meshtastic/firmware/blob/develop/bin/config-dist.yaml). + +The service uses a dedicated user and group account (`meshtasticd`) by default. +If you override the service user, ensure it is a member of the `spi` and `gpio` +groups so it can access the required hardware devices, as mandated by +Meshtastic’s default `udev` rules. diff --git a/nixos/modules/services/networking/meshtasticd.nix b/nixos/modules/services/networking/meshtasticd.nix new file mode 100644 index 0000000000000..d72affb8b6df0 --- /dev/null +++ b/nixos/modules/services/networking/meshtasticd.nix @@ -0,0 +1,124 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.meshtasticd; + format = pkgs.formats.yaml { }; + configFile = format.generate "config.yaml" cfg.settings; +in +{ + options.services.meshtasticd = { + enable = lib.mkEnableOption "Meshtastic daemon"; + package = lib.mkPackageOption pkgs "meshtasticd" { }; + + user = lib.mkOption { + default = "meshtasticd"; + description = "User meshtasticd runs as."; + type = lib.types.str; + }; + + group = lib.mkOption { + default = "meshtasticd"; + description = "Group meshtasticd runs as."; + type = lib.types.str; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 4403; + description = "Port to listen on"; + }; + + settings = lib.mkOption { + type = format.type; + example = lib.literalExpression '' + Lora = { + Module = "auto"; + }; + Webserver = { + Port = 9443; + RootPath = pkgs.meshtastic-web; + }; + General = { + MaxNodes = 200; + MaxMessageQueue = 100; + MACAddressSource = "eth0"; + }; + ''; + description = '' + The Meshtastic configuration file. + + An example of configuration can be found at + ''; + }; + + dataDir = lib.mkOption { + default = "/var/lib/meshtasticd"; + type = lib.types.path; + description = '' + The data directory. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + # Creation of the `meshtasticd` privilege user. + users = { + users = lib.mkIf (cfg.user == "meshtasticd") { + meshtasticd = { + home = cfg.dataDir; + description = "meshtasticd-daemon privilege user"; + group = cfg.group; + isSystemUser = true; + extraGroups = [ + "spi" + "gpio" + ]; + }; + }; + groups = lib.mkIf (cfg.group == "meshtasticd") { + meshtasticd = { }; + # These groups are required for udev rules to work properly. + spi = { }; + gpio = { }; + }; + }; + + # The `meshtasticd` package provides udev rules. + services.udev.packages = [ + cfg.package + ]; + + # Creation of the `meshtasticd` service. + # Based on the official meshtasticd service file: https://github.com/meshtastic/firmware/blob/develop/bin/meshtasticd.service + systemd.services.meshtasticd = { + description = "Meshtastic Native Daemon"; + after = [ + "network-online.target" + "network.target" + ]; + wants = [ + "network-online.target" + "network.target" + ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + Type = "simple"; + StateDirectory = "meshtasticd"; + AmbientCapabilities = [ + "CAP_NET_BIND_SERVICE" + ]; + ExecStart = "${lib.getExe cfg.package} --port=${builtins.toString cfg.port} --fsdir=${cfg.dataDir} --config=${configFile} --verbose"; + Restart = "always"; + RestartSec = "3"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index ba951d1605913..988cf1c0c76c6 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -939,6 +939,7 @@ in meilisearch = runTest ./meilisearch.nix; memcached = runTest ./memcached.nix; merecat = runTest ./merecat.nix; + meshtasticd = runTest ./networking/meshtasticd.nix; metabase = runTest ./metabase.nix; mihomo = runTest ./mihomo.nix; mimir = runTest ./mimir.nix; diff --git a/nixos/tests/networking/meshtasticd.nix b/nixos/tests/networking/meshtasticd.nix new file mode 100644 index 0000000000000..121886e7910a0 --- /dev/null +++ b/nixos/tests/networking/meshtasticd.nix @@ -0,0 +1,45 @@ +{ + lib, + pkgs, + ... +}: +let + mainPort = 9445; + webPort = 9446; +in +{ + name = "meshtasticd"; + meta.maintainers = [ lib.maintainers.drupol ]; + + nodes.machine = { + services.meshtasticd = { + enable = true; + port = mainPort; + + settings = { + Lora = { + Module = "sim"; + DIO2_AS_RF_SWITCH = false; + spiSpeed = "2000000"; + }; + Webserver = { + Port = webPort; + RootPath = pkgs.meshtastic-web; + }; + General = { + MaxNodes = 200; + MaxMessageQueue = 100; + MACAddressSource = "eth0"; + }; + }; + }; + }; + + testScript = '' + with subtest("Test meshtasticd service"): + machine.wait_for_unit("meshtasticd.service") + machine.wait_for_open_port(${builtins.toString mainPort}) + machine.wait_for_open_port(${builtins.toString webPort}) + machine.succeed("curl -fvvv -Ls http://localhost:${builtins.toString webPort} | grep -q 'Meshtastic Web Client'") + ''; +} diff --git a/pkgs/by-name/me/meshtastic-web/package.nix b/pkgs/by-name/me/meshtastic-web/package.nix new file mode 100644 index 0000000000000..ff09475e3425b --- /dev/null +++ b/pkgs/by-name/me/meshtastic-web/package.nix @@ -0,0 +1,41 @@ +{ + stdenv, + lib, + fetchurl, +}: +stdenv.mkDerivation (finalAttrs: { + pname = "meshtastic-web"; + version = "2.6.7"; + + src = fetchurl { + url = "https://github.com/meshtastic/web/releases/download/v${finalAttrs.version}/build.tar"; + hash = "sha256-o09DYKBIZUOmmN4g3lM1V0kudjq0Wfwn/OqV0ElRRO0="; + }; + + sourceRoot = "."; + + buildPhase = '' + runHook preBuild + + gzip -dr . + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out + cp -ar . $out/ + + runHook postInstall + ''; + + meta = { + description = "Meshtastic Web Client/JS Monorepo"; + homepage = "https://github.com/meshtastic/web"; + license = lib.licenses.gpl3Plus; + platforms = lib.platforms.all; + maintainers = with lib.maintainers; [ drupol ]; + }; +}) diff --git a/pkgs/by-name/me/meshtastic/package.nix b/pkgs/by-name/me/meshtastic/package.nix new file mode 100644 index 0000000000000..a30cef9d3dd18 --- /dev/null +++ b/pkgs/by-name/me/meshtastic/package.nix @@ -0,0 +1,7 @@ +{ lib, python3Packages }: + +python3Packages.toPythonApplication ( + python3Packages.meshtastic.overridePythonAttrs (prev: { + dependencies = prev.dependencies ++ lib.concatAttrValues prev.optional-dependencies; + }) +) diff --git a/pkgs/by-name/me/meshtasticd/package.nix b/pkgs/by-name/me/meshtasticd/package.nix index ff15a311da206..c712324f9b652 100644 --- a/pkgs/by-name/me/meshtasticd/package.nix +++ b/pkgs/by-name/me/meshtasticd/package.nix @@ -1,9 +1,13 @@ { - lib, stdenv, - fetchurl, - autoPatchelfHook, - dpkg, + lib, + fetchFromGitHub, + fetchzip, + libarchive, + pkg-config, + platformio-core, + writableTmpDirAsHomeHook, + bluez, i2c-tools, libX11, libgpiod_1, @@ -11,65 +15,141 @@ libusb1, libuv, libxkbcommon, - udevCheckHook, ulfius, + openssl, + gnutls, + jansson, + zlib, + libmicrohttpd, + orcania, + yder, yaml-cpp, + udevCheckHook, + versionCheckHook, + makeBinaryWrapper, + python3Packages, + enableDefaultConfig ? false, + meshtastic-web, # Only used when `enableDefaultConfig` is set to `true`. }: + +assert builtins.isBool enableDefaultConfig; + +let + version = "2.7.16.a597230"; + + platformio-deps-native = fetchzip { + url = "https://github.com/meshtastic/firmware/releases/download/v${version}/platformio-deps-native-tft-${version}.zip"; + hash = "sha256-Jo7e6zsCaiJs6NyIRmD6BWJFwbs0xVlUih206ePUpwk="; + }; +in stdenv.mkDerivation (finalAttrs: { pname = "meshtasticd"; - version = "2.6.11.25"; + inherit version; - src = fetchurl { - url = "https://download.opensuse.org/repositories/network:/Meshtastic:/beta/Debian_12/amd64/meshtasticd_${finalAttrs.version}~obs60ec05e~beta_amd64.deb"; - hash = "sha256-7JCv+1YgsCLwboGE/2f+8iyLLoUsKn3YdJ9Atnfj7Zw="; + src = fetchFromGitHub { + owner = "meshtastic"; + repo = "firmware"; + hash = "sha256-oU3Z8qjBNeNGPGT74VStAPHgsGqsQJKngHJR6m2CBa0="; + tag = "v${finalAttrs.version}"; + fetchSubmodules = true; }; + strictDeps = true; + nativeBuildInputs = [ - autoPatchelfHook - dpkg + libarchive + pkg-config + # This has been advised by the Meshtastic's developer. + # Without it, it will try to install grpcio-tools by itself and fail. + (platformio-core.overridePythonAttrs (oldAttrs: { + dependencies = oldAttrs.dependencies ++ [ + python3Packages.grpcio-tools + ]; + })) + writableTmpDirAsHomeHook + makeBinaryWrapper ]; - dontConfigure = true; - dontBuild = true; - - strictDeps = true; - buildInputs = [ + bluez + gnutls i2c-tools + jansson libX11 libgpiod_1 libinput + libmicrohttpd libusb1 libuv libxkbcommon + openssl + orcania ulfius yaml-cpp + yder + zlib ]; - autoPatchelfIgnoreMissingDeps = [ - "libyaml-cpp.so.0.7" - ]; + preConfigure = '' + mkdir -p platformio-deps-native + cp -ar ${platformio-deps-native}/. platformio-deps-native + chmod +w -R platformio-deps-native + + export PLATFORMIO_CORE_DIR=platformio-deps-native/core + export PLATFORMIO_LIBDEPS_DIR=platformio-deps-native/libdeps + export PLATFORMIO_PACKAGES_DIR=platformio-deps-native/packages + ''; + + buildPhase = '' + runHook preBuild + + platformio run --environment native-tft + + runHook postBuild + ''; installPhase = '' runHook preInstall - mkdir -p {$out,$out/bin} - cp -r {usr,lib} $out/ - - patchelf --replace-needed libyaml-cpp.so.0.7 libyaml-cpp.so.0.8 $out/usr/bin/meshtasticd + install -d $out/share/meshtasticd/config.d + install -d $out/share/meshtasticd/available.d + cp -R bin/config.d/* $out/share/meshtasticd/available.d - ln -s $out/usr/bin/meshtasticd $out/bin/meshtasticd + install -Dm644 bin/org.meshtastic.meshtasticd.svg -t $out/share/icons/hicolor/scalable/apps/ + install -Dm644 bin/org.meshtastic.meshtasticd.desktop -t $out/share/applications/ + install -Dm755 .pio/build/native-tft/program $out/bin/meshtasticd - substituteInPlace $out/lib/systemd/system/meshtasticd.service \ - --replace-fail "/usr/bin/meshtasticd" "$out/bin/meshtasticd" \ - --replace-fail 'User=meshtasticd' 'DynamicUser=yes' \ - --replace-fail 'Group=meshtasticd' "" + install -Dm644 bin/99-meshtasticd-udev.rules -t $out/etc/udev/rules.d + '' + + lib.optionalString enableDefaultConfig '' + install -Dm644 bin/config-dist.yaml $out/share/meshtasticd/config.yaml + substituteInPlace $out/share/meshtasticd/config.yaml \ + --replace-fail "/etc/meshtasticd/config.d" "$out/share/meshtasticd/config.d" \ + --replace-fail "/etc/meshtasticd/available.d" "$out/share/meshtasticd/available.d" + wrapProgram $out/bin/meshtasticd \ + --add-flags "-c $out/share/meshtasticd/config.yaml" + install -Dm644 bin/config.d/MUI/X11_480x480.yaml $out/share/meshtasticd/config.d/MUI.yaml + substituteInPlace $out/share/meshtasticd/config.yaml \ + --replace-fail "/usr/share/meshtasticd/web" "${meshtastic-web}" + install -d $out/share/meshtasticd/maps + for file in pio/libdeps/native-tft/meshtastic-device-ui/maps/*.zip; do + bsdtar -xf "$file" --no-same-owner --strip-components=1 -C $out/share/meshtasticd/maps; + done + '' + + '' runHook postInstall ''; doInstallCheck = true; - nativeInstallCheckInputs = [ udevCheckHook ]; + nativeInstallCheckInputs = [ + udevCheckHook + versionCheckHook + ]; + versionCheckProgramArg = "--version"; + preVersionCheck = '' + version="${lib.versions.major finalAttrs.version}.${lib.versions.minor finalAttrs.version}.${lib.versions.patch finalAttrs.version}" + ''; meta = { description = "Meshtastic daemon for communicating with Meshtastic devices"; @@ -77,12 +157,13 @@ stdenv.mkDerivation (finalAttrs: { This package has `udev` rules installed as part of the package. Add `services.udev.packages = [ pkgs.meshtasticd ]` into your NixOS configuration to enable them. + + To enable the default configuration, set the `enableDefaultConfig` parameter to true. ''; homepage = "https://github.com/meshtastic/firmware"; mainProgram = "meshtasticd"; license = lib.licenses.gpl3Plus; - platforms = [ "x86_64-linux" ]; - sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; + platforms = lib.platforms.linux; maintainers = with lib.maintainers; [ drupol ]; }; })