From 8ca5a1e6d0f29fce1c66f526116c6cd484c0f8b5 Mon Sep 17 00:00:00 2001 From: Jorijn Schrijvershof Date: Sun, 4 Jan 2026 20:10:20 +0100 Subject: [PATCH] feat: auto-load config from meshcore.conf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify setup by having Python automatically load configuration from meshcore.conf at module import time. This eliminates the need to source config files in cron jobs or use direnv. - Add _load_config_file() to env.py that parses shell-style config - Environment variables always take precedence (Docker-friendly) - Rename .envrc.example to meshcore.conf.example (no direnv dependency) - Update cron examples to use flock for USB serial locking - Simplify documentation to use traditional .venv/ virtualenv BREAKING CHANGE: Configuration file renamed from .envrc to meshcore.conf. Users must copy meshcore.conf.example to meshcore.conf and migrate their settings. The new file format is the same (shell-style exports) but without the direnv-specific "layout python3" command. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 1 + CLAUDE.md | 55 ++++++++--------- README.md | 52 ++++++++++------ .envrc.example => meshcore.conf.example | 10 ++- src/meshmon/env.py | 81 +++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 52 deletions(-) rename .envrc.example => meshcore.conf.example (94%) diff --git a/.gitignore b/.gitignore index bd61bc1..17934f4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ build/ # Environment .envrc .env +meshcore.conf # Data directories (keep structure, ignore content) data/snapshots/companion/**/*.json diff --git a/CLAUDE.md b/CLAUDE.md index 39c6c3a..f37422e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,21 +12,18 @@ | HTML templates | `src/meshmon/templates/*.html` | `out/*.html` | | JavaScript | `src/meshmon/templates/*.js` | `out/*.js` | -Always edit the source templates, then regenerate with `direnv exec . python scripts/render_site.py`. +Always edit the source templates, then regenerate with `python scripts/render_site.py`. ## Running Commands -**IMPORTANT**: Always use `direnv exec .` to run Python scripts in this project. This ensures the correct virtualenv and environment variables are loaded. - ```bash -# Correct way to run scripts -direnv exec . python scripts/render_site.py - -# NEVER use these (virtualenv won't be loaded correctly): -# source .envrc && python ... -# .direnv/python-3.12/bin/python ... +cd /path/to/meshcore-stats +source .venv/bin/activate +python scripts/render_site.py ``` +Configuration is automatically loaded from `meshcore.conf` (if it exists). Environment variables always take precedence over the config file. + ## Commit Message Guidelines This project uses [Conventional Commits](https://www.conventionalcommits.org/) with [release-please](https://github.com/googleapis/release-please) for automated releases. **Commit messages directly control versioning and changelog generation.** @@ -236,12 +233,13 @@ meshcore-stats/ │ │ └── MM/ │ │ └── index.html, report.txt, report.json # Monthly │ └── companion/ # Same structure as repeater -└── .envrc # Environment configuration +├── meshcore.conf.example # Example configuration +└── meshcore.conf # Your configuration (auto-loaded by scripts) ``` ## Configuration -All configuration via environment variables (see `.envrc`): +All configuration via `meshcore.conf` or environment variables. The config file is automatically loaded by scripts; environment variables take precedence. ### Connection Settings - `MESH_TRANSPORT`: "serial" (default), "tcp", or "ble" @@ -390,16 +388,13 @@ Current migrations: ## Running the Scripts -Always source the environment first: - ```bash -# Using direnv (automatic) cd /path/to/meshcore-stats - -# Or manually -source .envrc 2>/dev/null +source .venv/bin/activate ``` +Configuration is automatically loaded from `meshcore.conf`. + ### Data Collection ```bash @@ -593,31 +588,37 @@ meshcore-cli -s /dev/ttyACM0 reset_path "repeater name" - Have routing issues (asymmetric path) - Be offline or rebooted -2. **Environment variables not loaded**: Scripts must be run with direnv active or manually source `.envrc` +2. **Configuration not loaded**: Ensure `meshcore.conf` exists in the project root, or set environment variables directly. 3. **Empty charts**: Need at least 2 data points to display meaningful data. ## Cron Setup (Example) -**Important**: Stagger companion and repeater collection to avoid USB serial conflicts. +Use `flock` to prevent USB serial conflicts when companion and repeater collection overlap. ```cron -# Companion: every minute at :00 -* * * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/collect_companion.py +MESHCORE=/path/to/meshcore-stats -# Repeater: every 15 minutes at :01, :16, :31, :46 (offset by 1 min to avoid USB conflict) -1,16,31,46 * * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/collect_repeater.py +# Companion: every minute +* * * * * cd $MESHCORE && flock -w 60 /tmp/meshcore.lock .venv/bin/python scripts/collect_companion.py + +# Repeater: every 15 minutes (offset by 1 min for staggering) +1,16,31,46 * * * * cd $MESHCORE && flock -w 60 /tmp/meshcore.lock .venv/bin/python scripts/collect_repeater.py # Charts: every 5 minutes (generates SVG charts from database) -*/5 * * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/render_charts.py +*/5 * * * * cd $MESHCORE && .venv/bin/python scripts/render_charts.py # HTML: every 5 minutes -*/5 * * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/render_site.py +*/5 * * * * cd $MESHCORE && .venv/bin/python scripts/render_site.py # Reports: daily at midnight (historical stats don't change often) -0 0 * * * cd /path/to/meshcore-stats && .direnv/python-3.12/bin/python scripts/render_reports.py +0 0 * * * cd $MESHCORE && .venv/bin/python scripts/render_reports.py ``` +**Notes:** +- `cd $MESHCORE` is required because paths in the config are relative to the project root +- `flock -w 60` waits up to 60 seconds for the lock, preventing USB serial conflicts + ## Adding New Metrics With the EAV schema, adding new metrics is simple: @@ -653,7 +654,7 @@ To change a metric from gauge to counter (or vice versa): 1. Update `METRIC_CONFIG` in `src/meshmon/metrics.py` - change the `type` field 2. Update `scale` if needed (counters often use scale=60 for per-minute display) -3. Regenerate charts: `direnv exec . python scripts/render_charts.py` +3. Regenerate charts: `python scripts/render_charts.py` ## Database Maintenance diff --git a/README.md b/README.md index 44cfe76..ac6acc6 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,16 @@ source .venv/bin/activate pip install -r requirements.txt ``` -### 2. Configure Environment Variables +### 2. Configure Copy the example configuration file and customize it: ```bash -cp .envrc.example .envrc -# Edit .envrc with your settings +cp meshcore.conf.example meshcore.conf +# Edit meshcore.conf with your settings ``` -The `.envrc.example` file contains all available configuration options with documentation. Key settings to configure: +The configuration file is automatically loaded by the scripts. Key settings to configure: - **Connection**: `MESH_SERIAL_PORT`, `MESH_TRANSPORT` - **Repeater Identity**: `REPEATER_NAME`, `REPEATER_PASSWORD` @@ -59,16 +59,16 @@ The `.envrc.example` file contains all available configuration options with docu - **Hardware Info**: `REPEATER_HARDWARE`, `COMPANION_HARDWARE` - **Radio Config**: `RADIO_FREQUENCY`, `RADIO_BANDWIDTH`, etc. (includes presets for different regions) -If using direnv: -```bash -direnv allow -``` +See `meshcore.conf.example` for all available options with documentation. ## Usage ### Manual Execution ```bash +cd /path/to/meshcore-stats +source .venv/bin/activate + # Collect companion data python scripts/collect_companion.py @@ -82,32 +82,46 @@ python scripts/render_site.py python scripts/render_reports.py ``` +The configuration is automatically loaded from `meshcore.conf`. + ### Cron Setup Add these entries to your crontab (`crontab -e`): ```cron -# MeshCore Stats - adjust paths as needed -SHELL=/bin/bash -MESHCORE_STATS=/home/user/meshcore-stats -DIRENV=/usr/bin/direnv +# MeshCore Stats - adjust path as needed +MESHCORE=/home/user/meshcore-stats # Every minute: collect companion data -* * * * * cd $MESHCORE_STATS && $DIRENV exec . python scripts/collect_companion.py +* * * * * cd $MESHCORE && flock -w 60 /tmp/meshcore.lock .venv/bin/python scripts/collect_companion.py # Every 15 minutes: collect repeater data -*/15 * * * * cd $MESHCORE_STATS && $DIRENV exec . python scripts/collect_repeater.py +1,16,31,46 * * * * cd $MESHCORE && flock -w 60 /tmp/meshcore.lock .venv/bin/python scripts/collect_repeater.py # Every 5 minutes: render site -*/5 * * * * cd $MESHCORE_STATS && $DIRENV exec . python scripts/render_site.py +*/5 * * * * cd $MESHCORE && .venv/bin/python scripts/render_site.py # Daily at midnight: generate reports -0 0 * * * cd $MESHCORE_STATS && $DIRENV exec . python scripts/render_reports.py +0 0 * * * cd $MESHCORE && .venv/bin/python scripts/render_reports.py # Monthly at 3 AM on the 1st: database maintenance -0 3 1 * * cd $MESHCORE_STATS && ./scripts/db_maintenance.sh +0 3 1 * * $MESHCORE/scripts/db_maintenance.sh ``` +**Notes:** +- `cd $MESHCORE` is required because paths in the config are relative to the project root +- `flock` prevents USB serial conflicts when companion and repeater collection overlap + +### Docker / Container Usage + +When running in Docker, you can skip the config file and pass environment variables directly: + +```bash +docker run -e MESH_SERIAL_PORT=/dev/ttyUSB0 -e REPEATER_NAME="My Repeater" ... +``` + +Environment variables always take precedence over `meshcore.conf`. + ### Serving the Site The static site is generated in the `out/` directory. You can serve it with any web server: @@ -125,8 +139,8 @@ cd out && python3 -m http.server 8080 meshcore-stats/ ├── requirements.txt ├── README.md -├── .envrc.example # Example configuration (copy to .envrc) -├── .envrc # Your configuration (create this) +├── meshcore.conf.example # Example configuration +├── meshcore.conf # Your configuration (create this) ├── src/meshmon/ │ ├── __init__.py │ ├── env.py # Environment variable parsing diff --git a/.envrc.example b/meshcore.conf.example similarity index 94% rename from .envrc.example rename to meshcore.conf.example index 1833873..0dc8bb3 100644 --- a/.envrc.example +++ b/meshcore.conf.example @@ -1,11 +1,9 @@ # MeshCore Stats Configuration -# Copy this file to .envrc and customize for your setup: -# cp .envrc.example .envrc +# Copy this file to meshcore.conf and customize for your setup: +# cp meshcore.conf.example meshcore.conf # -# If using direnv, it will automatically load when you cd into this directory. -# Otherwise, source it manually: source .envrc - -layout python3 +# This file is automatically loaded by the scripts. No need to source it manually. +# Environment variables always take precedence over this file (useful for Docker). # ============================================================================= # Connection Settings diff --git a/src/meshmon/env.py b/src/meshmon/env.py index be1f9e7..f2c662a 100644 --- a/src/meshmon/env.py +++ b/src/meshmon/env.py @@ -1,10 +1,91 @@ """Environment variable parsing and configuration.""" import os +import re +import warnings from pathlib import Path from typing import Optional +def _parse_config_value(value: str) -> str: + """Parse a shell-style value, handling quotes and inline comments.""" + value = value.strip() + + if not value: + return "" + + # Handle double-quoted strings + if value.startswith('"'): + end = value.find('"', 1) + if end != -1: + return value[1:end] + return value[1:] # No closing quote + + # Handle single-quoted strings + if value.startswith("'"): + end = value.find("'", 1) + if end != -1: + return value[1:end] + return value[1:] + + # Unquoted value - strip inline comments (# preceded by whitespace) + comment_match = re.search(r"\s+#", value) + if comment_match: + value = value[: comment_match.start()] + + return value.strip() + + +def _load_config_file() -> None: + """Load meshcore.conf if it exists. Env vars take precedence. + + The config file is expected in the project root (three levels up from this module). + Scripts should be run from the project directory via cron: cd $MESHCORE && .venv/bin/python ... + """ + config_path = Path(__file__).resolve().parent.parent.parent / "meshcore.conf" + + if not config_path.exists(): + return + + try: + with open(config_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + + # Skip comments and empty lines + if not line or line.startswith("#"): + continue + + # Remove 'export ' prefix + if line.startswith("export "): + line = line[7:].lstrip() + + # Must have KEY=value format + if "=" not in line: + continue + + key, _, value = line.partition("=") + key = key.strip() + + # Validate key is a valid shell identifier + if not key or not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key): + continue + + # Parse value (handles quotes, inline comments) + value = _parse_config_value(value) + + # Only set if not already in environment + if key not in os.environ: + os.environ[key] = value + + except (OSError, UnicodeDecodeError) as e: + warnings.warn(f"Failed to load {config_path}: {e}") + + +# Load config file at module import time, before Config is instantiated +_load_config_file() + + def get_str(key: str, default: Optional[str] = None) -> Optional[str]: """Get string env var.""" return os.environ.get(key, default)