OPi: авто-захват кадра с RTSP + отправка на распознавание номера
This commit is contained in:
@@ -83,3 +83,8 @@
|
|||||||
- Реле: GPIO32 (красный), GPIO33 (зелёный) через BC547 транзисторы
|
- Реле: GPIO32 (красный), GPIO33 (зелёный) через BC547 транзисторы
|
||||||
- Питание: 5V 2A PSU для всей системы
|
- Питание: 5V 2A PSU для всей системы
|
||||||
- Весы DB25 пин 2 (TXD) → MAX3232 RX; DB25 пин 7 (GND) → общий GND
|
- Весы 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 threading
|
||||||
import select
|
import select
|
||||||
import os
|
import os
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
# ── Серийный порт ────────────────────────────────────────────────────────────
|
# ── Серийный порт ────────────────────────────────────────────────────────────
|
||||||
SERIAL_PORT = '/dev/ttyUSB0'
|
SERIAL_PORT = '/dev/ttyUSB0'
|
||||||
@@ -43,6 +46,13 @@ MQTT2_PORT = 1884
|
|||||||
MQTT2_USER = 'esp32'
|
MQTT2_USER = 'esp32'
|
||||||
MQTT2_PASS = 'Esp32Scales#2026'
|
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_WEIGHT_TOPIC = 'scale/weight'
|
||||||
MQTT_GO_TOPIC = 'scale/traffic/go'
|
MQTT_GO_TOPIC = 'scale/traffic/go'
|
||||||
|
|
||||||
@@ -171,6 +181,46 @@ def publish_weight(w):
|
|||||||
print(f"[{name}] Ошибка публикации: {e}")
|
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
|
# Парсер протокола A9
|
||||||
# ════════════════════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -274,7 +324,11 @@ def main():
|
|||||||
if max(vals) - min(vals) <= STABLE_DELTA:
|
if max(vals) - min(vals) <= STABLE_DELTA:
|
||||||
avg = sum(vals) / len(vals)
|
avg = sum(vals) / len(vals)
|
||||||
print(f"[STATE] → WAIT_GO: вес стабилен {avg:.1f} кг, отправляем")
|
print(f"[STATE] → WAIT_GO: вес стабилен {avg:.1f} кг, отправляем")
|
||||||
|
event_id = f"opi-{int(time.time()*1000)}"
|
||||||
publish_weight(avg)
|
publish_weight(avg)
|
||||||
|
if CAPTURE_ENABLED:
|
||||||
|
threading.Thread(target=capture_and_send,
|
||||||
|
args=(avg, event_id), daemon=True).start()
|
||||||
state = 'WAIT_GO'
|
state = 'WAIT_GO'
|
||||||
go_event.clear()
|
go_event.clear()
|
||||||
go_sent_time = time.time()
|
go_sent_time = time.time()
|
||||||
|
|||||||
Reference in New Issue
Block a user