OPi: авто-захват кадра с RTSP + отправка на распознавание номера

This commit is contained in:
claude
2026-06-26 15:09:33 +03:00
parent 69a773a2c9
commit 9d5d80ff7b
5 changed files with 90 additions and 0 deletions
+5
View File
@@ -83,3 +83,8 @@
- Реле: GPIO32 (красный), GPIO33 (зелёный) через BC547 транзисторы
- Питание: 5V 2A PSU для всей системы
- Весы DB25 пин 2 (TXD) → MAX3232 RX; DB25 пин 7 (GND) → общий GND
## 2026-06-26 — авто-захват фото + распознавание номера (Claude API)
- orange_pi/scales.py: при стабилизации веса снимает кадр с RTSP-камеры (ffmpeg) и шлёт на scales.zeroday.su/api/capture в фоновом потоке
- grab_test.py: разовый тест камеры → сервер
- сервер распознаёт номер через aiprimetech.io (claude-haiku-4-5 + fallback sonnet-4-6) и кладёт фото+номер рядом с весом
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
# Разовый тест: снять кадр с RTSP-камеры и отправить на наш сервер распознавания.
# Запуск (когда на весах стоит машина и виден номер): python3 grab_test.py
import subprocess, base64, urllib.request, json, datetime, time
RTSP_URL = 'rtsp://iivideo:Vk9mT2xLp7nQ4wBj@192.168.20.3:9784/cameras/124/streaming/main'
SERVER = 'https://scales.zeroday.su/api/capture'
FRAME = '/tmp/scale_frame_test.jpg'
print("1) Снимаю кадр с камеры...")
cmd = ['ffmpeg', '-y', '-loglevel', 'error', '-rtsp_transport', 'tcp',
'-i', RTSP_URL, '-frames:v', '1', '-update', '1',
'-vf', 'scale=1280:-2', '-q:v', '4', '-f', 'image2', FRAME]
try:
subprocess.run(cmd, timeout=15, check=True)
except Exception as e:
print(" ОШИБКА захвата кадра:", e); raise SystemExit(1)
img = open(FRAME, 'rb').read()
print(f" OK, кадр {len(img)} байт")
eid = f"opi-camtest-{int(time.time())}"
print(f"2) Отправляю на сервер (event_id={eid}, вес-заглушка 12345)...")
payload = json.dumps({
'event_id': eid, 'device_id': 'scales_opi', 'weight': 12345.0,
'captured_at': datetime.datetime.now().isoformat(), 'media_type': 'image/jpeg',
'photo_b64': base64.b64encode(img).decode()
}).encode()
req = urllib.request.Request(SERVER, data=payload, headers={'Content-Type': 'application/json'})
print(" ответ сервера:", urllib.request.urlopen(req, timeout=20).read().decode())
print(f"3) Готово! Скажи Claude event_id={eid} — проверю кадр и распознанный номер.")
Binary file not shown.
+54
View File
@@ -26,6 +26,9 @@ import datetime
import threading
import select
import os
import base64
import json
import urllib.request
# ── Серийный порт ────────────────────────────────────────────────────────────
SERIAL_PORT = '/dev/ttyUSB0'
@@ -43,6 +46,13 @@ MQTT2_PORT = 1884
MQTT2_USER = 'esp32'
MQTT2_PASS = 'Esp32Scales#2026'
# ── Камера + распознавание (наш VPS) ─────────────────────────────────────────
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'
CAPTURE_DEVICE_ID = 'scales_opi'
FRAME_PATH = '/tmp/scale_frame.jpg'
MQTT_WEIGHT_TOPIC = 'scale/weight'
MQTT_GO_TOPIC = 'scale/traffic/go'
@@ -171,6 +181,46 @@ def publish_weight(w):
print(f"[{name}] Ошибка публикации: {e}")
# ════════════════════════════════════════════════════════════════════════════════
# Захват кадра с камеры и отправка на сервер распознавания
# ════════════════════════════════════════════════════════════════════════════════
def grab_frame():
"""Снимает один кадр с RTSP-камеры через ffmpeg. Возвращает JPEG-байты или None."""
cmd = ['ffmpeg', '-y', '-loglevel', 'error', '-rtsp_transport', 'tcp',
'-i', RTSP_URL, '-frames:v', '1', '-update', '1',
'-vf', 'scale=1280:-2', '-q:v', '4', '-f', 'image2', FRAME_PATH]
try:
subprocess.run(cmd, timeout=15, check=True)
with open(FRAME_PATH, 'rb') as f:
return f.read()
except Exception as e:
print(f"[CAPTURE] Ошибка захвата кадра: {e}")
return None
def capture_and_send(weight, event_id):
"""Кадр + вес → наш VPS. Запускать в отдельном потоке (не блокирует светофор)."""
img = grab_frame()
if not img:
print(f"[CAPTURE] {event_id}: кадр не получен, пропуск")
return
try:
payload = json.dumps({
"event_id": event_id,
"device_id": CAPTURE_DEVICE_ID,
"weight": round(weight, 1),
"captured_at": datetime.datetime.now().isoformat(),
"media_type": "image/jpeg",
"photo_b64": base64.b64encode(img).decode(),
}).encode()
req = urllib.request.Request(SERVER_CAPTURE_URL, data=payload,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=20) as r:
print(f"[CAPTURE] {event_id}: отправлено ({len(img)} б), ответ {r.status}")
except Exception as e:
print(f"[CAPTURE] {event_id}: ошибка отправки: {e}")
# ════════════════════════════════════════════════════════════════════════════════
# Парсер протокола A9
# ════════════════════════════════════════════════════════════════════════════════
@@ -274,7 +324,11 @@ def main():
if max(vals) - min(vals) <= STABLE_DELTA:
avg = sum(vals) / len(vals)
print(f"[STATE] → WAIT_GO: вес стабилен {avg:.1f} кг, отправляем")
event_id = f"opi-{int(time.time()*1000)}"
publish_weight(avg)
if CAPTURE_ENABLED:
threading.Thread(target=capture_and_send,
args=(avg, event_id), daemon=True).start()
state = 'WAIT_GO'
go_event.clear()
go_sent_time = time.time()