From 2b4d6c344184ffa197a06ab8dd5e59c31f741467 Mon Sep 17 00:00:00 2001 From: d3m0k1d Date: Wed, 29 Apr 2026 14:46:41 +0300 Subject: [PATCH] chore: del move and update qr with dockerfile --- Move/ld2410b_counter.md | 88 -------------- Move/ld2410b_counter.yaml | 235 ------------------------------------- QR /scanner.md | 42 ------- QR /scanner.py | 90 -------------- backend/docker-compose.yml | 14 +++ scanner/Dockerfile | 17 +++ scanner/pyproject.toml | 18 +++ scanner/requirements.txt | 6 + scanner/scanner.py | 71 +++++++++++ 9 files changed, 126 insertions(+), 455 deletions(-) delete mode 100644 Move/ld2410b_counter.md delete mode 100644 Move/ld2410b_counter.yaml delete mode 100644 QR /scanner.md delete mode 100644 QR /scanner.py create mode 100644 scanner/Dockerfile create mode 100644 scanner/pyproject.toml create mode 100644 scanner/requirements.txt create mode 100644 scanner/scanner.py diff --git a/Move/ld2410b_counter.md b/Move/ld2410b_counter.md deleted file mode 100644 index a5ba931..0000000 --- a/Move/ld2410b_counter.md +++ /dev/null @@ -1,88 +0,0 @@ -# LD2410B Conveyor Counter - -ESPHome прошивка для подсчета товаров на конвейере с помощью mmWave сенсора **Hi-Link LD2410B** + **ESP32**. Данные передаются через **MQTT**. - -## Схема подключения - -``` -ESP32 DevKit V1 LD2410B -────────────────────────────────── -GPIO17 (TX) ────► RX -GPIO16 (RX) ────► TX -GPIO18 ────► OUT (digital motion) -3.3V / 5V ────► VCC -GND ────► GND -GPIO2 ────► LED (status, onboard) -``` - -**Важно:** Питание LD2410B — **5V** (не 3.3V!). UART логика 3.3V — совместима с ESP32. - -## Как работает - -1. **UART** (256000 baud) — чтение дистанции и энергии движения для каждой "зоны" (gate) -2. **GPIO OUT** — быстрый бинарный сигнал присутствия -3. **Конечный автомат** определяет момент прохода товара: - - `State 0` — ожидание - - `State 1` — товар в зоне (energy > threshold) - - `State 2` — товар прошел (debounce 300ms) → +1 к счетчику - -## MQTT топики - -| Топик | Описание | -|---|---| -| `ld2410b/conveyor/sensor/items_count/state` | Общий счетчик товаров | -| `ld2410b/conveyor/sensor/moving_distance/state` | Дистанция до цели (cm) | -| `ld2410b/conveyor/sensor/trigger_distance/state` | Дистанция в зоне детекции | -| `ld2410b/conveyor/text_sensor/counter_status/state` | Строка статуса (state, energy, count) | -| `ld2410b/conveyor/button/reset_counter/command` | Сброс счетчика (payload: любая) | -| `ld2410b/conveyor/number/energy_threshold/command` | Порог энергии детекции | -| `ld2410b/conveyor/number/min_distance_cm/command` | Мин. дистанция зоны (cm) | -| `ld2410b/conveyor/number/max_distance_cm/command` | Макс. дистанция зоны (cm) | - -## Настройка порогов - -### Energy Threshold -- **По умолчанию:** 50 -- **Диапазон:** 10–100 -- Чем выше — тем меньше ложных срабатываний, но нужно больше энергии движения для детекции - -### Min / Max Distance -- **По умолчанию:** 75–200 см -- Установите диапазон, в котором товары проходят через луч -- LD2410B имеет минимальную дистанцию ~75 см (gate 0) - -## Установка - -```bash -# Установите ESPHome -pip install esphome - -# Скомпилируйте и залейте -esphome run ld2410b_counter.yaml -``` - -## secrets.yaml - -Создайте файл `secrets.yaml` рядом с конфигом: - -```yaml -mqtt_broker: "192.168.1.100" -mqtt_user: "esphome" -mqtt_pass: "your_password" -``` - -## Отладка - -1. Откройте веб-интерфейс ESP (по IP устройства) -2. Следите за `Counter Status` — строка вида `State: 1 | E: 65 | D: 120 | Count: 42` -3. Если счетчик не увеличивается: - - Уменьшите `Energy Threshold` - - Проверьте что товар попадает в диапазон `Min/Max Distance` -4. Если ложные срабатывания — увеличьте `Energy Threshold` или сузьте диапазон дистанций - -## Ограничения LD2410B - -- **Минимальная дистанция:** ~75 см -- **Угол обзора:** ~120° (для сужения используйте физический экран/тубус перед сенсором) -- **Не различает** направление движения (для этого нужен LD2450 с трекингом X/Y) -- **Макс. скорость конвейера:** зависит от размера товаров и debounce (300ms). Для быстрого конвейера уменьшите debounce в коде. diff --git a/Move/ld2410b_counter.yaml b/Move/ld2410b_counter.yaml deleted file mode 100644 index 9b88ca5..0000000 --- a/Move/ld2410b_counter.yaml +++ /dev/null @@ -1,235 +0,0 @@ -esphome: - name: ld2410b-counter - friendly_name: "LD2410B Conveyor Counter" - -esp32: - board: esp32dev - framework: - type: arduino - -# Подключение LD2410B по UART -uart: - tx_pin: GPIO17 - rx_pin: GPIO16 - baud_rate: 256000 - parity: NONE - stop_bits: 1 - -# GPIO OUT пин для быстрого детектирования движения -binary_sensor: - - platform: gpio - pin: GPIO18 - name: "LD2410B Motion GPIO" - id: motion_gpio - device_class: motion - internal: true - -ld2410: - -sensor: - # Дистанция до движущейся цели - - platform: ld2410 - moving_distance: - name: "Moving Distance" - id: moving_dist - - # Дистанция до неподвижной цели - - platform: ld2410 - still_distance: - name: "Still Distance" - id: still_dist - - # Энергия движения в зоне 0 (ближняя зона, 0.75-1.5м) - - platform: ld2410 - g0: - move_energy: - name: "Gate 0 Move Energy" - id: g0_energy - internal: true - still_energy: - name: "Gate 0 Still Energy" - id: g0_still - internal: true - - # Энергия движения в зоне 1 (1.5-2.25м) - - platform: ld2410 - g1: - move_energy: - name: "Gate 1 Move Energy" - id: g1_energy - internal: true - - # Счетчик прошедших товаров - - platform: template - name: "Items Count" - id: items_count - icon: "mdi:counter" - unit_of_measurement: "items" - accuracy_decimals: 0 - state_class: total_increasing - device_class: count - - # Дистанция срабатывания (для отладки) - - platform: template - name: "Trigger Distance" - id: trigger_dist - icon: "mdi:ruler" - unit_of_measurement: "cm" - accuracy_decimals: 0 - update_interval: 0.5s - -# MQTT -mqtt: - broker: !secret mqtt_broker - username: !secret mqtt_user - password: !secret mqtt_pass - topic_prefix: ld2410b/conveyor - -# Глобальные переменные для логики подсчета -globals: - - id: item_count_total - type: int - restore_value: true - initial_value: "0" - - - id: item_state - type: int - restore_value: no - initial_value: "0" - - - id: last_trigger_time - type: unsigned long - restore_value: no - initial_value: "0" - - - id: energy_threshold - type: float - restore_value: no - initial_value: "50.0" - - - id: distance_min_cm - type: float - restore_value: no - initial_value: "75.0" - - - id: distance_max_cm - type: float - restore_value: no - initial_value: "200.0" - -text_sensor: - # Статус счетчика - - platform: template - name: "Counter Status" - id: counter_status - icon: "mdi:information" - update_interval: 1s - -# Кнопки управления -button: - - platform: template - name: "Reset Counter" - id: reset_counter - on_press: - - lambda: |- - id(item_count_total) = 0; - id(items_count).publish_state(0); - -# Number для настройки порогов из Home Assistant / MQTT -number: - - platform: template - name: "Energy Threshold" - id: energy_threshold_ui - min_value: 10 - max_value: 100 - step: 5 - initial_value: 50 - set_action: - - lambda: id(energy_threshold) = x; - - - platform: template - name: "Min Distance (cm)" - id: dist_min_ui - min_value: 50 - max_value: 150 - step: 5 - initial_value: 75 - set_action: - - lambda: id(distance_min_cm) = x; - - - platform: template - name: "Max Distance (cm)" - id: dist_max_ui - min_value: 100 - max_value: 500 - step: 10 - initial_value: 200 - set_action: - - lambda: id(distance_max_cm) = x; - -# Основной цикл подсчета + статус -interval: - - interval: 50ms - then: - - lambda: |- - // Получаем текущие значения - float energy = id(g0_energy).state; - float dist = id(moving_dist).state; - - // Проверка валидности дистанции - bool valid_dist = (dist >= id(distance_min_cm)) && (dist <= id(distance_max_cm)); - - // Обновляем отладочный сенсор дистанции - id(trigger_dist).publish_state(valid_dist ? dist : 0); - - // Конечный автомат: 0 = ожидание, 1 = объект в зоне, 2 = debounce после прохода - unsigned long now = millis(); - int current_state = id(item_state); - - // Объект вошел в зону (энергия выше порога + дистанция в пределах) - if (current_state == 0 && energy > id(energy_threshold) && valid_dist) { - id(item_state) = 1; - id(last_trigger_time) = now; - } - - // Объект ушел из зоны — инкремент счетчика - if (current_state == 1 && energy < (id(energy_threshold) * 0.5)) { - id(item_state) = 2; - id(item_count_total)++; - id(items_count).publish_state(id(item_count_total)); - id(last_trigger_time) = now; - } - - // Debounce: ждем 300мс после прохода перед новым детектом - if (current_state == 2 && (now - id(last_trigger_time)) > 300) { - id(item_state) = 0; - } - - // Сброс stuck состояния (объект "завис" в зоне больше 5 сек) - if (current_state == 1 && (now - id(last_trigger_time)) > 5000) { - id(item_state) = 0; - } - - - interval: 1s - then: - - lambda: |- - std::string status = "State: " + std::to_string(id(item_state)) + - " | E: " + std::to_string(id(g0_energy).state, 0) + - " | D: " + std::to_string(id(moving_dist).state, 0) + - " | Count: " + std::to_string(id(item_count_total)); - id(counter_status).publish_state(status.c_str()); - -# Светодиод статуса (GPIO2 на ESP32 devkit) -output: - - platform: gpio - pin: GPIO2 - id: status_led - -light: - - platform: binary - id: led_status - output: status_led - internal: true - -logger: - level: INFO diff --git a/QR /scanner.md b/QR /scanner.md deleted file mode 100644 index 7d15e1f..0000000 --- a/QR /scanner.md +++ /dev/null @@ -1,42 +0,0 @@ -# QR/Barcode Scanner - -Считыватель QR-кодов и штрихкодов на базе **pyzbar** + **OpenCV** для **Raspberry Pi Camera Module v3**. - -## Зависимости - -```bash -sudo apt install libzbar0 libgstreamer1.0-dev -pip install opencv-python pyzbar -``` - -## Запуск - -```bash -python scanner.py -``` - -## Поддерживаемые форматы - -- QR-код -- Code 128, Code 39, EAN-13, EAN-8 -- UPC-A, UPC-E -- Interleaved 2 of 5 -- И другие форматы, поддерживаемые `pyzbar` - -## Управление - -| Клавиша | Действие | -|---------|-----------------------------------| -| `q` | Выход | -| `r` | Сброс списка отсканированных кодов| - -## Как работает - -1. Подключение камеры через GStreamer (`libcamerasrc`) — родной путь для Camera Module v3 -2. Каждый кадр конвертируется в градации серого и передаётся в `pyzbar.decode()` -3. Найденные коды обводятся зелёной рамкой, данные выводятся в консоль и на экран -4. Дубликаты фильтруются — каждый уникальный код выводится один раз (до сброса) - -## Fallback - -Если GStreamer-пайплайн недоступен, скрипт автоматически пробует открыть камеру через `/dev/video0` (V4L2). diff --git a/QR /scanner.py b/QR /scanner.py deleted file mode 100644 index a898d06..0000000 --- a/QR /scanner.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -"""QR/Barcode scanner using pyzbar + OpenCV for Raspberry Pi Camera Module v3.""" - -import cv2 -from pyzbar import pyzbar -import time - - -def main(): - # Camera Module v3 uses libcamera (not V4L2 by default) - # GStreamer pipeline for libcamera - gst_pipeline = ( - "libcamerasrc ! " - "video/x-raw, format=NV12, width=1280, height=960 ! " - "videoconvert ! " - "video/x-raw, format=BGR ! " - "appsink" - ) - - cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER) - - if not cap.isOpened(): - # Fallback: try V4L2 if libcamera pipeline fails - print("GStreamer pipeline failed, trying /dev/video0...") - cap = cv2.VideoCapture(0) - if not cap.isOpened(): - print("Error: Cannot open camera") - return - - cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) - cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 960) - - scanned_codes = set() - - print("Scanner running. Press 'q' to quit, 'r' to reset scanned codes.") - - try: - while True: - ret, frame = cap.read() - if not ret: - print("Failed to grab frame") - break - - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - codes = pyzbar.decode(gray) - - for code in codes: - data = code.data.decode("utf-8") - code_type = code.type - - if data not in scanned_codes: - scanned_codes.add(data) - print(f"[{code_type}] {data} (total unique: {len(scanned_codes)})") - - # Draw bounding box - points = code.polygon - if len(points) == 4: - pts = [(p.x, p.y) for p in points] - for i in range(4): - cv2.line(frame, pts[i], pts[(i + 1) % 4], (0, 255, 0), 3) - - # Draw data label - cv2.putText( - frame, - data[:40], - (points[0].x, points[0].y - 10), - cv2.FONT_HERSHEY_SIMPLEX, - 0.5, - (0, 255, 0), - 2, - ) - - cv2.imshow("Scanner", frame) - - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break - elif key == ord("r"): - scanned_codes.clear() - print("Scanned codes reset.") - - except KeyboardInterrupt: - pass - finally: - cap.release() - cv2.destroyAllWindows() - - -if __name__ == "__main__": - main() diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 2c60a0f..887e0dc 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -51,5 +51,19 @@ services: /server " + scanner: + build: + context: ../scanner + dockerfile: Dockerfile + environment: + SERVER_URL: http://app:8080 + depends_on: + app: + condition: service_started + devices: + - /dev/video0:/dev/video0 + profiles: + - scanner + volumes: postgres_data: \ No newline at end of file diff --git a/scanner/Dockerfile b/scanner/Dockerfile new file mode 100644 index 0000000..a992c4e --- /dev/null +++ b/scanner/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y \ + libzbar0 \ + libzbar-dev \ + libgl1-mesa-glx \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY scanner.py . + +CMD ["python", "scanner.py"] \ No newline at end of file diff --git a/scanner/pyproject.toml b/scanner/pyproject.toml new file mode 100644 index 0000000..1035b59 --- /dev/null +++ b/scanner/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "scanner" +version = "0.1.0" +description = "QR Scanner for rostpoliplast" +requires-python = ">=3.11" +dependencies = [ + "pyzbar==0.1.9", + "requests==2.31.0", + "opencv-python-headless==4.9.0.80", + "Pillow==10.2.0", + "numpy==1.26.4", +] + +[project.optional-dependencies] +dev = ["pytest"] + +[tool.uv] +dev-dependencies = [] \ No newline at end of file diff --git a/scanner/requirements.txt b/scanner/requirements.txt new file mode 100644 index 0000000..92c8b9e --- /dev/null +++ b/scanner/requirements.txt @@ -0,0 +1,6 @@ +# QR Scanner dependencies +pyzbar==0.1.9 +requests==2.31.0 +opencv-python-headless==4.9.0.80 +Pillow==10.2.0 +numpy==1.26.4 \ No newline at end of file diff --git a/scanner/scanner.py b/scanner/scanner.py new file mode 100644 index 0000000..4cc42c4 --- /dev/null +++ b/scanner/scanner.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +import cv2 +import os +import sys +import requests +import json +from urllib.parse import urlparse, parse_qs +from pyzbar.pyzbar import decode as decode_qr + +SERVER_URL = os.getenv("SERVER_URL", "http://localhost:8080") +API_ENDPOINT = f"{SERVER_URL}/api/v1/bales" + + +def extract_type_from_url(url): + parsed = urlparse(url) + params = parse_qs(parsed.query) + if "type" in params: + return params["type"][0] + return parsed.path.split("=")[-1] if "=" in parsed.path else None + + +def scan_and_send(): + cap = cv2.VideoCapture(0) + + if not cap.isOpened(): + print("Error: Cannot open camera", file=sys.stderr) + sys.exit(1) + + print("Scanning for QR codes... Press 'q' to quit") + print(f"Server: {SERVER_URL}") + + while True: + ret, frame = cap.read() + if not ret: + print("Failed to grab frame") + break + + qr_codes = decode_qr(frame) + + for qr in qr_codes: + url_data = qr.data.decode("utf-8") + print(f"Found QR: {url_data}") + + try: + bale_type = extract_type_from_url(url_data) + + if not bale_type: + print("Error: type not found in QR") + continue + + response = requests.post(f"{API_ENDPOINT}?type={bale_type}", timeout=5) + + if response.status_code in (200, 201): + print(f"OK: Bale created - {response.json()}") + else: + print(f"Error: {response.status_code} - {response.text}") + + except Exception as e: + print(f"Request error: {e}") + + cv2.imshow("QR Scanner", frame) + + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + cap.release() + cv2.destroyAllWindows() + + +if __name__ == "__main__": + scan_and_send()