OPi: авто-захват кадра с RTSP + отправка на распознавание номера
This commit is contained in:
@@ -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.
@@ -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.
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user