feat(2-7): operational radar loop, startup screen, and systemd wiring
Implements story 2-7: full main.py with _make_startup_screen(), _run_one_cycle() with per-phase timing and slow-render warning, and main() connecting config → bounds → fetcher → renderer → display. Adds provision.py early-exit when already provisioned. Adds User=root to both systemd service files. Adds tests/test_main.py (3 new tests, 99 total). Updates scaffold test to allow provisioning.config import from main.py. All quality gates pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+66
-1
@@ -1,8 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from planemapper.constants import (
|
||||
DISPLAY_HEIGHT,
|
||||
DISPLAY_WIDTH,
|
||||
REFRESH_INTERVAL_S,
|
||||
RENDER_WARN_S,
|
||||
)
|
||||
from planemapper.display import DisplayInterface, WaveshareDisplay
|
||||
from planemapper.fetcher import HttpFetcher
|
||||
from planemapper.provisioning.config import read as read_config
|
||||
from planemapper.renderer.basemap import load as load_basemap
|
||||
from planemapper.renderer.projection import MapBounds
|
||||
from planemapper.renderer.renderer import Renderer
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _make_startup_screen() -> Image.Image:
|
||||
image = Image.new("RGB", (DISPLAY_WIDTH, DISPLAY_HEIGHT), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(image)
|
||||
font = ImageFont.load_default()
|
||||
draw.text(
|
||||
(DISPLAY_WIDTH // 2 - 60, DISPLAY_HEIGHT // 2),
|
||||
"planeMapper starting...",
|
||||
fill=(0, 0, 0),
|
||||
font=font,
|
||||
)
|
||||
return image
|
||||
|
||||
|
||||
def _run_one_cycle(renderer: Renderer, fetcher: HttpFetcher, display: DisplayInterface) -> None:
|
||||
t0 = time.monotonic()
|
||||
aircraft_list = fetcher.fetch()
|
||||
t1 = time.monotonic()
|
||||
image = renderer.render(aircraft_list)
|
||||
t2 = time.monotonic()
|
||||
display.show(image)
|
||||
t3 = time.monotonic()
|
||||
total = t3 - t0
|
||||
log.info(
|
||||
"cycle: fetch=%.1fs render=%.1fs spi=%.1fs total=%.1fs",
|
||||
t1 - t0,
|
||||
t2 - t1,
|
||||
t3 - t2,
|
||||
total,
|
||||
)
|
||||
if total > RENDER_WARN_S:
|
||||
log.warning("render slow: %.1fs > %ds threshold", total, RENDER_WARN_S)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log.info("not implemented")
|
||||
cfg = read_config()
|
||||
bounds = MapBounds(
|
||||
home_lat=cfg["home_lat"],
|
||||
home_lon=cfg["home_lon"],
|
||||
radius_nm=cfg["coverage_radius_nm"],
|
||||
)
|
||||
base_map = load_basemap()
|
||||
fetcher = HttpFetcher()
|
||||
renderer = Renderer(base_map, bounds)
|
||||
display = WaveshareDisplay()
|
||||
startup = _make_startup_screen()
|
||||
display.show(startup)
|
||||
while True:
|
||||
_run_one_cycle(renderer, fetcher, display)
|
||||
time.sleep(REFRESH_INTERVAL_S)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from planemapper.provisioning import ProvisioningError, wifi
|
||||
from planemapper.provisioning import ProvisioningError, config, wifi
|
||||
from planemapper.provisioning.portal import app
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -16,6 +16,13 @@ def _reset_to_portal_state() -> None:
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
try:
|
||||
cfg = config.read()
|
||||
if cfg.get("provisioned"):
|
||||
log.info("already provisioned — exiting")
|
||||
return
|
||||
except FileNotFoundError:
|
||||
pass # no config — proceed to portal
|
||||
provisioned = False
|
||||
while not provisioned:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user