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
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 sys module is imported but it is not used in the script. It's good practice to remove unused imports to keep the code clean.

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]
Comment on lines +24 to +27
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 function _load_platformio_env currently selects the first [env:*] section from platformio.ini. This can be problematic if multiple environments are defined, as it might not be the intended one. To make this more robust, consider handling multiple environments, for example by allowing the user to specify an environment via a command-line argument. A simpler improvement would be to warn the user if more than one env: section is found.

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 +80 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 is nested within an if/else block inside the main function. This reduces readability. Consider extracting this logic into a separate function to improve the structure and make main easier to follow.


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