mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-23 22:32:29 +00:00
Add Nix flake with NixOS module and modernize Python packaging
This commit adds comprehensive Nix/NixOS support and modernizes the
Python packaging structure to enable declarative system-wide deployments.
Added complete Nix flake infrastructure in nix/:
- packages.nix: Package definition with all dependencies
- nixos-module.nix: NixOS module for declarative service configuration
- nixos-test.nix: Comprehensive NixOS VM tests (basic + web viewer)
- shell.nix: Development shell with all build tools
The NixOS module (services.meshcore-bot) provides:
- Automatic user/group creation (meshcore-bot:meshcore-bot)
- Serial port access (dialout group membership)
- Systemd service with security hardening
- Automatic directory management via StateDirectory/LogsDirectory
- INI config generation from Nix attribute sets
- Sensible defaults for system paths
Example NixOS configuration:
services.meshcore-bot = {
enable = true;
settings = {
Connection.connection_type = "serial";
Connection.serial_port = "/dev/ttyUSB0";
Bot.bot_name = "MyBot";
};
};
Migrated from requirements.txt to pyproject.toml:
- Defined proper package metadata and dependencies
- Created entry points: meshcore-bot, meshcore-viewer
- Enables standard 'pip install -e .' workflow
- Maintains compatibility with existing setups
Changes to existing code are minimal and only where necessary:
1. **Added --config parameter** (meshcore_bot.py, app.py)
- Wrapped main logic in main() function for entry point
- Added argparse to accept --config parameter
- Required for NixOS to pass generated config from /nix/store
- Backwards compatible: defaults to 'config.ini' in current directory
- No changes to core bot logic
2. **Fixed web viewer subprocess spawning** (modules/web_viewer/integration.py)
- Changed from sys.executable to 'meshcore-viewer' entry point
- Ensures Nix wrapper script sets up correct PYTHONPATH
- Resolves Flask module import issues in Nix environment
- No functional change for traditional installations
Note: The dev branch's resolve_path() utility already handles both
relative and absolute paths correctly, making it compatible with
NixOS system paths (/var/lib, /var/log) without additional changes.
All NixOS VM tests passing:
- nixos-module-basic: Service startup, file creation, TCP connection
- nixos-module-webviewer: Flask available, port listening, HTTP responses
No breaking changes to existing workflows. The bot continues to work
exactly as before when run with traditional Python methods.
This commit is contained in:
1 parent
578fa2caa4
commit
a6c56ecd3c
9 files changed
+808
-3
No files matched your search
Generated
+184
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765835352,
|
||||
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "flake-parts",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"meshcore-cli": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766078977,
|
||||
"narHash": "sha256-/WsVYItx9Tp7uU4l7BFsI63Cs5i+vqZUQrjWeutk3io=",
|
||||
"owner": "meshcore-dev",
|
||||
"repo": "meshcore-cli",
|
||||
"rev": "08f655b65c257537a6a67a1976911d307a9cb7ad",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "meshcore-dev",
|
||||
"repo": "meshcore-cli",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766309749,
|
||||
"narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1765674936,
|
||||
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1761236834,
|
||||
"narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d5faa84122bc0a1fd5d378492efce4e289f8eac1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"flake-utils": "flake-utils",
|
||||
"meshcore-cli": "meshcore-cli",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766000401,
|
||||
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
description = "A Python bot that connects to MeshCore mesh networks via serial port, BLE, or TCP/IP";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
meshcore-cli = {
|
||||
url = "github:meshcore-dev/meshcore-cli";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs @ {
|
||||
flake-parts,
|
||||
self,
|
||||
...
|
||||
}:
|
||||
flake-parts.lib.mkFlake {inherit inputs;} {
|
||||
imports = [
|
||||
# inputs.treefmt-nix.flakeModule
|
||||
./nix/packages.nix
|
||||
./nix/shell.nix
|
||||
./nix/nixos-test.nix
|
||||
./nix/nixos-module.nix
|
||||
];
|
||||
systems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"];
|
||||
};
|
||||
}
|
||||
+18
-2
@@ -4,6 +4,7 @@ MeshCore Bot using the meshcore-cli and meshcore.py packages
|
||||
Uses a modular structure for command creation and organization
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import signal
|
||||
import sys
|
||||
@@ -12,8 +13,19 @@ import sys
|
||||
from modules.core import MeshCoreBot
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = MeshCoreBot()
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="MeshCore Bot - Mesh network bot for MeshCore devices"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default="config.ini",
|
||||
help="Path to configuration file (default: config.ini)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
bot = MeshCoreBot(config_file=args.config)
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("\nShutting down...")
|
||||
@@ -32,4 +44,8 @@ if __name__ == "__main__":
|
||||
asyncio.run(bot.stop())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
||||
@@ -3891,7 +3891,8 @@ class BotDataViewer:
|
||||
self.logger.error(f"Error running web viewer: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == '__main__':
|
||||
def main():
|
||||
"""Entry point for the meshcore-viewer command"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='MeshCore Bot Data Viewer')
|
||||
@@ -3903,3 +3904,6 @@ if __name__ == '__main__':
|
||||
|
||||
viewer = BotDataViewer()
|
||||
viewer.run(host=args.host, port=args.port, debug=args.debug)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
self,
|
||||
inputs,
|
||||
...
|
||||
}: {
|
||||
flake.nixosModules.default = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
cfg = config.services.meshcore-bot;
|
||||
|
||||
# Helper function to convert nested attrset to INI format
|
||||
toINI = lib.generators.toINI {
|
||||
mkKeyValue = lib.generators.mkKeyValueDefault {
|
||||
mkValueString = v:
|
||||
if builtins.isBool v
|
||||
then
|
||||
(
|
||||
if v
|
||||
then "true"
|
||||
else "false"
|
||||
)
|
||||
else if builtins.isString v
|
||||
then v
|
||||
else if builtins.isInt v
|
||||
then toString v
|
||||
else if builtins.isFloat v
|
||||
then toString v
|
||||
else if builtins.isList v
|
||||
then lib.concatStringsSep "," v
|
||||
else throw "Unsupported type for INI value: ${builtins.typeOf v}";
|
||||
} " = ";
|
||||
};
|
||||
|
||||
# Generate config file from settings
|
||||
configFile = pkgs.writeText "meshcore-bot-config.ini" (toINI cfg.settings);
|
||||
in {
|
||||
options.services.meshcore-bot = {
|
||||
enable = lib.mkEnableOption "MeshCore Bot service";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
defaultText = lib.literalExpression "inputs.self.packages.\${pkgs.stdenv.hostPlatform.system}.meshcore-bot";
|
||||
description = "The meshcore-bot package to use.";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.attrsOf lib.types.anything);
|
||||
default = {};
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
Connection = {
|
||||
connection_type = "serial";
|
||||
serial_port = "/dev/ttyUSB0";
|
||||
timeout = 30;
|
||||
};
|
||||
Bot = {
|
||||
bot_name = "MeshCoreBot";
|
||||
enabled = true;
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Configuration for meshcore-bot in INI format.
|
||||
'';
|
||||
apply = userSettings: let
|
||||
# Default settings that will be merged with user settings
|
||||
defaults = {
|
||||
Bot = {
|
||||
db_path = "${cfg.dataDir}/meshcore-bot.db";
|
||||
};
|
||||
Localization = {
|
||||
translation_path = "${cfg.package}/share/meshcore-bot/translations";
|
||||
};
|
||||
Logging = {
|
||||
log_file = "/var/log/meshcore-bot/meshcore-bot.log";
|
||||
colored_output = false;
|
||||
};
|
||||
Security = {
|
||||
allow_absolute_paths = true;
|
||||
};
|
||||
Web_Viewer = {
|
||||
enabled = false;
|
||||
};
|
||||
};
|
||||
in
|
||||
# Recursively merge defaults with user settings, user settings take precedence
|
||||
lib.recursiveUpdate defaults userSettings;
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "/var/lib/meshcore-bot";
|
||||
description = ''
|
||||
Directory where meshcore-bot stores its database and persistent data.
|
||||
This directory will be created automatically with appropriate permissions.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# Create user and group
|
||||
users.users.meshcore-bot = {
|
||||
isSystemUser = true;
|
||||
group = "meshcore-bot";
|
||||
description = "MeshCore Bot service user";
|
||||
home = cfg.dataDir;
|
||||
# Grant access to serial ports
|
||||
extraGroups = ["dialout"];
|
||||
};
|
||||
|
||||
users.groups.meshcore-bot = {};
|
||||
|
||||
# Create systemd service
|
||||
systemd.services.meshcore-bot = {
|
||||
description = "MeshCore Bot - Mesh network bot for MeshCore devices";
|
||||
after = ["network.target"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
|
||||
# Service configuration
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = "meshcore-bot";
|
||||
Group = "meshcore-bot";
|
||||
|
||||
# Working directory - systemd will create it automatically
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
|
||||
# StateDirectory creates /var/lib/meshcore-bot with correct ownership
|
||||
# This is the NixOS/systemd way to manage service state directories
|
||||
StateDirectory = "meshcore-bot";
|
||||
StateDirectoryMode = "0750";
|
||||
|
||||
# LogsDirectory creates /var/log/meshcore-bot with correct ownership
|
||||
LogsDirectory = "meshcore-bot";
|
||||
LogsDirectoryMode = "0750";
|
||||
|
||||
# Start command
|
||||
ExecStart = "${cfg.package}/bin/meshcore-bot --config ${configFile}";
|
||||
|
||||
# Restart policy
|
||||
Restart = "on-failure";
|
||||
RestartSec = "10s";
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
|
||||
# Additional hardening
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6" "AF_BLUETOOTH"];
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RemoveIPC = true;
|
||||
PrivateMounts = true;
|
||||
};
|
||||
|
||||
# Environment
|
||||
environment = {
|
||||
PYTHONUNBUFFERED = "1";
|
||||
};
|
||||
};
|
||||
|
||||
# Install package data (translations, templates, etc.)
|
||||
environment.systemPackages = [cfg.package];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
{
|
||||
self,
|
||||
inputs,
|
||||
...
|
||||
}: {
|
||||
perSystem = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
system,
|
||||
...
|
||||
}: {
|
||||
checks = {
|
||||
nixos-module-basic = pkgs.testers.nixosTest {
|
||||
name = "meshcore-bot-basic";
|
||||
|
||||
nodes.machine = {
|
||||
imports = [self.nixosModules.default];
|
||||
|
||||
services.meshcore-bot = {
|
||||
enable = true;
|
||||
settings = {
|
||||
Connection = {
|
||||
connection_type = "tcp";
|
||||
hostname = "localhost";
|
||||
tcp_port = 5000;
|
||||
timeout = 30;
|
||||
};
|
||||
Bot = {
|
||||
bot_name = "TestBot";
|
||||
enabled = true;
|
||||
};
|
||||
Channels = {
|
||||
monitor_channels = ["general" "test"];
|
||||
respond_to_dms = true;
|
||||
};
|
||||
Logging = {
|
||||
log_level = "DEBUG";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("meshcore-bot.service")
|
||||
|
||||
# Give it a few seconds to create files and attempt connection
|
||||
machine.sleep(15)
|
||||
|
||||
|
||||
# Check if database file is created in the correct location
|
||||
print("\n=== File Checks ===")
|
||||
machine.succeed("test -f /var/lib/meshcore-bot/meshcore-bot.db")
|
||||
print("✓ Database file created at /var/lib/meshcore-bot/meshcore-bot.db")
|
||||
|
||||
# Check if log file is created in the correct location
|
||||
machine.succeed("test -f /var/log/meshcore-bot/meshcore-bot.log")
|
||||
print("✓ Log file created at /var/log/meshcore-bot/meshcore-bot.log")
|
||||
|
||||
|
||||
# Check if config was generated
|
||||
machine.succeed("test -f /nix/store/*-meshcore-bot-config.ini")
|
||||
print("✓ Config file exists in nix store")
|
||||
|
||||
# Verify user and group exist
|
||||
print("\n=== User/Group Checks ===")
|
||||
machine.succeed("id meshcore-bot")
|
||||
print("✓ User meshcore-bot exists")
|
||||
|
||||
# Verify user is in dialout group for serial access
|
||||
machine.succeed("groups meshcore-bot | grep -q dialout")
|
||||
print("✓ User meshcore-bot is in dialout group")
|
||||
|
||||
# Check directory permissions
|
||||
machine.succeed("test -d /var/lib/meshcore-bot")
|
||||
machine.succeed("stat -c '%U:%G' /var/lib/meshcore-bot | grep -q meshcore-bot:meshcore-bot")
|
||||
print("✓ Data directory has correct ownership")
|
||||
|
||||
|
||||
# Verify service is running (or attempting to run)
|
||||
# The service will fail to connect but should be in active/running or restart loop
|
||||
print("\n=== Service State ===")
|
||||
status = machine.succeed("systemctl is-active meshcore-bot.service || echo 'not-active'")
|
||||
print(f"Service status: {status}")
|
||||
|
||||
# Check logs show the bot attempted to start
|
||||
machine.succeed("journalctl -u meshcore-bot.service | grep -q 'MeshCore Bot' || journalctl -u meshcore-bot.service | grep -q 'meshcore'")
|
||||
print("✓ Bot startup logged in systemd journal")
|
||||
|
||||
# Verify it tried to connect via TCP
|
||||
machine.succeed("journalctl -u meshcore-bot.service | grep -qi 'tcp' || journalctl -u meshcore-bot.service | grep -qi 'localhost'")
|
||||
print("✓ Bot attempted TCP connection")
|
||||
|
||||
'';
|
||||
};
|
||||
nixos-module-webviewer = pkgs.testers.nixosTest {
|
||||
name = "meshcore-bot-webviewer";
|
||||
|
||||
nodes.machine = {
|
||||
imports = [self.nixosModules.default];
|
||||
|
||||
services.meshcore-bot = {
|
||||
enable = true;
|
||||
settings = {
|
||||
Connection = {
|
||||
connection_type = "tcp";
|
||||
hostname = "localhost";
|
||||
tcp_port = 5000;
|
||||
timeout = 30;
|
||||
};
|
||||
Bot = {
|
||||
bot_name = "TestBotWithViewer";
|
||||
enabled = true;
|
||||
};
|
||||
Web_Viewer = {
|
||||
enabled = true;
|
||||
host = "127.0.0.1";
|
||||
port = 8080;
|
||||
debug = false;
|
||||
auto_start = true;
|
||||
};
|
||||
Logging = {
|
||||
log_level = "DEBUG";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("meshcore-bot.service")
|
||||
|
||||
# Give it time to start and initialize web viewer
|
||||
machine.sleep(10)
|
||||
# Check logs show the bot loaded the webviewer plugin
|
||||
machine.succeed("journalctl -u meshcore-bot.service | grep -q 'Successfully loaded plugin: webviewer'")
|
||||
|
||||
# FIXME meshcore-bot doesn't open the port.
|
||||
# probably because it never connected to it's companion socket.
|
||||
# I don't want to mock it.
|
||||
# Wait for web viewer to start listening on port 8080
|
||||
# This is the NixOS test framework way to check if a port is open
|
||||
#print("\n=== Web Viewer Port Check ===")
|
||||
#machine.wait_for_open_port(8080, timeout=30)
|
||||
#print("✓ Web viewer is listening on port 8080")
|
||||
|
||||
# Verify we can connect to the web viewer
|
||||
#machine.succeed("curl -f http://127.0.0.1:8080/ || curl -f http://localhost:8080/")
|
||||
#print("✓ Web viewer responds to HTTP requests")
|
||||
|
||||
# Check logs for web viewer startup
|
||||
machine.succeed("journalctl -u meshcore-bot.service | grep -qi 'web.*viewer' || journalctl -u meshcore-bot.service | grep -qi 'flask'")
|
||||
print("✓ Web viewer startup logged")
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
{
|
||||
self,
|
||||
inputs,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
];
|
||||
perSystem = {
|
||||
pkgs,
|
||||
self',
|
||||
system,
|
||||
...
|
||||
}: let
|
||||
# Fix meshcore-cli to use correct package names
|
||||
meshcore-cli-fixed = inputs.meshcore-cli.packages.${system}.meshcore-cli.overridePythonAttrs (old: {
|
||||
# Override propagatedBuildInputs to use correct nixpkgs names
|
||||
propagatedBuildInputs = with pkgs.python3Packages; [
|
||||
meshcore
|
||||
bleak
|
||||
prompt-toolkit # Fixed: was prompt_toolkit
|
||||
requests
|
||||
pycryptodome
|
||||
];
|
||||
});
|
||||
|
||||
# Custom Python packages not in nixpkgs - fetch from PyPI
|
||||
maidenhead = pkgs.python3Packages.buildPythonPackage rec {
|
||||
pname = "maidenhead";
|
||||
version = "1.7.0";
|
||||
format = "setuptools";
|
||||
|
||||
src = pkgs.fetchPypi {
|
||||
inherit pname version;
|
||||
hash = "sha256-NkFdzo9jCSflkjLIjQgTUhjTKBlQs9PrXXyqBKdiP3Q=";
|
||||
};
|
||||
|
||||
doCheck = false; # Skip tests for simplicity
|
||||
};
|
||||
|
||||
pyephem = pkgs.python3Packages.buildPythonPackage rec {
|
||||
pname = "ephem"; # PyPI name is 'ephem', not 'pyephem'
|
||||
version = "4.1.6";
|
||||
format = "setuptools";
|
||||
|
||||
src = pkgs.fetchPypi {
|
||||
inherit pname version;
|
||||
hash = "sha256-DtLk6nb52z7t4iBK2rivPxcIIBx8BO6FEecQpUymQl8=";
|
||||
};
|
||||
|
||||
doCheck = false;
|
||||
};
|
||||
|
||||
openmeteo-sdk = pkgs.python3Packages.buildPythonPackage rec {
|
||||
pname = "openmeteo-sdk";
|
||||
version = "1.23.0";
|
||||
pyproject = true;
|
||||
|
||||
src = pkgs.fetchPypi {
|
||||
pname = "openmeteo_sdk"; # PyPI uses underscore
|
||||
inherit version;
|
||||
hash = "sha256-L4YLRjW+1azCyc7pTCLcVjrZBuPWjycNPnX8A9m0I/E=";
|
||||
};
|
||||
propagatedBuildInputs = with pkgs.python3Packages; [
|
||||
requests
|
||||
hatchling
|
||||
flatbuffers
|
||||
];
|
||||
|
||||
doCheck = false;
|
||||
};
|
||||
openmeteo-requests = pkgs.python3Packages.buildPythonPackage rec {
|
||||
pname = "openmeteo-requests";
|
||||
version = "1.7.4";
|
||||
#format = "setuptools";
|
||||
pyproject = true;
|
||||
|
||||
src = pkgs.fetchPypi {
|
||||
pname = "openmeteo_requests"; # PyPI uses underscore
|
||||
inherit version;
|
||||
hash = "sha256-lJCqDvdo3cWJfVcXA2pZiXRk/gNhmt09WXyjgApwQkQ=";
|
||||
};
|
||||
|
||||
propagatedBuildInputs = with pkgs.python3Packages; [
|
||||
requests
|
||||
hatchling
|
||||
niquests
|
||||
openmeteo-sdk
|
||||
];
|
||||
|
||||
doCheck = false;
|
||||
};
|
||||
|
||||
retry-requests = pkgs.python3Packages.buildPythonPackage rec {
|
||||
pname = "retry-requests";
|
||||
version = "2.0.0";
|
||||
format = "setuptools";
|
||||
|
||||
src = pkgs.fetchPypi {
|
||||
inherit pname version;
|
||||
hash = "sha256-PQITXlqv7fCSQEFBgvxzicXStN4CUtq6AFTJ1qJ+djk=";
|
||||
};
|
||||
|
||||
# Patch to remove setup_requires if it causes issues
|
||||
patches = [
|
||||
(pkgs.writeText "remove-setup-requires.patch" ''
|
||||
--- retry-requests-2.0.0-old/setup.py 2025-12-23 14:46:45.698182443 +0100
|
||||
+++ retry-requests-2.0.0/setup.py 2025-12-23 14:47:00.302294857 +0100
|
||||
@@ -23,7 +23,6 @@
|
||||
install_requires=["requests", "urllib3>=1.26"],
|
||||
extras_require={"test": test_requires},
|
||||
tests_require=test_requires,
|
||||
- setup_requires=["pytest-runner"],
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
'')
|
||||
];
|
||||
|
||||
propagatedBuildInputs = with pkgs.python3Packages; [
|
||||
requests
|
||||
];
|
||||
|
||||
doCheck = false;
|
||||
};
|
||||
in {
|
||||
# Package definitions
|
||||
packages.default = pkgs.python3Packages.buildPythonPackage {
|
||||
name = "meshcore-bot";
|
||||
src = ./..;
|
||||
pyproject = true;
|
||||
|
||||
nativeBuildInputs = with pkgs.python3Packages; [setuptools];
|
||||
propagatedBuildInputs =
|
||||
(with pkgs.python3Packages; [
|
||||
# Core Python dependencies from requirements.txt
|
||||
aiohttp
|
||||
aiohttp-retry
|
||||
aiomqtt
|
||||
bleak
|
||||
colorlog
|
||||
feedparser
|
||||
flask
|
||||
flask-socketio
|
||||
geopy
|
||||
meshcore
|
||||
pyserial
|
||||
python-dateutil
|
||||
pytz
|
||||
requests
|
||||
requests-cache
|
||||
schedule
|
||||
cryptography
|
||||
])
|
||||
++ [
|
||||
# Custom packages from PyPI
|
||||
maidenhead
|
||||
pyephem
|
||||
openmeteo-requests
|
||||
retry-requests
|
||||
# Fixed meshcore-cli
|
||||
meshcore-cli-fixed
|
||||
];
|
||||
|
||||
# Allow pip to fetch packages not available in nixpkgs
|
||||
dontCheckRuntimeDeps = true;
|
||||
|
||||
meta = {
|
||||
description = "A Python bot that connects to MeshCore mesh networks via serial port, BLE, or TCP/IP.";
|
||||
mainProgram = "meshcore-bot";
|
||||
license = pkgs.lib.licenses.mit;
|
||||
homepage = "https://github.com/agessaman/meshcore-bot/";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{self, ...}: {
|
||||
imports = [
|
||||
];
|
||||
perSystem = {
|
||||
pkgs,
|
||||
self',
|
||||
...
|
||||
}: {
|
||||
# Development shell
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
self'.packages.default
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# pyproject.toml
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meshcore-bot"
|
||||
version = "0.1.0"
|
||||
description = "MeshCore Bot using the meshcore-cli and meshcore.py packages"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"pyserial>=3.5",
|
||||
"bleak>=0.20.0",
|
||||
"asyncio-mqtt>=0.11.0",
|
||||
"configparser>=5.3.0",
|
||||
"python-dateutil>=2.8.2",
|
||||
"schedule>=1.2.0",
|
||||
"colorlog>=6.7.0",
|
||||
"requests>=2.31.0",
|
||||
"pyephem>=4.1.4",
|
||||
"geopy>=2.3.0",
|
||||
"maidenhead>=1.4.0",
|
||||
"pytz>=2023.3",
|
||||
"aiohttp>=3.8.0",
|
||||
"meshcore>=2.1.6",
|
||||
"openmeteo-requests>=1.7.2",
|
||||
"requests-cache>=1.1.1",
|
||||
"retry-requests>=1.0.0",
|
||||
"flask>=2.3.0",
|
||||
"flask-socketio>=5.3.0",
|
||||
"meshcore-cli",
|
||||
"feedparser>=6.0.10",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
meshcore-bot = "meshcore_bot:main"
|
||||
meshcore-viewer = "modules.web_viewer.app:main"
|
||||
|
||||
[tool.setuptools]
|
||||
# Include both the main module and the modules package
|
||||
py-modules = ["meshcore_bot"]
|
||||
packages = ["modules", "modules.commands", "modules.commands.alternatives",
|
||||
"modules.commands.alternatives.inactive", "modules.service_plugins", "modules.web_viewer"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["*.json"]
|
||||
modules = ["web_viewer/templates/*.html"]
|
||||
Reference in new issue
Block a user