feat(edge): runnable daemon loop (step + periodic sync/heartbeat/flush)
This commit is contained in:
parent
92eb97c078
commit
6f54706476
4 changed files with 104 additions and 0 deletions
4
edge/__main__.py
Normal file
4
edge/__main__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from edge.daemon import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
54
edge/daemon.py
Normal file
54
edge/daemon.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def run_loop(runtime, client, queue, *, clock, sleep, iterations=None,
|
||||||
|
sync_every=150, heartbeat_every=900, flush_every=300,
|
||||||
|
frame_interval=0.0):
|
||||||
|
counters = {"frames": 0, "syncs": 0, "beats": 0, "flushes": 0, "errors": 0}
|
||||||
|
i = 0
|
||||||
|
while iterations is None or i < iterations:
|
||||||
|
i += 1
|
||||||
|
try:
|
||||||
|
runtime.step(clock())
|
||||||
|
counters["frames"] += 1
|
||||||
|
except Exception:
|
||||||
|
counters["errors"] += 1
|
||||||
|
if sync_every and i % sync_every == 0:
|
||||||
|
cfg = client.fetch_config()
|
||||||
|
if cfg is not None:
|
||||||
|
from edge.zones import zones_from_config
|
||||||
|
runtime.apply_zones(zones_from_config(cfg))
|
||||||
|
counters["syncs"] += 1
|
||||||
|
if heartbeat_every and i % heartbeat_every == 0:
|
||||||
|
if client.send_heartbeat({"fps": 0.0, "frames": counters["frames"]}):
|
||||||
|
counters["beats"] += 1
|
||||||
|
if flush_every and i % flush_every == 0:
|
||||||
|
queue.flush()
|
||||||
|
counters["flushes"] += 1
|
||||||
|
if frame_interval:
|
||||||
|
sleep(frame_interval)
|
||||||
|
return counters
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None: # pragma: no cover (integration entrypoint, exercised on device)
|
||||||
|
import time
|
||||||
|
from edge.config import load_config
|
||||||
|
from edge.cloudclient import CloudClient
|
||||||
|
from edge.alert import PostClient
|
||||||
|
from edge.queue import OfflineQueue
|
||||||
|
from edge.runtime import PipelineRuntime
|
||||||
|
from edge.adapters.uvc_source import UvcSource
|
||||||
|
from edge.adapters.movenet_pose import MoveNetPoseAdapter
|
||||||
|
|
||||||
|
cfg = load_config(dict(os.environ))
|
||||||
|
nonce = lambda: os.urandom(8).hex()
|
||||||
|
now = time.time
|
||||||
|
base = cfg.cafe24_url.rstrip("/")
|
||||||
|
client = CloudClient(base, cfg.device_id, cfg.secret, nonce, now)
|
||||||
|
post = PostClient(base + "/api/events", cfg.device_id, cfg.secret, nonce, now)
|
||||||
|
queue = OfflineQueue(os.environ.get("QUEUE_DB", "queue.db"), post)
|
||||||
|
source = UvcSource(int(os.environ.get("UVC_DEVICE", "0")))
|
||||||
|
pose = MoveNetPoseAdapter(os.environ["MOVENET_ONNX"])
|
||||||
|
runtime = PipelineRuntime(cfg, source, pose, queue)
|
||||||
|
run_loop(runtime, client, queue, clock=now, sleep=time.sleep, iterations=None,
|
||||||
|
frame_interval=1.0 / float(os.environ.get("TARGET_FPS", "12")))
|
||||||
|
|
@ -7,6 +7,9 @@ dependencies = ["numpy", "opencv-python", "requests"]
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = ["pytest"]
|
dev = ["pytest"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
move-detect-edge = "edge.daemon:main"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
addopts = "-q"
|
addopts = "-q"
|
||||||
|
|
|
||||||
43
tests/test_daemon.py
Normal file
43
tests/test_daemon.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from edge.daemon import run_loop
|
||||||
|
|
||||||
|
|
||||||
|
class _Runtime:
|
||||||
|
def __init__(self): self.steps = 0; self.applied = None
|
||||||
|
def step(self, now): self.steps += 1; return []
|
||||||
|
def apply_zones(self, zones): self.applied = zones
|
||||||
|
|
||||||
|
|
||||||
|
class _Client:
|
||||||
|
def __init__(self): self.configs = 0; self.beats = 0
|
||||||
|
def fetch_config(self): self.configs += 1; return {"zones": [], "thresholds": {}}
|
||||||
|
def send_heartbeat(self, m): self.beats += 1; return True
|
||||||
|
|
||||||
|
|
||||||
|
class _Queue:
|
||||||
|
def __init__(self): self.flushes = 0
|
||||||
|
def flush(self): self.flushes += 1; return 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_loop_steps_and_periodics():
|
||||||
|
rt, cl, q = _Runtime(), _Client(), _Queue()
|
||||||
|
ticks = iter(range(1000))
|
||||||
|
counters = run_loop(rt, cl, q, clock=lambda: float(next(ticks)),
|
||||||
|
sleep=lambda s: None, iterations=10,
|
||||||
|
sync_every=5, heartbeat_every=5, flush_every=5)
|
||||||
|
assert rt.steps == 10
|
||||||
|
assert cl.configs == 2 and cl.beats == 2 and q.flushes == 2 # at frames 5 and 10
|
||||||
|
assert rt.applied == [] # zones applied from fetched config
|
||||||
|
|
||||||
|
|
||||||
|
def test_loop_survives_step_exception():
|
||||||
|
class Boom(_Runtime):
|
||||||
|
def step(self, now):
|
||||||
|
self.steps += 1
|
||||||
|
if self.steps == 2: raise RuntimeError("frame glitch")
|
||||||
|
return []
|
||||||
|
rt = Boom()
|
||||||
|
counters = run_loop(rt, _Client(), _Queue(), clock=lambda: 0.0,
|
||||||
|
sleep=lambda s: None, iterations=3,
|
||||||
|
sync_every=99, heartbeat_every=99, flush_every=99)
|
||||||
|
assert rt.steps == 3 # kept going past the exception
|
||||||
|
assert counters["errors"] == 1
|
||||||
Loading…
Reference in a new issue