Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ python scripts/compile_check.py

If `src/config.h` is missing, the script copies `src/config_template.h`, runs `platformio run`, and removes the temporary file again.

For real node validation before OTA upload:

```bash
python scripts/hardware_smoke_check.py
```

The preflight checks whether `src/config.h` exists, whether it still contains placeholder values, whether the configured OTA target is reachable, and whether the configured MQTT broker answers on port `1883`.

Additional operating context now lives in:

- [docs/hardware.md](docs/hardware.md)
Expand All @@ -50,6 +58,6 @@ Additional operating context now lives in:
## Immediate Next Steps

1. Replace the legacy `config.h` flow with a clearer credentials/config separation.
2. Add a real hardware smoke-test routine after the new compile-only validation path.
2. Turn the current hardware preflight into a fuller post-upload smoke workflow with serial/MQTT assertions.
3. Decide which parts should remain standalone versus moving into `SkySentinel`.
4. Add hive-specific telemetry beyond the current BME280 baseline once the hardware scope is stable.
3 changes: 2 additions & 1 deletion docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ HiveTech is an ESP32 firmware prototype for live beehive monitoring. It reads BM
- Still uses a legacy `config.h` include path
- PlatformIO/IoT profile context is now committed in `anr.profile.yaml` plus focused hardware and operations docs
- Compile-only validation now works via `python scripts/compile_check.py`
- Hardware preflight now works via `python scripts/hardware_smoke_check.py`
- CI build validation is defined in `.github/workflows/build.yml`
- Local developer deployment settings are still present outside committed templates

Expand All @@ -20,7 +21,7 @@ HiveTech is an ESP32 firmware prototype for live beehive monitoring. It reads BM
- Hardware wiring is documented only to the level visible in current firmware assumptions
- Deployment is still local and OTA-target specific
- MQTT contract is documented but still topic-only, without retained/availability semantics
- Real hardware smoke validation is still manual
- Real hardware post-upload smoke validation is still manual

## Next Milestone

Expand Down
2 changes: 2 additions & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ Current runtime flow:
## Build And Validation

- `python scripts/compile_check.py` is the default compile-only validation path
- `python scripts/hardware_smoke_check.py` is the preflight path before OTA upload
- the script creates a temporary `src/config.h` from `src/config_template.h` when needed
- the hardware preflight checks local config completeness, OTA target reachability, and MQTT broker reachability
- CI uses the same path so local and remote validation stay aligned

## Safety Rules
Expand Down
10 changes: 6 additions & 4 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ Keep firmware changes reproducible and separate "compiles locally" from "works o
## Minimum Checks

1. Run `python scripts/compile_check.py` for a compile-only check.
2. If you need device-specific validation, provide a real local `src/config.h`.
3. Flash or OTA-update the node using the local deployment path from `platformio.ini`.
4. Confirm serial boot output reaches:
2. Run `python scripts/hardware_smoke_check.py` before OTA upload.
3. If you need device-specific validation, provide a real local `src/config.h`.
4. Flash or OTA-update the node using the local deployment path from `platformio.ini`.
5. Confirm serial boot output reaches:
- Wi-Fi connect
- MQTT connect
- BME280 detection
- deep sleep entry
5. Verify these MQTT topics receive fresh values:
6. Verify these MQTT topics receive fresh values:
- `HT_BME280_Temperature`
- `HT_BME280_Humidity`
- `HT_BME280_Pressure`
Expand All @@ -23,4 +24,5 @@ Keep firmware changes reproducible and separate "compiles locally" from "works o
## Separation Of Checks

- Compile-only validation is now available via `python scripts/compile_check.py`.
- Hardware preflight is now available via `python scripts/hardware_smoke_check.py`.
- Real hardware validation is still mandatory after sensor, networking, OTA, or sleep-logic changes.
124 changes: 124 additions & 0 deletions scripts/hardware_smoke_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from __future__ import annotations

import configparser
import re
import socket
import sys
from dataclasses import dataclass
from pathlib import Path


PLACEHOLDER_VALUES = {"your_wifi_ssid", "your_wifi_password", "your_mqtt_server"}


@dataclass
class CheckResult:
name: str
ok: bool
detail: str


def _load_platformio_env(repo_root: Path) -> tuple[str, dict[str, str]]:
config = configparser.ConfigParser(inline_comment_prefixes=(";", "#"))
config.read(repo_root / "platformio.ini", encoding="utf-8")
env_sections = [section for section in config.sections() if section.startswith("env:")]
if not env_sections:
raise ValueError("platformio.ini does not define an [env:*] section")
section = env_sections[0]
return section, {key: value.strip() for key, value in config[section].items()}


def _read_config_values(config_path: Path) -> dict[str, str]:
content = config_path.read_text(encoding="utf-8")
values: dict[str, str] = {}
for key in ("ssid", "password", "mqtt_server"):
match = re.search(rf'{key}\s*=\s*"([^"]+)"', content)
if match:
values[key] = match.group(1).strip()
return values


def _is_serial_target(value: str) -> bool:
upper = value.upper()
return upper.startswith("COM") or value.startswith("/dev/")


def _check_host(host: str, port: int | None = None) -> CheckResult:
try:
resolved = socket.gethostbyname(host)
except OSError as exc:
return CheckResult(f"resolve:{host}", False, f"name resolution failed: {exc}")

if port is None:
return CheckResult(f"resolve:{host}", True, f"resolved to {resolved}")

try:
with socket.create_connection((host, port), timeout=2):
return CheckResult(f"connect:{host}:{port}", True, f"reachable at {resolved}:{port}")
except OSError as exc:
return CheckResult(f"connect:{host}:{port}", False, f"connection failed: {exc}")


def main() -> int:
repo_root = Path(__file__).resolve().parent.parent
config_path = repo_root / "src" / "config.h"

results: list[CheckResult] = []

try:
env_name, env_config = _load_platformio_env(repo_root)
except ValueError as exc:
print(f"preflight error: {exc}")
return 1

upload_target = env_config.get("upload_port", "")
upload_protocol = env_config.get("upload_protocol", "")

results.append(CheckResult("platformio-env", True, f"using {env_name}"))
results.append(CheckResult("upload-protocol", bool(upload_protocol), f"value={upload_protocol or 'missing'}"))

if not config_path.exists():
results.append(CheckResult("config.h", False, f"missing local config: {config_path}"))
else:
values = _read_config_values(config_path)
missing_keys = [key for key in ("ssid", "password", "mqtt_server") if key not in values]
if missing_keys:
results.append(CheckResult("config.h", False, f"missing keys: {', '.join(missing_keys)}"))
else:
placeholders = [key for key, value in values.items() if value in PLACEHOLDER_VALUES]
if placeholders:
results.append(CheckResult("config.h", False, f"placeholder values still set: {', '.join(placeholders)}"))
else:
results.append(CheckResult("config.h", True, "real local credentials/config detected"))

mqtt_host = values.get("mqtt_server", "")
if mqtt_host:
results.append(_check_host(mqtt_host, 1883))
Comment on lines +82 to +96
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for checking config.h and the MQTT host can be improved. The current nested structure has a few issues:

  1. If some keys are missing from config.h (e.g., ssid), the check for MQTT host connectivity is skipped, even if mqtt_server is correctly defined. This can lead to incomplete diagnostics.
  2. If mqtt_server is set to a placeholder value, the script reports it as a placeholder but then still attempts a connection to that placeholder value, resulting in a redundant failure message.

I suggest refactoring this block to flatten the logic. This will ensure that all relevant checks are performed independently and that error messages are not duplicated.

Suggested change
else:
values = _read_config_values(config_path)
missing_keys = [key for key in ("ssid", "password", "mqtt_server") if key not in values]
if missing_keys:
results.append(CheckResult("config.h", False, f"missing keys: {', '.join(missing_keys)}"))
else:
placeholders = [key for key, value in values.items() if value in PLACEHOLDER_VALUES]
if placeholders:
results.append(CheckResult("config.h", False, f"placeholder values still set: {', '.join(placeholders)}"))
else:
results.append(CheckResult("config.h", True, "real local credentials/config detected"))
mqtt_host = values.get("mqtt_server", "")
if mqtt_host:
results.append(_check_host(mqtt_host, 1883))
else:
values = _read_config_values(config_path)
missing_keys = {key for key in ("ssid", "password", "mqtt_server") if key not in values}
placeholders = {key for key, value in values.items() if value in PLACEHOLDER_VALUES}
if missing_keys:
results.append(CheckResult("config.h", False, f"missing keys: {', '.join(sorted(missing_keys))}"))
elif placeholders:
results.append(CheckResult("config.h", False, f"placeholder values still set: {', '.join(sorted(placeholders))}"))
else:
results.append(CheckResult("config.h", True, "real local credentials/config detected"))
# Check MQTT host if it is defined and not a placeholder value.
mqtt_host = values.get("mqtt_server")
if mqtt_host and "mqtt_server" not in placeholders:
results.append(_check_host(mqtt_host, 1883))


if not upload_target:
results.append(CheckResult("upload-target", False, "upload_port missing"))
elif _is_serial_target(upload_target):
results.append(CheckResult("upload-target", True, f"serial target configured: {upload_target}"))
else:
results.append(_check_host(upload_target, 3232))

failures = 0
print("HiveTech hardware preflight")
print("")
for result in results:
status = "PASS" if result.ok else "FAIL"
print(f"[{status}] {result.name}: {result.detail}")
if not result.ok:
failures += 1

print("")
if failures:
print(f"preflight result: FAIL ({failures} failed checks)")
return 1

print("preflight result: PASS")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading