diff --git a/orange_pi/scales.py b/orange_pi/scales.py index 14d5486..db31a68 100644 --- a/orange_pi/scales.py +++ b/orange_pi/scales.py @@ -30,6 +30,7 @@ import os import base64 import json import urllib.request +import urllib.error # ── outbox (store-and-forward, встроенный) ────────────────────────────────── try: @@ -58,6 +59,9 @@ MQTT2_PASS = 'Esp32Scales#2026' CAPTURE_ENABLED = True RTSP_URL = 'rtsp://iivideo:Vk9mT2xLp7nQ4wBj@192.168.20.3:9784/cameras/124/streaming/main' 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' FRAME_PATH = '/tmp/scale_frame.jpg' @@ -219,6 +223,57 @@ def grab_frame(): print(f"[CAPTURE] Ошибка захвата кадра: {e}") 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): """ Кадр + вес → локальную очередь (outbox). @@ -229,6 +284,10 @@ def capture_and_send(weight, event_id, is_test=False): print(f"[CAPTURE] {event_id}: кадр не получен, пропуск") return + # shadow-распознавание Nomeroff в фоне (не для тестовых снимков камеры) + if not is_test: + threading.Thread(target=_offline_anpr, args=(img, event_id), daemon=True).start() + # Положить в outbox if outbox: outbox.enqueue(