OPi: shadow-распознавание Nomeroff (офлайн ANPR) параллельно с Claude

- фоновый вызов Nomeroff (:5010/recognize?crop=1&enhance=0) после захвата кадра
- результат -> /api/local-plate с ретраем на 404 (гонка с capture)
- полностью изолирован: ошибка/таймаут не влияют на боевой поток
- только для боевых снимков (не is_test)
This commit is contained in:
ZeroDay
2026-07-06 18:13:25 +03:00
parent 3c325cbfc1
commit 451a30f74d
+59
View File
@@ -30,6 +30,7 @@ import os
import base64 import base64
import json import json
import urllib.request import urllib.request
import urllib.error
# ── outbox (store-and-forward, встроенный) ────────────────────────────────── # ── outbox (store-and-forward, встроенный) ──────────────────────────────────
try: try:
@@ -58,6 +59,9 @@ MQTT2_PASS = 'Esp32Scales#2026'
CAPTURE_ENABLED = True CAPTURE_ENABLED = True
RTSP_URL = 'rtsp://iivideo:Vk9mT2xLp7nQ4wBj@192.168.20.3:9784/cameras/124/streaming/main' RTSP_URL = 'rtsp://iivideo:Vk9mT2xLp7nQ4wBj@192.168.20.3:9784/cameras/124/streaming/main'
SERVER_CAPTURE_URL = 'https://scales.zeroday.su/api/capture' SERVER_CAPTURE_URL = 'https://scales.zeroday.su/api/capture'
# ── офлайн-распознавание (Nomeroff shadow) ───────────────────────────────────
NOMEROFF_URL = 'http://192.168.20.4:5010/recognize?crop=1&enhance=0'
LOCAL_PLATE_URL = SERVER_CAPTURE_URL.rsplit('/api/capture', 1)[0] + '/api/local-plate'
CAPTURE_DEVICE_ID = 'scales_opi' CAPTURE_DEVICE_ID = 'scales_opi'
FRAME_PATH = '/tmp/scale_frame.jpg' FRAME_PATH = '/tmp/scale_frame.jpg'
@@ -219,6 +223,57 @@ def grab_frame():
print(f"[CAPTURE] Ошибка захвата кадра: {e}") print(f"[CAPTURE] Ошибка захвата кадра: {e}")
return None return None
def _offline_anpr(img, event_id):
"""Фоновый shadow-вызов Nomeroff + отправка результата на сервер.
Полностью изолирован: любая ошибка/таймаут НЕ влияет на боевой поток взвешивания."""
try:
boundary = '----opi' + str(int(time.time() * 1000))
body = (
('--%s\r\n' % boundary).encode()
+ b'Content-Disposition: form-data; name="photo"; filename="frame.jpg"\r\n'
+ b'Content-Type: image/jpeg\r\n\r\n' + img
+ ('\r\n--%s--\r\n' % boundary).encode()
)
req = urllib.request.Request(
NOMEROFF_URL, data=body,
headers={'Content-Type': 'multipart/form-data; boundary=%s' % boundary})
with urllib.request.urlopen(req, timeout=8) as r:
resp = json.loads(r.read().decode())
except Exception as e:
print(f"[ANPR] {event_id}: Nomeroff недоступен: {e}")
return
if not resp.get('found') or not resp.get('plate'):
print(f"[ANPR] {event_id}: офлайн не распознал")
return
payload = json.dumps({
"event_id": event_id,
"local_plate": resp.get('plate'),
"local_confidence": resp.get('confidence'),
"local_model": "nomeroff",
"local_elapsed": resp.get('elapsed_sec'),
}).encode()
# ретрай на 404: событие могло ещё не создаться на сервере (capture летит параллельно)
for attempt in range(3):
try:
req = urllib.request.Request(
LOCAL_PLATE_URL, data=payload,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=10) as r:
print(f"[ANPR] {event_id}: офлайн {resp.get('plate')} → сервер, ответ {r.status}")
return
except urllib.error.HTTPError as e:
if e.code == 404 and attempt < 2:
time.sleep(1.2)
continue
print(f"[ANPR] {event_id}: ошибка отправки: {e}")
return
except Exception as e:
print(f"[ANPR] {event_id}: ошибка отправки: {e}")
return
def capture_and_send(weight, event_id, is_test=False): def capture_and_send(weight, event_id, is_test=False):
""" """
Кадр + вес → локальную очередь (outbox). Кадр + вес → локальную очередь (outbox).
@@ -229,6 +284,10 @@ def capture_and_send(weight, event_id, is_test=False):
print(f"[CAPTURE] {event_id}: кадр не получен, пропуск") print(f"[CAPTURE] {event_id}: кадр не получен, пропуск")
return return
# shadow-распознавание Nomeroff в фоне (не для тестовых снимков камеры)
if not is_test:
threading.Thread(target=_offline_anpr, args=(img, event_id), daemon=True).start()
# Положить в outbox # Положить в outbox
if outbox: if outbox:
outbox.enqueue( outbox.enqueue(