feat: auto-load config from meshcore.conf

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 <noreply@anthropic.com>
This commit is contained in:
Jorijn Schrijvershof
2026-01-04 20:10:20 +01:00
parent 0f8b0a3492
commit 8ca5a1e6d0
5 changed files with 147 additions and 52 deletions

1
.gitignore vendored
View File

@@ -15,6 +15,7 @@ build/
# Environment
.envrc
.env
meshcore.conf
# Data directories (keep structure, ignore content)
data/snapshots/companion/**/*.json

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)