chore: add unique assets from other branches into dan_branch
Bring hardware/spec content from drho1y-mvp_1 (3d_models, arduino_code, backend_control, kicad, specification) so dan_branch holds the shared union of branch files without rewriting other branch tips. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2
3d_models/.gitignore
vendored
Normal file
2
3d_models/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.FCBak
|
||||
/export
|
||||
BIN
3d_models/conveer.FCStd
Normal file
BIN
3d_models/conveer.FCStd
Normal file
Binary file not shown.
5
arduino_code/Test/.gitignore
vendored
Normal file
5
arduino_code/Test/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
10
arduino_code/Test/.vscode/extensions.json
vendored
Normal file
10
arduino_code/Test/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
138
arduino_code/Test/README.md
Normal file
138
arduino_code/Test/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
Вот обновленная и расширенная документация по MQTT-интерфейсу, включающая новую функциональность работы с датчиками расстояния **VL53L0X**, а также оптимизации, появившиеся в коде.
|
||||
|
||||
---
|
||||
|
||||
# 📡 Документация по MQTT интерфейсу (ESP32 + TMC2209 + Servo + VL53L0X)
|
||||
|
||||
## 📌 Общая информация
|
||||
- **Архитектура**: ESP32 (FreeRTOS задача `mqttTask`).
|
||||
- **Период опроса телеметрии**: 500 мс.
|
||||
- **Оптимизация трафика**:
|
||||
1. Публикация данных происходит **только при изменении значения** (строгое кэширование).
|
||||
2. Используется статический буфер (`intToString`/`uintToString`) вместо динамического класса `String` для экономии памяти и предотвращения фрагментации кучи.
|
||||
- **Префиксы**:
|
||||
- `.../control/...` — топики для **отправки команд** устройству (подписка).
|
||||
- `.../feedback/...` — топики для **получения статуса/телеметрии** от устройства (публикация).
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 1. Управление шаговым двигателем (Motor Control)
|
||||
|
||||
| Топик | Тип данных | Описание | Пример Payload |
|
||||
| :--- | :---: | :--- | :--- |
|
||||
| `motor/control/rpm` | Integer | Целевая скорость в об/мин (RPM). Отрицательные значения включают реверс. | `-150`, `0`, `300` |
|
||||
| `motor/control/driver` | String | Аппаратное вкл/выкл драйвера (пин `EN_PIN`). `on` = LOW (вкл), иначе = HIGH (выкл). | `on`, `off` |
|
||||
| `motor/control/totalsteps/reset`| Any | Сброс счетчика шагов в ноль. Устройство сразу опубликует `"0"` в feedback. | `1`, `reset` |
|
||||
|
||||
### Телеметрия двигателя (Motor Feedback)
|
||||
*(Публикуется только при изменении)*
|
||||
- `motor/feedback/rpm` (Integer): Текущая скорость.
|
||||
- `motor/feedback/totalsteps` (Integer): Общее количество шагов.
|
||||
- `motor/feedback/is_run` (String: `true`/`false`): Двигатель движется.
|
||||
- `motor/feedback/driver/status` (String: `on`/`off`): Общий статус драйвера.
|
||||
- `motor/feedback/tmc/status` (String: `on`/`off`): Статус программного включения TMC.
|
||||
|
||||
#### Детальная телеметрия TMC2209 (`motor/feedback/tmc/...`)
|
||||
- `current_percent` (Integer): Текущий % рабочего тока.
|
||||
- `microsteps` (Integer): Текущий режим микрошага.
|
||||
- `sg_result` (Integer): Текущее значение StallGuard (нагрузка).
|
||||
- `interstep_duration` (Integer): Длительность между шагами.
|
||||
- `status/over_temp` (String: `true`/`false`): Перегрев.
|
||||
- `status/short_to_ground` (String: `true`/`false`): КЗ на землю.
|
||||
- `status/open_load` (String: `true`/`false`): Обрыв нагрузки.
|
||||
- `status/stealth_chop_active` (String: `true`/`false`): Активен ли StealthChop.
|
||||
- `status/standstill` (String: `true`/`false`): Двигатель в покое.
|
||||
- `status/current_scaling` (Integer): Внутренний масштабный коэффициент тока.
|
||||
|
||||
---
|
||||
|
||||
## 🦾 2. Управление сервоприводами (Servo Control)
|
||||
|
||||
Поддержка нескольких каналов (`{channel}` от `0` до `MAX_SERVOS - 1`).
|
||||
|
||||
### Команды
|
||||
| Топик (пример для канала 0) | Тип данных | Описание | Пример Payload |
|
||||
| :--- | :---: | :--- | :--- |
|
||||
| `servo/control/0/angle` | Integer (0-180) | Установить угол поворота. | `90` |
|
||||
| `servo/control/0/enable` | String | Включить (`on`, `1`, `true`) или выключить. | `on` |
|
||||
|
||||
### Обратная связь
|
||||
- `servo/0/feedback/status` (String: `on`/`off`): Статус питания сервопривода.
|
||||
- `servo/0/feedback/angle` (Integer): Текущий установленный угол.
|
||||
|
||||
---
|
||||
|
||||
## 📏 3. Управление датчиками VL53L0X (Sensor Control) **(НОВОЕ)**
|
||||
|
||||
Поддержка до 8 каналов (`VL53L0X_MAX_CHANNELS = 8`). Топики используют параметр `{channel}` (0–7).
|
||||
|
||||
| Топик | Тип данных | Описание | Пример Payload |
|
||||
| :--- | :---: | :--- | :--- |
|
||||
| `sensor/control/mode` | Integer | Установка глобального режима измерения (0 до `MODE_COUNT - 1`). | `0`, `1` |
|
||||
| `sensor/control/mode_name` | String | *Заглушка/Логирование.* Принимает имя режима для отладки. | `LongRange` |
|
||||
| `sensor/control/calibrate/start/{ch}`| Integer | Начало калибровки: указать близкое расстояние в мм. | `50` |
|
||||
| `sensor/control/calibrate/finish/{ch}`| Integer | Завершение калибровки: указать дальнее расстояние в мм. | `500` |
|
||||
| `sensor/control/clear_cal/{ch}` | Any | Сбросить калибровку для указанного канала. | `1` |
|
||||
| `sensor/control/enable/{ch}` | String | Включить (`on`, `1`, `true`) или выключить конкретный канал. | `on` |
|
||||
| `sensor/control/publish_all` | Any | **Принудительный сброс кэша.** Заставляет устройство немедленно опубликовать текущие значения всех датчиков, даже если они не изменились. | `1` |
|
||||
|
||||
---
|
||||
|
||||
## 📊 4. Телеметрия датчиков VL53L0X (Sensor Feedback) **(НОВОЕ)**
|
||||
|
||||
### Глобальная информация о режиме
|
||||
Публикуется только при смене режима измерения:
|
||||
- `sensor/feedback/mode` (String): Человекочитаемое имя текущего режима (например, "Default", "LongRange").
|
||||
- `sensor/feedback/mode_id` (Integer): Числовой ID текущего режима.
|
||||
- `sensor/feedback/max_range` (Integer): Максимальная дальность для текущего режима (в мм).
|
||||
|
||||
### Постатусная информация по каналам (`{channel}` = 0..7)
|
||||
*(Публикуется только при изменении состояния или значения)*
|
||||
|
||||
| Топик (пример для канала 0) | Тип данных | Описание |
|
||||
| :--- | :---: | :--- |
|
||||
| `sensor/feedback/0/status` | String (`on`/`off`) | Включен ли логически данный канал. |
|
||||
| `sensor/feedback/0/calibrated` | String (`true`/`false`)| Была ли проведена калибровка для этого канала. |
|
||||
| `sensor/feedback/0/distance` | Integer или String | **Калиброванное** расстояние в мм. Если значение `65535` (ошибка/вне диапазона), публикуется строка `"out_of_range"`. |
|
||||
| `sensor/feedback/0/raw` | Integer | **Сырое** (некалиброванное) значение расстояния в мм. |
|
||||
|
||||
*Примечание: Топики `distance` и `raw` публикуются только если канал активен (`vl53l0xIsChannelActive`).*
|
||||
|
||||
---
|
||||
|
||||
## 💡 Важные особенности реализации (Обновлено)
|
||||
|
||||
1. **Безопасная работа со строками**: В новом коде добавлены функции `intToString` и `uintToString`, использующие статические буферы. Это полностью устраняет риск фрагментации памяти (heap fragmentation) при частой публикации телеметрии, который был присущ использованию класса `String`.
|
||||
2. **Принудительная публикация**: Топик `sensor/control/publish_all` сбрасывает кэш значений `distance` и `raw` на `65535`. При следующем цикле (через 500 мс) система "увидит" изменение и гарантированно отправит актуальные данные. Это полезно при подключении нового клиента, которому нужно получить текущее состояние без перезагрузки устройства.
|
||||
3. **Обработка ошибок дальности**: Если датчик возвращает `65535` (стандартный код ошибки "вне диапазона" или сбоя измерения для VL53L0X), в топик `distance` публикуется понятная строка `"out_of_range"`, а не число, что упрощает обработку на стороне клиента.
|
||||
4. **Изоляция каналов**: Цикл телеметрии VL53L0X предварительно проверяет `vl53l0xIsChannelPresent(ch)`, поэтому несуществующие или отключенные на аппаратном уровне каналы не создают лишнего трафика.
|
||||
|
||||
---
|
||||
|
||||
### 🛠️ Примеры использования (CLI / mosquitto)
|
||||
|
||||
```bash
|
||||
# --- Двигатель ---
|
||||
mosquitto_pub -t "motor/control/rpm" -m "100"
|
||||
mosquitto_pub -t "motor/control/tmc/stealthchop" -m "on"
|
||||
|
||||
# --- Сервопривод (канал 0) ---
|
||||
mosquitto_pub -t "servo/control/0/enable" -m "on"
|
||||
mosquitto_pub -t "servo/control/0/angle" -m "90"
|
||||
|
||||
# --- Датчики VL53L0X ---
|
||||
# Включить канал 1
|
||||
mosquitto_pub -t "sensor/control/enable/1" -m "on"
|
||||
|
||||
# Начать калибровку канала 1 (близкая точка 50 мм)
|
||||
mosquitto_pub -t "sensor/control/calibrate/start/1" -m "50"
|
||||
# ... передвинуть объект ...
|
||||
# Завершить калибровку канала 1 (дальняя точка 400 мм)
|
||||
mosquitto_pub -t "sensor/control/calibrate/finish/1" -m "400"
|
||||
|
||||
# Принудительно запросить публикацию всех текущих показаний датчиков
|
||||
mosquitto_pub -t "sensor/control/publish_all" -m "1"
|
||||
|
||||
# Подписаться на все события датчиков для отладки
|
||||
mosquitto_sub -t "sensor/feedback/#" -v
|
||||
```
|
||||
37
arduino_code/Test/include/README
Normal file
37
arduino_code/Test/include/README
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the convention is to give header files names that end with `.h'.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
46
arduino_code/Test/lib/README
Normal file
46
arduino_code/Test/lib/README
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into the executable file.
|
||||
|
||||
The source code of each library should be placed in a separate directory
|
||||
("lib/your_library_name/[Code]").
|
||||
|
||||
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
Example contents of `src/main.c` using Foo and Bar:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries by scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
23
arduino_code/Test/platformio.ini
Normal file
23
arduino_code/Test/platformio.ini
Normal file
@@ -0,0 +1,23 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:upesy_wroom]
|
||||
platform = espressif32
|
||||
board = upesy_wroom
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
upload_speed = 921600
|
||||
upload_port = /dev/ttyUSB0
|
||||
lib_deps =
|
||||
knolleary/PubSubClient@^2.8
|
||||
janelia-arduino/TMC2209@^9.4.0
|
||||
madhephaestus/ESP32Servo@^3.2.1
|
||||
adafruit/Adafruit PWM Servo Driver Library@^3.0.3
|
||||
pololu/VL53L0X@^1.3.1
|
||||
38
arduino_code/Test/src/config.cpp
Normal file
38
arduino_code/Test/src/config.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#include "config.h"
|
||||
|
||||
const char* WIFI_SSID = "TP-Link_3E5C";
|
||||
const char* WIFI_PASS = "12697571";
|
||||
const char* MQTT_SERVER = "192.168.0.200";
|
||||
const int MQTT_PORT = 1883;
|
||||
const char* MQTT_USER = "test";
|
||||
const char* MQTT_PASS = "1234";
|
||||
const char* MQTT_CLIENT_ID = "ESP32_Stepper";
|
||||
|
||||
// UART2: RX=16, TX=17
|
||||
HardwareSerial& TMC_SERIAL = Serial2;
|
||||
const uint32_t TMC_BAUD_RATE = 115200;
|
||||
const uint8_t TMC_SERIAL_ADDRESS = 0; // Если MS1 и MS2 на GND
|
||||
|
||||
const int16_t TMC_RX_PIN = 16;
|
||||
const int16_t TMC_TX_PIN = 17;
|
||||
const int EN_PIN = 18;
|
||||
|
||||
const int STEPS_PER_REVOLUTION = 200;
|
||||
const unsigned long RAMP_DURATION_MS = 2000;
|
||||
|
||||
// Ток задается в процентах от максимума (зависит от R_sense).
|
||||
// Для R_sense=0.11 Ом, 100% ~ 1.77А RMS. Для R_sense=0.15 Ом, 100% ~ 1.2А RMS.
|
||||
const uint8_t TMC_RUN_CURRENT_PERCENT = 50; // 50% тока при движении
|
||||
const uint8_t TMC_HOLD_CURRENT_PERCENT = 20; // 20% тока в простое
|
||||
const uint8_t TMC_STALL_GUARD_THRESH = 10;
|
||||
const uint16_t TMC_MICROSTEPS = 1;
|
||||
|
||||
// I2C Настройка
|
||||
const int16_t I2C_SDA_PIN = 21;
|
||||
const int16_t I2C_SCL_PIN = 22;
|
||||
|
||||
|
||||
// ========== SERVO CONFIGURATION ==========
|
||||
#define SERVO_DEFAULT_CHANNEL 0
|
||||
#define SERVO_MIN_ANGLE 0
|
||||
#define SERVO_MAX_ANGLE 180
|
||||
38
arduino_code/Test/src/config.h
Normal file
38
arduino_code/Test/src/config.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
// WiFi & MQTT
|
||||
extern const char* WIFI_SSID;
|
||||
extern const char* WIFI_PASS;
|
||||
extern const char* MQTT_SERVER;
|
||||
extern const int MQTT_PORT;
|
||||
extern const char* MQTT_USER;
|
||||
extern const char* MQTT_PASS;
|
||||
extern const char* MQTT_CLIENT_ID;
|
||||
|
||||
// UART for TMC2209
|
||||
extern HardwareSerial& TMC_SERIAL;
|
||||
extern const uint32_t TMC_BAUD_RATE;
|
||||
extern const uint8_t TMC_SERIAL_ADDRESS; // Адрес драйвера (0-3)
|
||||
extern const int16_t TMC_RX_PIN;
|
||||
extern const int16_t TMC_TX_PIN;
|
||||
extern const int EN_PIN;
|
||||
|
||||
// Motor Params
|
||||
extern const int STEPS_PER_REVOLUTION; // Базовые шаги мотора (обычно 200)
|
||||
extern const unsigned long RAMP_DURATION_MS;
|
||||
|
||||
// TMC2209 Defaults (Токи в процентах 0-100%)
|
||||
extern const uint8_t TMC_RUN_CURRENT_PERCENT;
|
||||
extern const uint8_t TMC_HOLD_CURRENT_PERCENT;
|
||||
extern const uint8_t TMC_STALL_GUARD_THRESH;
|
||||
extern const uint16_t TMC_MICROSTEPS;
|
||||
|
||||
// I2C Настройка
|
||||
extern const int16_t I2C_SDA_PIN;
|
||||
extern const int16_t I2C_SCL_PIN;
|
||||
|
||||
#endif
|
||||
42
arduino_code/Test/src/main.cpp
Normal file
42
arduino_code/Test/src/main.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include <Arduino.h>
|
||||
#include <TMC2209.h>
|
||||
#include "config.h"
|
||||
#include "motor.h"
|
||||
#include "vl53l0x_sensor.h"
|
||||
#include "mqtt_handler.h"
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
// Инициализация мотора (Core 1 context initially)
|
||||
motorInit();
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
sensorTask,
|
||||
"SensorTask",
|
||||
4096,
|
||||
NULL,
|
||||
1,
|
||||
NULL,
|
||||
0
|
||||
);
|
||||
|
||||
// Запуск задачи MQTT на Core 0
|
||||
xTaskCreatePinnedToCore(
|
||||
mqttTask,
|
||||
"MQTT_Task",
|
||||
20480,
|
||||
NULL,
|
||||
3,
|
||||
NULL,
|
||||
0 // CORE 0
|
||||
);
|
||||
|
||||
Serial.println("System Initialized. Multi-core ready.");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Loop выполняется на Core 1
|
||||
motorLoop();
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
295
arduino_code/Test/src/motor.cpp
Normal file
295
arduino_code/Test/src/motor.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
#include "motor.h"
|
||||
#include "config.h"
|
||||
|
||||
static TMC2209 stepper_driver;
|
||||
static bool tmc_initialized = false;
|
||||
static SemaphoreHandle_t tmc_uart_mutex = NULL;
|
||||
|
||||
volatile unsigned long total_steps = 0;
|
||||
static int target_rpm = 0;
|
||||
static int current_rpm_display = 0;
|
||||
|
||||
static bool is_ramping = false;
|
||||
static unsigned long ramp_start_ms = 0;
|
||||
static float start_speed_sps = 0;
|
||||
static float end_speed_sps = 0;
|
||||
static float current_speed_sps = 0;
|
||||
static int32_t last_vactual = 0;
|
||||
static bool velocity_sent = false; // Флаг для отправки хотя бы раз
|
||||
|
||||
static uint16_t current_microsteps = TMC_MICROSTEPS;
|
||||
static uint8_t current_run_percent = TMC_RUN_CURRENT_PERCENT;
|
||||
|
||||
#define TMC_LOCK() xSemaphoreTake(tmc_uart_mutex, portMAX_DELAY)
|
||||
#define TMC_UNLOCK() xSemaphoreGive(tmc_uart_mutex)
|
||||
|
||||
const float TMC_FCLK = 12800000.0;
|
||||
const float VACTUAL_FACTOR = 8388608.0 / TMC_FCLK;
|
||||
|
||||
int32_t calculateVActual(float microsteps_per_second) {
|
||||
return (int32_t)(microsteps_per_second * VACTUAL_FACTOR);
|
||||
}
|
||||
|
||||
void motorInit() {
|
||||
pinMode(EN_PIN, OUTPUT);
|
||||
digitalWrite(EN_PIN, LOW);
|
||||
tmc_uart_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
TMC_SERIAL.begin(TMC_BAUD_RATE, SERIAL_8N1, TMC_RX_PIN, TMC_TX_PIN);
|
||||
delay(500);
|
||||
|
||||
TMC_LOCK();
|
||||
|
||||
stepper_driver.setup(TMC_SERIAL, TMC_BAUD_RATE,
|
||||
(TMC2209::SerialAddress)TMC_SERIAL_ADDRESS,
|
||||
TMC_RX_PIN, TMC_TX_PIN);
|
||||
|
||||
delay(200);
|
||||
|
||||
// Проверка связи
|
||||
if (!stepper_driver.isCommunicating()) {
|
||||
Serial.println("ERROR: TMC2209 not communicating!");
|
||||
TMC_UNLOCK();
|
||||
tmc_initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("TMC2209 communicating OK");
|
||||
|
||||
// Базовая настройка
|
||||
stepper_driver.setMicrostepsPerStep(TMC_MICROSTEPS);
|
||||
stepper_driver.setRunCurrent(TMC_RUN_CURRENT_PERCENT);
|
||||
stepper_driver.setHoldCurrent(TMC_HOLD_CURRENT_PERCENT);
|
||||
stepper_driver.setHoldDelay(7);
|
||||
stepper_driver.setStallGuardThreshold(TMC_STALL_GUARD_THRESH);
|
||||
|
||||
stepper_driver.enableAutomaticCurrentScaling();
|
||||
stepper_driver.enableAutomaticGradientAdaptation();
|
||||
|
||||
// КРИТИЧНО: Отключаем StealthChop для работы moveAtVelocity()!
|
||||
stepper_driver.disableStealthChop();
|
||||
delay(10);
|
||||
|
||||
// Включаем CoolStep для энергосбережения
|
||||
stepper_driver.enableCoolStep();
|
||||
|
||||
// Программное включение драйвера
|
||||
stepper_driver.enable();
|
||||
delay(100);
|
||||
|
||||
tmc_initialized = stepper_driver.isSetupAndCommunicating();
|
||||
|
||||
if (tmc_initialized) {
|
||||
Serial.println("TMC2209 initialized successfully");
|
||||
TMC2209::Settings settings = stepper_driver.getSettings();
|
||||
Serial.printf("Run: %d%%, Hold: %d%%, Microsteps: %d, StealthChop: %s\n",
|
||||
settings.irun_percent, settings.ihold_percent,
|
||||
settings.microsteps_per_step, settings.stealth_chop_enabled ? "ON" : "OFF");
|
||||
} else {
|
||||
Serial.println("ERROR: TMC2209 setup failed!");
|
||||
}
|
||||
|
||||
TMC_UNLOCK();
|
||||
|
||||
current_microsteps = TMC_MICROSTEPS;
|
||||
current_run_percent = TMC_RUN_CURRENT_PERCENT;
|
||||
}
|
||||
|
||||
void motorLoop() {
|
||||
if (is_ramping) {
|
||||
unsigned long now = millis();
|
||||
unsigned long elapsed = now - ramp_start_ms;
|
||||
|
||||
if (elapsed >= RAMP_DURATION_MS) {
|
||||
is_ramping = false;
|
||||
current_speed_sps = end_speed_sps;
|
||||
} else {
|
||||
float progress = (float)elapsed / RAMP_DURATION_MS;
|
||||
current_speed_sps = start_speed_sps + (end_speed_sps - start_speed_sps) * progress;
|
||||
}
|
||||
|
||||
int32_t vactual = calculateVActual(current_speed_sps);
|
||||
|
||||
// Отправляем если: скорость изменилась ИЛИ это первая отправка в рампе
|
||||
if (abs(vactual - last_vactual) >= 0 || !velocity_sent) {
|
||||
TMC_LOCK();
|
||||
stepper_driver.moveAtVelocity(vactual);
|
||||
TMC_UNLOCK();
|
||||
last_vactual = vactual;
|
||||
velocity_sent = true;
|
||||
|
||||
Serial.printf("VACTUAL: %d (SPS: %.1f, RPM: %d)\n",
|
||||
vactual, current_speed_sps, current_rpm_display);
|
||||
}
|
||||
|
||||
unsigned long steps_per_rev = (unsigned long)STEPS_PER_REVOLUTION * current_microsteps;
|
||||
current_rpm_display = ((unsigned long)abs(current_speed_sps) * 60) / steps_per_rev;
|
||||
} else if (!velocity_sent && current_speed_sps == 0) {
|
||||
// Если мотор стоит и скорость не отправлялась - отправляем 0
|
||||
TMC_LOCK();
|
||||
stepper_driver.moveAtVelocity(0);
|
||||
TMC_UNLOCK();
|
||||
velocity_sent = true;
|
||||
}
|
||||
if (current_speed_sps != 0) {
|
||||
total_steps += (unsigned long)(abs(current_speed_sps) * 0.005);
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
|
||||
void setTargetRPM(int rpm) {
|
||||
target_rpm = rpm;
|
||||
if (rpm != 0 && current_rpm_display < 5) resetSteps();
|
||||
|
||||
unsigned long steps_per_rev = (unsigned long)STEPS_PER_REVOLUTION * current_microsteps;
|
||||
|
||||
start_speed_sps = current_speed_sps;
|
||||
end_speed_sps = (rpm != 0) ? ((float)rpm * steps_per_rev) / 60.0f : 0;
|
||||
|
||||
ramp_start_ms = millis();
|
||||
is_ramping = true;
|
||||
velocity_sent = false; // Сбрасываем флаг для новой отправки
|
||||
|
||||
Serial.printf("Target RPM: %d -> SPS: %.1f\n", rpm, end_speed_sps);
|
||||
}
|
||||
|
||||
void resetSteps() {
|
||||
total_steps = 0;
|
||||
}
|
||||
|
||||
unsigned long getMotorSteps() { return total_steps; }
|
||||
int getCurrentRPM() { return current_rpm_display; }
|
||||
bool isMotorRunning() { return (abs(current_speed_sps) > 1); }
|
||||
|
||||
void tmcSetCurrentPercent(uint8_t run_percent, uint8_t hold_percent) {
|
||||
if (!tmc_initialized) return;
|
||||
TMC_LOCK();
|
||||
stepper_driver.setAllCurrentValues(run_percent, hold_percent, 7);
|
||||
TMC_UNLOCK();
|
||||
current_run_percent = run_percent;
|
||||
}
|
||||
|
||||
void tmcSetMicrosteps(uint16_t ms) {
|
||||
if (!tmc_initialized) return;
|
||||
TMC_LOCK();
|
||||
stepper_driver.setMicrostepsPerStep(ms);
|
||||
TMC_UNLOCK();
|
||||
current_microsteps = ms;
|
||||
if (target_rpm != 0) setTargetRPM(target_rpm);
|
||||
}
|
||||
|
||||
void tmcSetStallGuard(uint8_t threshold) {
|
||||
if (!tmc_initialized) return;
|
||||
TMC_LOCK();
|
||||
stepper_driver.setStallGuardThreshold(threshold);
|
||||
TMC_UNLOCK();
|
||||
}
|
||||
|
||||
void tmcSoftwareEnable(bool enable) {
|
||||
if (!tmc_initialized) return;
|
||||
TMC_LOCK();
|
||||
if (enable) {
|
||||
stepper_driver.enable();
|
||||
// После enable нужно заново отправить скорость
|
||||
velocity_sent = false;
|
||||
} else {
|
||||
stepper_driver.moveAtVelocity(0);
|
||||
stepper_driver.disable();
|
||||
last_vactual = 0;
|
||||
current_speed_sps = 0;
|
||||
is_ramping = false;
|
||||
}
|
||||
TMC_UNLOCK();
|
||||
}
|
||||
|
||||
void tmcSetStealthChop(bool enable) {
|
||||
if (!tmc_initialized) return;
|
||||
TMC_LOCK();
|
||||
if (enable) {
|
||||
stepper_driver.enableStealthChop();
|
||||
// В StealthChop VACTUAL не работает, останавливаем мотор
|
||||
stepper_driver.moveAtVelocity(0);
|
||||
last_vactual = 0;
|
||||
current_speed_sps = 0;
|
||||
} else {
|
||||
stepper_driver.disableStealthChop();
|
||||
velocity_sent = false;
|
||||
}
|
||||
TMC_UNLOCK();
|
||||
}
|
||||
|
||||
void tmcSetCoolStep(bool enable) {
|
||||
if (!tmc_initialized) return;
|
||||
TMC_LOCK();
|
||||
enable ? stepper_driver.enableCoolStep() : stepper_driver.disableCoolStep();
|
||||
TMC_UNLOCK();
|
||||
}
|
||||
|
||||
bool tmcIsInitialized() { return tmc_initialized; }
|
||||
|
||||
bool tmcIsCommunicating() {
|
||||
if (!tmc_initialized) return false;
|
||||
TMC_LOCK();
|
||||
bool res = stepper_driver.isCommunicating();
|
||||
TMC_UNLOCK();
|
||||
return res;
|
||||
}
|
||||
|
||||
// Функция проверки состояния EN_PIN
|
||||
bool checkDriverStatus() {
|
||||
bool en_state = (digitalRead(EN_PIN) == LOW);
|
||||
return en_state;
|
||||
}
|
||||
|
||||
// Функция проверки программного состояния TMC
|
||||
bool checkTmcSoftwareEnable() {
|
||||
TMC2209::Settings s = tmcGetSettings();
|
||||
bool tmc_state = s.software_enabled;
|
||||
return tmc_state;
|
||||
}
|
||||
|
||||
uint16_t tmcGetMicrostepsSetting() { return current_microsteps; }
|
||||
uint8_t tmcGetRunCurrentPercent() { return current_run_percent; }
|
||||
|
||||
TMC2209::Status tmcGetStatus() {
|
||||
TMC2209::Status s = {};
|
||||
if (!tmc_initialized) return s;
|
||||
TMC_LOCK();
|
||||
s = stepper_driver.getStatus();
|
||||
TMC_UNLOCK();
|
||||
return s;
|
||||
}
|
||||
|
||||
TMC2209::Settings tmcGetSettings() {
|
||||
TMC2209::Settings s = {};
|
||||
if (!tmc_initialized) return s;
|
||||
TMC_LOCK();
|
||||
s = stepper_driver.getSettings();
|
||||
TMC_UNLOCK();
|
||||
return s;
|
||||
}
|
||||
|
||||
uint16_t tmcGetStallGuardResult() {
|
||||
if (!tmc_initialized) return 0;
|
||||
TMC_LOCK();
|
||||
uint16_t res = stepper_driver.getStallGuardResult();
|
||||
TMC_UNLOCK();
|
||||
return res;
|
||||
}
|
||||
|
||||
uint32_t tmcGetInterstepDuration() {
|
||||
if (!tmc_initialized) return 0;
|
||||
TMC_LOCK();
|
||||
uint32_t res = stepper_driver.getInterstepDuration();
|
||||
TMC_UNLOCK();
|
||||
return res;
|
||||
}
|
||||
|
||||
uint16_t tmcGetMicrostepCounter() {
|
||||
if (!tmc_initialized) return 0;
|
||||
TMC_LOCK();
|
||||
uint16_t res = stepper_driver.getMicrostepCounter();
|
||||
TMC_UNLOCK();
|
||||
return res;
|
||||
}
|
||||
42
arduino_code/Test/src/motor.h
Normal file
42
arduino_code/Test/src/motor.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#ifndef MOTOR_H
|
||||
#define MOTOR_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <TMC2209.h>
|
||||
|
||||
void motorInit();
|
||||
void motorLoop();
|
||||
|
||||
// Управление движением (через UART VACTUAL)
|
||||
void setTargetRPM(int rpm); // Поддерживает отрицательные значения для реверса!
|
||||
void resetSteps();
|
||||
|
||||
// Геттеры состояния движения
|
||||
unsigned long getMotorSteps(); // Считается программно
|
||||
int getCurrentRPM();
|
||||
bool isMotorRunning();
|
||||
|
||||
// Управление TMC2209 через UART
|
||||
void tmcSetCurrentPercent(uint8_t run_percent, uint8_t hold_percent);
|
||||
void tmcSetMicrosteps(uint16_t ms);
|
||||
void tmcSetStallGuard(uint8_t threshold);
|
||||
void tmcSoftwareEnable(bool enable); // Вкл/Выкл драйвер программно
|
||||
void tmcSetStealthChop(bool enable);
|
||||
void tmcSetCoolStep(bool enable);
|
||||
|
||||
// Расширенная телеметрия
|
||||
bool tmcIsInitialized();
|
||||
bool tmcIsCommunicating();
|
||||
uint16_t tmcGetMicrostepsSetting();
|
||||
uint8_t tmcGetRunCurrentPercent();
|
||||
|
||||
// Структуры статусов для MQTT
|
||||
TMC2209::Status tmcGetStatus();
|
||||
TMC2209::Settings tmcGetSettings();
|
||||
uint16_t tmcGetStallGuardResult();
|
||||
uint32_t tmcGetInterstepDuration();
|
||||
uint16_t tmcGetMicrostepCounter();
|
||||
bool checkDriverStatus();
|
||||
bool checkTmcSoftwareEnable();
|
||||
|
||||
#endif
|
||||
506
arduino_code/Test/src/mqtt_handler.cpp
Normal file
506
arduino_code/Test/src/mqtt_handler.cpp
Normal file
@@ -0,0 +1,506 @@
|
||||
#include "mqtt_handler.h"
|
||||
#include "config.h"
|
||||
#include "motor.h"
|
||||
#include "servo_control.h"
|
||||
#include "vl53l0x_sensor.h"
|
||||
|
||||
static WiFiClient espClient;
|
||||
static PubSubClient client(espClient);
|
||||
|
||||
// Кэш телеметрии
|
||||
static unsigned long last_feedback_time = 0;
|
||||
static int last_pub_rpm = -1;
|
||||
static unsigned long last_pub_steps = -1;
|
||||
static int last_pub_is_run = -1;
|
||||
|
||||
// TMC Кэш
|
||||
static uint16_t last_pub_sg = 65535;
|
||||
static uint32_t last_pub_interstep = 0;
|
||||
static uint8_t last_pub_current_pct = 255;
|
||||
static uint16_t last_pub_microsteps = 0;
|
||||
|
||||
// Статусы (битовые флаги)
|
||||
static int last_pub_over_temp = -1;
|
||||
static int last_pub_short_gnd = -1;
|
||||
static int last_pub_open_load = -1;
|
||||
static int last_pub_stealth_active = -1;
|
||||
static int last_pub_standstill = -1;
|
||||
static int last_pub_driver_status = -1;
|
||||
static int last_pub_tmc_software_enable = -1;
|
||||
static uint8_t last_pub_current_scaling = 255;
|
||||
|
||||
// ============================================
|
||||
// КЭШИРОВАНИЕ ДЛЯ VL53L0X
|
||||
// ============================================
|
||||
|
||||
#define VL53L0X_MAX_CHANNELS 8
|
||||
static uint16_t last_pub_vl53_distance[VL53L0X_MAX_CHANNELS] = {65535};
|
||||
static uint16_t last_pub_vl53_raw[VL53L0X_MAX_CHANNELS] = {65535};
|
||||
static int last_pub_vl53_status[VL53L0X_MAX_CHANNELS] = {-1};
|
||||
static int last_pub_vl53_calibrated[VL53L0X_MAX_CHANNELS] = {-1};
|
||||
static MeasurementMode last_pub_vl53_mode = MODE_COUNT;
|
||||
|
||||
// ============================================
|
||||
// БУФЕРЫ ДЛЯ ПРЕОБРАЗОВАНИЯ
|
||||
// ============================================
|
||||
|
||||
static char int_buffer[16];
|
||||
static char uint_buffer[16];
|
||||
|
||||
static const char* intToString(int value) {
|
||||
snprintf(int_buffer, sizeof(int_buffer), "%d", value);
|
||||
return int_buffer;
|
||||
}
|
||||
|
||||
static const char* uintToString(unsigned long value) {
|
||||
snprintf(uint_buffer, sizeof(uint_buffer), "%lu", value);
|
||||
return uint_buffer;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WIFI И MQTT ПОДКЛЮЧЕНИЕ
|
||||
// ============================================
|
||||
|
||||
static void setup_wifi() {
|
||||
Serial.print("Connecting to WiFi");
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
Serial.print(".");
|
||||
}
|
||||
Serial.println("\nWiFi Connected");
|
||||
}
|
||||
|
||||
static void resetAllCaches() {
|
||||
// Motor
|
||||
last_pub_rpm = -1;
|
||||
last_pub_steps = (unsigned long)-1;
|
||||
last_pub_is_run = -1;
|
||||
last_pub_sg = 65535;
|
||||
last_pub_interstep = 0;
|
||||
last_pub_current_pct = 255;
|
||||
last_pub_microsteps = 0;
|
||||
last_pub_over_temp = -1;
|
||||
last_pub_short_gnd = -1;
|
||||
last_pub_open_load = -1;
|
||||
last_pub_stealth_active = -1;
|
||||
last_pub_standstill = -1;
|
||||
last_pub_current_scaling = 255;
|
||||
last_pub_driver_status = -1;
|
||||
last_pub_tmc_software_enable = -1;
|
||||
|
||||
// VL53L0X
|
||||
for (int i = 0; i < VL53L0X_MAX_CHANNELS; i++) {
|
||||
last_pub_vl53_distance[i] = 65535;
|
||||
last_pub_vl53_raw[i] = 65535;
|
||||
last_pub_vl53_status[i] = -1;
|
||||
last_pub_vl53_calibrated[i] = -1;
|
||||
}
|
||||
last_pub_vl53_mode = MODE_COUNT;
|
||||
}
|
||||
|
||||
static void subscribeToAllTopics() {
|
||||
// Motor
|
||||
client.subscribe("motor/control/rpm");
|
||||
client.subscribe("motor/control/driver");
|
||||
client.subscribe("motor/control/totalsteps/reset");
|
||||
client.subscribe("motor/control/tmc/current_percent");
|
||||
client.subscribe("motor/control/tmc/microsteps");
|
||||
client.subscribe("motor/control/tmc/stallguard");
|
||||
client.subscribe("motor/control/tmc/enable");
|
||||
client.subscribe("motor/control/tmc/stealthchop");
|
||||
client.subscribe("motor/control/tmc/coolstep");
|
||||
|
||||
// Servo
|
||||
client.subscribe("servo/control/+/#");
|
||||
|
||||
// VL53L0X
|
||||
client.subscribe("sensor/control/mode");
|
||||
client.subscribe("sensor/control/mode_name");
|
||||
client.subscribe("sensor/control/calibrate/start/+");
|
||||
client.subscribe("sensor/control/calibrate/finish/+");
|
||||
client.subscribe("sensor/control/clear_cal/+");
|
||||
client.subscribe("sensor/control/enable/+");
|
||||
client.subscribe("sensor/control/publish_all");
|
||||
}
|
||||
|
||||
static void reconnect() {
|
||||
while (!client.connected()) {
|
||||
if (client.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) {
|
||||
resetAllCaches();
|
||||
subscribeToAllTopics();
|
||||
Serial.println("MQTT Connected and subscribed");
|
||||
} else {
|
||||
Serial.printf("MQTT connection failed, rc=%d, retrying...\n", client.state());
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ОБРАБОТКА ВХОДЯЩИХ MQTT КОМАНД
|
||||
// ============================================
|
||||
static void callback(char* topic, byte* payload, unsigned int length) {
|
||||
char msg[length + 1];
|
||||
memcpy(msg, payload, length);
|
||||
msg[length] = '\0';
|
||||
|
||||
if (strcmp(topic, "motor/control/rpm") == 0) {
|
||||
setTargetRPM(atoi(msg)); // Поддерживает отрицательные для реверса!
|
||||
}
|
||||
else if (strcmp(topic, "motor/control/driver") == 0) {
|
||||
// TMC2209: LOW = Enabled, HIGH = Disabled
|
||||
bool enable = (strcmp(msg, "on") == 0);
|
||||
digitalWrite(EN_PIN, enable ? LOW : HIGH);
|
||||
}
|
||||
else if (strcmp(topic, "motor/control/totalsteps/reset") == 0) {
|
||||
resetSteps();
|
||||
if (client.connected()) client.publish("motor/feedback/totalsteps", "0");
|
||||
last_pub_steps = 0;
|
||||
}
|
||||
// --- TMC Control ---
|
||||
else if (strcmp(topic, "motor/control/tmc/current_percent") == 0) {
|
||||
uint8_t pct = atoi(msg);
|
||||
if (pct <= 100) tmcSetCurrentPercent(pct, pct / 2); // Hold = 50% от Run
|
||||
}
|
||||
else if (strcmp(topic, "motor/control/tmc/microsteps") == 0) {
|
||||
uint16_t ms = atoi(msg);
|
||||
tmcSetMicrosteps(ms);
|
||||
}
|
||||
else if (strcmp(topic, "motor/control/tmc/stallguard") == 0) {
|
||||
tmcSetStallGuard(atoi(msg));
|
||||
}
|
||||
else if (strcmp(topic, "motor/control/tmc/enable") == 0) {
|
||||
tmcSoftwareEnable(strcmp(msg, "on") == 0);
|
||||
}
|
||||
else if (strcmp(topic, "motor/control/tmc/stealthchop") == 0) {
|
||||
tmcSetStealthChop(strcmp(msg, "on") == 0);
|
||||
}
|
||||
else if (strcmp(topic, "motor/control/tmc/coolstep") == 0) {
|
||||
tmcSetCoolStep(strcmp(msg, "on") == 0);
|
||||
}
|
||||
else if (strncmp(topic, "servo/control", 12) == 0) {
|
||||
handleServoMQTTCommand(topic, msg);
|
||||
}
|
||||
// === VL53L0X ===
|
||||
else if (strcmp(topic, "sensor/control/mode") == 0) {
|
||||
int mode = atoi(msg);
|
||||
if (mode >= 0 && mode < MODE_COUNT) {
|
||||
vl53l0xSetMode((MeasurementMode)mode);
|
||||
}
|
||||
}
|
||||
else if (strcmp(topic, "sensor/control/mode_name") == 0) {
|
||||
// Маппинг имени режима на ID (если нужно)
|
||||
// Пока просто логируем
|
||||
Serial.printf("Mode name request: %s\n", msg);
|
||||
}
|
||||
else if (strncmp(topic, "sensor/control/calibrate/start/", 31) == 0) {
|
||||
int channel = atoi(topic + 31);
|
||||
int near_mm = atoi(msg);
|
||||
if (channel >= 0 && channel < VL53L0X_MAX_CHANNELS && near_mm > 0) {
|
||||
vl53l0xStartCalibration(channel, near_mm);
|
||||
}
|
||||
}
|
||||
else if (strncmp(topic, "sensor/control/calibrate/finish/", 32) == 0) {
|
||||
int channel = atoi(topic + 32);
|
||||
int far_mm = atoi(msg);
|
||||
if (channel >= 0 && channel < VL53L0X_MAX_CHANNELS && far_mm > 0) {
|
||||
vl53l0xFinishCalibration(channel, far_mm);
|
||||
}
|
||||
}
|
||||
else if (strncmp(topic, "sensor/control/clear_cal/", 25) == 0) {
|
||||
int channel = atoi(topic + 25);
|
||||
if (channel >= 0 && channel < VL53L0X_MAX_CHANNELS) {
|
||||
vl53l0xClearCalibration(channel);
|
||||
}
|
||||
}
|
||||
else if (strncmp(topic, "sensor/control/enable/", 22) == 0) {
|
||||
int channel = atoi(topic + 22);
|
||||
bool enable = (strcmp(msg, "on") == 0 || strcmp(msg, "1") == 0 || strcmp(msg, "true") == 0);
|
||||
if (channel >= 0 && channel < VL53L0X_MAX_CHANNELS) {
|
||||
vl53l0xEnableChannel(channel, enable);
|
||||
}
|
||||
}
|
||||
else if (strcmp(topic, "sensor/control/publish_all") == 0) {
|
||||
// Сбрасываем кэш для принудительной публикации
|
||||
for (int i = 0; i < VL53L0X_MAX_CHANNELS; i++) {
|
||||
last_pub_vl53_distance[i] = 65535;
|
||||
last_pub_vl53_raw[i] = 65535;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ПУБЛИКАЦИЯ MOTOR TELEMETRY
|
||||
// ============================================
|
||||
|
||||
static void publishMotorTelemetry() {
|
||||
// RPM
|
||||
int rpm = getCurrentRPM();
|
||||
if (rpm != last_pub_rpm) {
|
||||
client.publish("motor/feedback/rpm", intToString(rpm));
|
||||
last_pub_rpm = rpm;
|
||||
}
|
||||
|
||||
// Steps
|
||||
unsigned long steps = getMotorSteps();
|
||||
if (steps != last_pub_steps) {
|
||||
client.publish("motor/feedback/totalsteps", uintToString(steps));
|
||||
last_pub_steps = steps;
|
||||
}
|
||||
|
||||
// Is running
|
||||
int run = isMotorRunning() ? 1 : 0;
|
||||
if (run != last_pub_is_run) {
|
||||
client.publish("motor/feedback/is_run", run ? "true" : "false");
|
||||
last_pub_is_run = run;
|
||||
}
|
||||
|
||||
// TMC
|
||||
if (tmcIsInitialized()) {
|
||||
uint8_t pct = tmcGetRunCurrentPercent();
|
||||
if (pct != last_pub_current_pct) {
|
||||
client.publish("motor/feedback/tmc/current_percent", intToString(pct));
|
||||
last_pub_current_pct = pct;
|
||||
}
|
||||
|
||||
uint16_t ms = tmcGetMicrostepsSetting();
|
||||
if (ms != last_pub_microsteps) {
|
||||
client.publish("motor/feedback/tmc/microsteps", intToString(ms));
|
||||
last_pub_microsteps = ms;
|
||||
}
|
||||
|
||||
uint16_t sg = tmcGetStallGuardResult();
|
||||
if (sg != last_pub_sg) {
|
||||
client.publish("motor/feedback/tmc/sg_result", intToString(sg));
|
||||
last_pub_sg = sg;
|
||||
}
|
||||
|
||||
uint32_t interstep = tmcGetInterstepDuration();
|
||||
if (interstep != last_pub_interstep) {
|
||||
client.publish("motor/feedback/tmc/interstep_duration", uintToString(interstep));
|
||||
last_pub_interstep = interstep;
|
||||
}
|
||||
|
||||
TMC2209::Status status = tmcGetStatus();
|
||||
|
||||
int ot = (status.over_temperature_warning || status.over_temperature_shutdown) ? 1 : 0;
|
||||
if (ot != last_pub_over_temp) {
|
||||
client.publish("motor/feedback/tmc/status/over_temp", ot ? "true" : "false");
|
||||
last_pub_over_temp = ot;
|
||||
}
|
||||
|
||||
int sgnd = (status.short_to_ground_a || status.short_to_ground_b) ? 1 : 0;
|
||||
if (sgnd != last_pub_short_gnd) {
|
||||
client.publish("motor/feedback/tmc/status/short_to_ground", sgnd ? "true" : "false");
|
||||
last_pub_short_gnd = sgnd;
|
||||
}
|
||||
|
||||
int ol = (status.open_load_a || status.open_load_b) ? 1 : 0;
|
||||
if (ol != last_pub_open_load) {
|
||||
client.publish("motor/feedback/tmc/status/open_load", ol ? "true" : "false");
|
||||
last_pub_open_load = ol;
|
||||
}
|
||||
|
||||
int sa = status.stealth_chop_mode ? 1 : 0;
|
||||
if (sa != last_pub_stealth_active) {
|
||||
client.publish("motor/feedback/tmc/status/stealth_chop_active", sa ? "true" : "false");
|
||||
last_pub_stealth_active = sa;
|
||||
}
|
||||
|
||||
int ss = status.standstill ? 1 : 0;
|
||||
if (ss != last_pub_standstill) {
|
||||
client.publish("motor/feedback/tmc/status/standstill", ss ? "true" : "false");
|
||||
last_pub_standstill = ss;
|
||||
}
|
||||
|
||||
if (status.current_scaling != last_pub_current_scaling) {
|
||||
client.publish("motor/feedback/tmc/status/current_scaling", intToString(status.current_scaling));
|
||||
last_pub_current_scaling = status.current_scaling;
|
||||
}
|
||||
|
||||
int cds = checkDriverStatus() ? 1 : 0;
|
||||
if (cds != last_pub_driver_status) {
|
||||
client.publish("motor/feedback/driver/status", cds ? "on" : "off");
|
||||
last_pub_driver_status = cds;
|
||||
}
|
||||
|
||||
int tse = checkTmcSoftwareEnable() ? 1 : 0;
|
||||
if (tse != last_pub_tmc_software_enable) {
|
||||
client.publish("motor/feedback/tmc/status", tse ? "on" : "off");
|
||||
last_pub_tmc_software_enable = tse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ПУБЛИКАЦИЯ VL53L0X TELEMETRY
|
||||
// ============================================
|
||||
|
||||
static void publishVL53L0XTelemetry() {
|
||||
// Публикация режима
|
||||
MeasurementMode current_mode = vl53l0xGetMode();
|
||||
if (current_mode != last_pub_vl53_mode) {
|
||||
const ModeProfile* profile = vl53l0xGetModeProfile();
|
||||
|
||||
client.publish("sensor/feedback/mode", profile->name);
|
||||
client.publish("sensor/feedback/mode_id", intToString(current_mode));
|
||||
client.publish("sensor/feedback/max_range", intToString(profile->max_range_mm));
|
||||
|
||||
last_pub_vl53_mode = current_mode;
|
||||
}
|
||||
|
||||
// Публикация данных с каждого канала
|
||||
for (uint8_t ch = 0; ch < VL53L0X_MAX_CHANNELS; ch++) {
|
||||
if (!vl53l0xIsChannelPresent(ch)) continue;
|
||||
|
||||
char topic[64];
|
||||
|
||||
// Статус канала
|
||||
int status = vl53l0xIsChannelEnabled(ch) ? 1 : 0;
|
||||
if (status != last_pub_vl53_status[ch]) {
|
||||
snprintf(topic, sizeof(topic), "sensor/feedback/%d/status", ch);
|
||||
client.publish(topic, status ? "on" : "off");
|
||||
last_pub_vl53_status[ch] = status;
|
||||
}
|
||||
|
||||
// Статус калибровки
|
||||
int calibrated = vl53l0xIsCalibrated(ch) ? 1 : 0;
|
||||
if (calibrated != last_pub_vl53_calibrated[ch]) {
|
||||
snprintf(topic, sizeof(topic), "sensor/feedback/%d/calibrated", ch);
|
||||
client.publish(topic, calibrated ? "true" : "false");
|
||||
last_pub_vl53_calibrated[ch] = calibrated;
|
||||
}
|
||||
|
||||
// Только если канал активен, публикуем расстояния
|
||||
if (vl53l0xIsChannelActive(ch)) {
|
||||
// Калиброванное расстояние
|
||||
uint16_t distance = vl53l0xReadDistance(ch);
|
||||
if (distance != last_pub_vl53_distance[ch]) {
|
||||
snprintf(topic, sizeof(topic), "sensor/feedback/%d/distance", ch);
|
||||
if (distance != 65535) {
|
||||
client.publish(topic, intToString(distance));
|
||||
} else {
|
||||
client.publish(topic, "out_of_range");
|
||||
}
|
||||
last_pub_vl53_distance[ch] = distance;
|
||||
}
|
||||
|
||||
// Сырое значение
|
||||
uint16_t raw = vl53l0xReadRawDistance(ch);
|
||||
if (raw != last_pub_vl53_raw[ch]) {
|
||||
snprintf(topic, sizeof(topic), "sensor/feedback/%d/raw", ch);
|
||||
if (raw != 65535) {
|
||||
client.publish(topic, intToString(raw));
|
||||
}
|
||||
last_pub_vl53_raw[ch] = raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////
|
||||
|
||||
void checkAndPublishServoStatus(uint8_t channel) {
|
||||
if (channel >= MAX_SERVOS) return;
|
||||
|
||||
bool current_state = servo_enabled[channel];
|
||||
if (current_state != last_published_status[channel]) {
|
||||
last_published_status[channel] = current_state;
|
||||
|
||||
char topic[64];
|
||||
snprintf(topic, sizeof(topic), "servo/%d/feedback/status", channel);
|
||||
|
||||
String status = current_state ? "on" : "off";
|
||||
client.publish(topic, status.c_str());
|
||||
|
||||
Serial.printf("Published servo %d status: %s\n", channel, status.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void checkAndPublishServoAngle(uint8_t channel) {
|
||||
if (channel >= MAX_SERVOS) return;
|
||||
|
||||
uint8_t current_angle = current_angles[channel];
|
||||
if (current_angle != last_published_angles[channel]) {
|
||||
last_published_angles[channel] = current_angle;
|
||||
|
||||
char topic[64];
|
||||
snprintf(topic, sizeof(topic), "servo/%d/feedback/angle", channel);
|
||||
|
||||
char payload[8];
|
||||
snprintf(payload, sizeof(payload), "%d", current_angle);
|
||||
client.publish(topic, payload);
|
||||
|
||||
Serial.printf("Published servo %d angle: %d\n", channel, current_angle);
|
||||
}
|
||||
}
|
||||
|
||||
void handleServoMQTTCommand(const char* topic, const char* payload) {
|
||||
// Парсим топик: servo/control/{channel}/{command}
|
||||
int channel = -1;
|
||||
char command[32] = {0};
|
||||
|
||||
if (sscanf(topic, "servo/control/%2d/%8s", &channel, command) != 2) {
|
||||
Serial.printf("Invalid servo topic: %s\n", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel < 0 || channel >= MAX_SERVOS) {
|
||||
Serial.printf("Invalid servo channel: %d\n", channel);
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("Servo %d command: %s = %s\n", channel, command, payload);
|
||||
|
||||
if (strcmp(command, "angle") == 0) {
|
||||
int angle = atoi(payload);
|
||||
if (angle >= 0 && angle <= 180) {
|
||||
setServoAngle(channel, (uint8_t)angle);
|
||||
checkAndPublishServoAngle(channel);
|
||||
checkAndPublishServoStatus(channel);
|
||||
}
|
||||
}
|
||||
else if (strcmp(command, "enable") == 0) {
|
||||
bool enable = (strcmp(payload, "on") == 0 || strcmp(payload, "1") == 0 || strcmp(payload, "true") == 0);
|
||||
if (enable) {
|
||||
enableServo(channel);
|
||||
} else {
|
||||
disableServo(channel);
|
||||
}
|
||||
checkAndPublishServoStatus(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/////
|
||||
|
||||
// ============================================
|
||||
// ГЛАВНЫЙ ЦИКЛ ПУБЛИКАЦИИ
|
||||
// ============================================
|
||||
|
||||
static void publishTelemetry() {
|
||||
unsigned long now = millis();
|
||||
if (now - last_feedback_time >= 500) {
|
||||
publishMotorTelemetry();
|
||||
publishVL53L0XTelemetry();
|
||||
last_feedback_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
void mqttTask(void *parameter) {
|
||||
static unsigned long last_stack_check = 0;
|
||||
|
||||
setup_wifi();
|
||||
client.setServer(MQTT_SERVER, MQTT_PORT);
|
||||
client.setCallback(callback);
|
||||
|
||||
servoInit();
|
||||
|
||||
for (;;) {
|
||||
if (!client.connected()) reconnect();
|
||||
client.loop();
|
||||
publishTelemetry();
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
11
arduino_code/Test/src/mqtt_handler.h
Normal file
11
arduino_code/Test/src/mqtt_handler.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef MQTT_HANDLER_H
|
||||
#define MQTT_HANDLER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
|
||||
void mqttTask(void *parameter);
|
||||
void handleServoMQTTCommand(const char* topic, const char* payload);
|
||||
|
||||
#endif
|
||||
98
arduino_code/Test/src/servo_control.cpp
Normal file
98
arduino_code/Test/src/servo_control.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#include "servo_control.h"
|
||||
#include "mqtt_handler.h"
|
||||
#include "config.h"
|
||||
|
||||
static Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
|
||||
static bool servo_initialized = false;
|
||||
|
||||
|
||||
|
||||
void servoInit() {
|
||||
Serial.println("Initializing PCA9685 servo driver...");
|
||||
|
||||
// Инициализация I2C на пинах 21 (SDA) и 22 (SCL)
|
||||
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
|
||||
|
||||
pwm.begin();
|
||||
pwm.setOscillatorFrequency(27000000);
|
||||
pwm.setPWMFreq(50); // 50 Hz для сервоприводов
|
||||
|
||||
delay(10);
|
||||
|
||||
servo_initialized = true;
|
||||
Serial.println("PCA9685 initialized successfully");
|
||||
|
||||
// Инициализируем все каналы как выключенные
|
||||
for (int i = 0; i < MAX_SERVOS; i++) {
|
||||
current_angles[i] = 90; // Начальное положение - середина
|
||||
servo_enabled[i] = false;
|
||||
disableServo(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Преобразование угла (0-180) в длину импульса
|
||||
uint16_t angleToPulse(uint8_t angle) {
|
||||
if (angle > 180) angle = 180;
|
||||
return map(angle, 0, 180, SERVO_MIN_PULSE, SERVO_MAX_PULSE);
|
||||
}
|
||||
|
||||
// Преобразование длины импульса в угол
|
||||
uint8_t pulseToAngle(uint16_t pulse) {
|
||||
if (pulse < SERVO_MIN_PULSE) return 0;
|
||||
if (pulse > SERVO_MAX_PULSE) return 180;
|
||||
return map(pulse, SERVO_MIN_PULSE, SERVO_MAX_PULSE, 0, 180);
|
||||
}
|
||||
|
||||
void setServoAngle(uint8_t channel, uint8_t angle) {
|
||||
if (!servo_initialized || channel >= MAX_SERVOS) return;
|
||||
|
||||
if (angle > 180) angle = 180;
|
||||
|
||||
current_angles[channel] = angle;
|
||||
servo_enabled[channel] = true;
|
||||
|
||||
uint16_t pulse = angleToPulse(angle);
|
||||
setServoPulse(channel, pulse);
|
||||
|
||||
Serial.printf("Servo %d: angle=%d, pulse=%d\n", channel, angle, pulse);
|
||||
}
|
||||
|
||||
void setServoPulse(uint8_t channel, uint16_t pulse) {
|
||||
if (!servo_initialized || channel >= MAX_SERVOS) return;
|
||||
|
||||
// Преобразование микросекунд в тики PCA9685
|
||||
// PCA9685 имеет 4096 тиков на период при 50Hz = 20000 мкс
|
||||
// 1 мкс = 4096 / 20000 = 0.2048 тика
|
||||
double pulselength = 4096.0 / 20000.0; // тиков на микросекунду
|
||||
uint16_t ticks = pulse * pulselength;
|
||||
|
||||
pwm.setPWM(channel, 0, ticks);
|
||||
}
|
||||
|
||||
void enableServo(uint8_t channel) {
|
||||
if (!servo_initialized || channel >= MAX_SERVOS) return;
|
||||
|
||||
servo_enabled[channel] = true;
|
||||
setServoAngle(channel, current_angles[channel]);
|
||||
|
||||
Serial.printf("Servo %d: ENABLED\n", channel);
|
||||
}
|
||||
|
||||
void disableServo(uint8_t channel) {
|
||||
if (!servo_initialized || channel >= MAX_SERVOS) return;
|
||||
|
||||
servo_enabled[channel] = false;
|
||||
pwm.setPWM(channel, 0, 0); // Отключаем сигнал
|
||||
|
||||
Serial.printf("Servo %d: DISABLED\n", channel);
|
||||
}
|
||||
|
||||
uint8_t getServoAngle(uint8_t channel) {
|
||||
if (channel >= MAX_SERVOS) return 0;
|
||||
return current_angles[channel];
|
||||
}
|
||||
|
||||
bool isServoEnabled(uint8_t channel) {
|
||||
if (channel >= MAX_SERVOS) return false;
|
||||
return servo_enabled[channel];
|
||||
}
|
||||
32
arduino_code/Test/src/servo_control.h
Normal file
32
arduino_code/Test/src/servo_control.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#ifndef SERVO_H
|
||||
#define SERVO_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Adafruit_PWMServoDriver.h>
|
||||
|
||||
|
||||
// Хранение текущего состояния сервоприводов
|
||||
#define MAX_SERVOS 16
|
||||
static uint8_t current_angles[MAX_SERVOS] = {0};
|
||||
static bool servo_enabled[MAX_SERVOS] = {false};
|
||||
static uint8_t last_published_angles[MAX_SERVOS] = {255};
|
||||
static bool last_published_status[MAX_SERVOS] = {false};
|
||||
|
||||
// Минимальная и максимальная длина импульса для сервопривода (в микросекундах)
|
||||
static const uint16_t SERVO_MIN_PULSE = 600;
|
||||
static const uint16_t SERVO_MAX_PULSE = 2400;
|
||||
|
||||
// Инициализация сервопривода
|
||||
void servoInit();
|
||||
|
||||
// Управление сервоприводом
|
||||
void setServoAngle(uint8_t channel, uint8_t angle);
|
||||
void setServoPulse(uint8_t channel, uint16_t pulse);
|
||||
void enableServo(uint8_t channel);
|
||||
void disableServo(uint8_t channel);
|
||||
|
||||
// Получение состояния
|
||||
uint8_t getServoAngle(uint8_t channel);
|
||||
bool isServoEnabled(uint8_t channel);
|
||||
|
||||
#endif // SERVO_H
|
||||
477
arduino_code/Test/src/vl53l0x_sensor.cpp
Normal file
477
arduino_code/Test/src/vl53l0x_sensor.cpp
Normal file
@@ -0,0 +1,477 @@
|
||||
#include "vl53l0x_sensor.h"
|
||||
#include "config.h"
|
||||
|
||||
// ============================================
|
||||
// КОНСТАНТЫ
|
||||
// ============================================
|
||||
|
||||
#define TCA9548A_ADDRESS 0x70
|
||||
#define TCA9548A_CHANNELS 8
|
||||
|
||||
#define MIN_DISTANCE_MM 30
|
||||
#define OUT_OF_RANGE_VALUE 65535
|
||||
|
||||
#define FILTER_SIZE 5
|
||||
|
||||
// ============================================
|
||||
// ПРОФИЛИ РЕЖИМОВ
|
||||
// ============================================
|
||||
|
||||
static const ModeProfile MODES[MODE_COUNT] = {
|
||||
{
|
||||
"HIGH_ACCURACY",
|
||||
200000, 14, 10, 0.5f, 500, 2
|
||||
},
|
||||
{
|
||||
"PRECISION",
|
||||
66000, 14, 10, 0.3f, 1000, 5
|
||||
},
|
||||
{
|
||||
"DEFAULT",
|
||||
33000, 14, 10, 0.25f, 1200, 15
|
||||
},
|
||||
{
|
||||
"LONG_RANGE",
|
||||
33000, 18, 14, 0.1f, 2000, 40
|
||||
},
|
||||
{
|
||||
"ULTRA_LONG",
|
||||
100000, 18, 14, 0.05f, 2500, 80
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
|
||||
// ============================================
|
||||
|
||||
static VL53L0X sensor;
|
||||
static Preferences preferences;
|
||||
|
||||
static MeasurementMode current_mode = MODE_DEFAULT;
|
||||
static bool sensor_present[TCA9548A_CHANNELS] = {false};
|
||||
static bool channel_enabled[TCA9548A_CHANNELS] = {false};
|
||||
static CalibrationData calibration[TCA9548A_CHANNELS];
|
||||
|
||||
static uint16_t filter_buffer[TCA9548A_CHANNELS][FILTER_SIZE];
|
||||
static uint8_t filter_index[TCA9548A_CHANNELS] = {0};
|
||||
|
||||
static bool calibration_in_progress = false;
|
||||
static uint8_t calibration_channel = 0;
|
||||
static uint16_t calibration_near_raw = 0;
|
||||
static uint16_t calibration_near_known = 0;
|
||||
|
||||
// ============================================
|
||||
// TCA9548A МУЛЬТИПЛЕКСОР
|
||||
// ============================================
|
||||
|
||||
static void TCA9548A_Select(uint8_t channel) {
|
||||
Wire.beginTransmission(TCA9548A_ADDRESS);
|
||||
Wire.write((channel < TCA9548A_CHANNELS) ? (1 << channel) : 0x00);
|
||||
Wire.endTransmission();
|
||||
delay(3);
|
||||
}
|
||||
|
||||
static void TCA9548A_DisableAll() {
|
||||
Wire.beginTransmission(TCA9548A_ADDRESS);
|
||||
Wire.write(0x00);
|
||||
Wire.endTransmission();
|
||||
}
|
||||
|
||||
static bool checkTCA9548A() {
|
||||
Wire.beginTransmission(TCA9548A_ADDRESS);
|
||||
return (Wire.endTransmission() == 0);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// NVS (СОХРАНЕНИЕ В FLASH)
|
||||
// ============================================
|
||||
|
||||
static void loadCalibration() {
|
||||
preferences.begin("vl53_cal", true);
|
||||
|
||||
for (uint8_t ch = 0; ch < TCA9548A_CHANNELS; ch++) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "ch%d", ch);
|
||||
|
||||
size_t len = preferences.getBytesLength(key);
|
||||
if (len == sizeof(CalibrationData)) {
|
||||
preferences.getBytes(key, &calibration[ch], sizeof(CalibrationData));
|
||||
} else {
|
||||
calibration[ch].valid = false;
|
||||
calibration[ch].scale = 1.0f;
|
||||
calibration[ch].offset = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
int saved_mode = preferences.getInt("mode", MODE_DEFAULT);
|
||||
if (saved_mode >= 0 && saved_mode < MODE_COUNT) {
|
||||
current_mode = (MeasurementMode)saved_mode;
|
||||
}
|
||||
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
static void saveCalibration(uint8_t channel) {
|
||||
preferences.begin("vl53_cal", false);
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "ch%d", channel);
|
||||
preferences.putBytes(key, &calibration[channel], sizeof(CalibrationData));
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
static void saveMode() {
|
||||
preferences.begin("vl53_cal", false);
|
||||
preferences.putInt("mode", (int)current_mode);
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ПРИМЕНЕНИЕ РЕЖИМА
|
||||
// ============================================
|
||||
|
||||
static bool applyModeProfile() {
|
||||
const ModeProfile& profile = MODES[current_mode];
|
||||
|
||||
sensor.setTimeout(500);
|
||||
|
||||
if (!sensor.init()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sensor.setAddress(0x29);
|
||||
sensor.setSignalRateLimit(profile.signal_rate_limit);
|
||||
|
||||
sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, profile.vcsel_prerange);
|
||||
sensor.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, profile.vcsel_final);
|
||||
sensor.setMeasurementTimingBudget(profile.timing_budget_us);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// КАЛИБРОВКА И ФИЛЬТРАЦИЯ
|
||||
// ============================================
|
||||
|
||||
static uint16_t applyCalibration(uint8_t channel, uint16_t raw_mm) {
|
||||
if (!calibration[channel].valid || raw_mm == OUT_OF_RANGE_VALUE) {
|
||||
return raw_mm;
|
||||
}
|
||||
|
||||
float calibrated = (float)raw_mm * calibration[channel].scale + calibration[channel].offset;
|
||||
|
||||
const ModeProfile& profile = MODES[current_mode];
|
||||
if (calibrated < MIN_DISTANCE_MM) calibrated = MIN_DISTANCE_MM;
|
||||
if (calibrated > profile.max_range_mm) return OUT_OF_RANGE_VALUE;
|
||||
|
||||
return (uint16_t)calibrated;
|
||||
}
|
||||
|
||||
static uint16_t applyFilter(uint8_t channel, uint16_t new_value) {
|
||||
if (new_value == OUT_OF_RANGE_VALUE) return OUT_OF_RANGE_VALUE;
|
||||
|
||||
filter_buffer[channel][filter_index[channel]] = new_value;
|
||||
filter_index[channel] = (filter_index[channel] + 1) % FILTER_SIZE;
|
||||
|
||||
uint32_t sum = 0;
|
||||
uint8_t count = 0;
|
||||
for (uint8_t i = 0; i < FILTER_SIZE; i++) {
|
||||
if (filter_buffer[channel][i] != 0) {
|
||||
sum += filter_buffer[channel][i];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return (count > 0) ? (uint16_t)(sum / count) : new_value;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ПУБЛИЧНЫЕ ФУНКЦИИ
|
||||
// ============================================
|
||||
|
||||
void vl53l0xInit() {
|
||||
Serial.println("Initializing VL53L0X sensors...");
|
||||
|
||||
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
|
||||
Wire.setClock(400000);
|
||||
delay(100);
|
||||
|
||||
if (!checkTCA9548A()) {
|
||||
Serial.println("ERROR: TCA9548A not found!");
|
||||
return;
|
||||
}
|
||||
Serial.println("✓ TCA9548A found");
|
||||
TCA9548A_DisableAll();
|
||||
|
||||
loadCalibration();
|
||||
Serial.printf("✓ Loaded mode: %s\n", MODES[current_mode].name);
|
||||
|
||||
Serial.println("\nScanning VL53L0X channels:");
|
||||
for (uint8_t channel = 0; channel < TCA9548A_CHANNELS; channel++) {
|
||||
TCA9548A_Select(channel);
|
||||
delay(20);
|
||||
|
||||
if (applyModeProfile()) {
|
||||
sensor_present[channel] = true;
|
||||
channel_enabled[channel] = true;
|
||||
Serial.printf(" ✓ Channel %d: VL53L0X found", channel);
|
||||
if (calibration[channel].valid) Serial.print(" [CALIBRATED]");
|
||||
Serial.println();
|
||||
|
||||
memset(filter_buffer[channel], 0, sizeof(filter_buffer[channel]));
|
||||
filter_index[channel] = 0;
|
||||
} else {
|
||||
sensor_present[channel] = false;
|
||||
channel_enabled[channel] = false;
|
||||
Serial.printf(" ✗ Channel %d: No device\n", channel);
|
||||
}
|
||||
|
||||
TCA9548A_DisableAll();
|
||||
delay(5);
|
||||
}
|
||||
|
||||
Serial.printf("\n✓ VL53L0X initialized: %d sensors found\n\n",
|
||||
vl53l0xGetChannelCount());
|
||||
}
|
||||
|
||||
void vl53l0xLoop() {
|
||||
// Внутренняя логика датчиков (если нужна)
|
||||
// Сейчас вся публикация в mqtt_handle.cpp
|
||||
}
|
||||
|
||||
bool vl53l0xSetMode(MeasurementMode mode) {
|
||||
if (mode < 0 || mode >= MODE_COUNT) {
|
||||
Serial.printf("ERROR: Invalid mode %d\n", mode);
|
||||
return false;
|
||||
}
|
||||
|
||||
current_mode = mode;
|
||||
saveMode();
|
||||
|
||||
const ModeProfile& profile = MODES[current_mode];
|
||||
Serial.printf("✓ Mode changed to: %s (max %d mm)\n",
|
||||
profile.name, profile.max_range_mm);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
MeasurementMode vl53l0xGetMode() {
|
||||
return current_mode;
|
||||
}
|
||||
|
||||
const ModeProfile* vl53l0xGetModeProfile() {
|
||||
return &MODES[current_mode];
|
||||
}
|
||||
|
||||
uint16_t vl53l0xReadDistance(uint8_t channel) {
|
||||
if (channel >= TCA9548A_CHANNELS || !sensor_present[channel] || !channel_enabled[channel]) {
|
||||
return OUT_OF_RANGE_VALUE;
|
||||
}
|
||||
|
||||
TCA9548A_Select(channel);
|
||||
|
||||
if (!applyModeProfile()) {
|
||||
TCA9548A_DisableAll();
|
||||
return OUT_OF_RANGE_VALUE;
|
||||
}
|
||||
|
||||
uint16_t distance = sensor.readRangeSingleMillimeters();
|
||||
bool timeout = sensor.timeoutOccurred();
|
||||
|
||||
TCA9548A_DisableAll();
|
||||
|
||||
if (distance == 65535 || timeout) return OUT_OF_RANGE_VALUE;
|
||||
|
||||
const ModeProfile& profile = MODES[current_mode];
|
||||
if (distance < MIN_DISTANCE_MM || distance > profile.max_range_mm) {
|
||||
return OUT_OF_RANGE_VALUE;
|
||||
}
|
||||
|
||||
uint16_t filtered = applyFilter(channel, distance);
|
||||
uint16_t calibrated = applyCalibration(channel, filtered);
|
||||
|
||||
return calibrated;
|
||||
}
|
||||
|
||||
uint16_t vl53l0xReadRawDistance(uint8_t channel) {
|
||||
if (channel >= TCA9548A_CHANNELS || !sensor_present[channel] || !channel_enabled[channel]) {
|
||||
return OUT_OF_RANGE_VALUE;
|
||||
}
|
||||
|
||||
TCA9548A_Select(channel);
|
||||
|
||||
if (!applyModeProfile()) {
|
||||
TCA9548A_DisableAll();
|
||||
return OUT_OF_RANGE_VALUE;
|
||||
}
|
||||
|
||||
uint16_t distance = sensor.readRangeSingleMillimeters();
|
||||
bool timeout = sensor.timeoutOccurred();
|
||||
|
||||
TCA9548A_DisableAll();
|
||||
|
||||
if (distance == 65535 || timeout) return OUT_OF_RANGE_VALUE;
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
bool vl53l0xIsChannelActive(uint8_t channel) {
|
||||
return (channel < TCA9548A_CHANNELS && sensor_present[channel] && channel_enabled[channel]);
|
||||
}
|
||||
|
||||
int vl53l0xGetChannelCount() {
|
||||
int count = 0;
|
||||
for (uint8_t i = 0; i < TCA9548A_CHANNELS; i++) {
|
||||
if (sensor_present[i]) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool vl53l0xStartCalibration(uint8_t channel, uint16_t near_known_mm) {
|
||||
if (channel >= TCA9548A_CHANNELS || !sensor_present[channel]) {
|
||||
Serial.printf("ERROR: Channel %d not available\n", channel);
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("Starting calibration for channel %d (near point: %d mm)\n",
|
||||
channel, near_known_mm);
|
||||
|
||||
uint32_t sum = 0;
|
||||
uint8_t valid = 0;
|
||||
|
||||
for (uint8_t i = 0; i < 20; i++) {
|
||||
uint16_t d = vl53l0xReadRawDistance(channel);
|
||||
if (d != OUT_OF_RANGE_VALUE) {
|
||||
sum += d;
|
||||
valid++;
|
||||
}
|
||||
delay(50);
|
||||
}
|
||||
|
||||
if (valid == 0) {
|
||||
Serial.println("ERROR: Failed to read near point");
|
||||
return false;
|
||||
}
|
||||
|
||||
calibration_near_raw = (uint16_t)(sum / valid);
|
||||
calibration_near_known = near_known_mm;
|
||||
calibration_channel = channel;
|
||||
calibration_in_progress = true;
|
||||
|
||||
Serial.printf("✓ Near point captured: raw=%d mm, actual=%d mm\n",
|
||||
calibration_near_raw, calibration_near_known);
|
||||
Serial.println("Now place object at FAR point and call vl53l0xFinishCalibration()");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool vl53l0xFinishCalibration(uint8_t channel, uint16_t far_known_mm) {
|
||||
if (!calibration_in_progress || channel != calibration_channel) {
|
||||
Serial.println("ERROR: Calibration not in progress or wrong channel");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("Finishing calibration for channel %d (far point: %d mm)\n",
|
||||
channel, far_known_mm);
|
||||
|
||||
uint32_t sum = 0;
|
||||
uint8_t valid = 0;
|
||||
|
||||
for (uint8_t i = 0; i < 20; i++) {
|
||||
uint16_t d = vl53l0xReadRawDistance(channel);
|
||||
if (d != OUT_OF_RANGE_VALUE) {
|
||||
sum += d;
|
||||
valid++;
|
||||
}
|
||||
delay(50);
|
||||
}
|
||||
|
||||
if (valid == 0) {
|
||||
Serial.println("ERROR: Failed to read far point");
|
||||
calibration_in_progress = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t far_raw = (uint16_t)(sum / valid);
|
||||
|
||||
Serial.printf("✓ Far point captured: raw=%d mm, actual=%d mm\n",
|
||||
far_raw, far_known_mm);
|
||||
|
||||
if (far_raw == calibration_near_raw) {
|
||||
Serial.println("ERROR: Raw values are identical");
|
||||
calibration_in_progress = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
float scale = (float)(far_known_mm - calibration_near_known) /
|
||||
(float)(far_raw - calibration_near_raw);
|
||||
float offset = (float)calibration_near_known -
|
||||
(float)calibration_near_raw * scale;
|
||||
|
||||
calibration[channel].valid = true;
|
||||
calibration[channel].near_raw = calibration_near_raw;
|
||||
calibration[channel].near_known = calibration_near_known;
|
||||
calibration[channel].far_raw = far_raw;
|
||||
calibration[channel].far_known = far_known_mm;
|
||||
calibration[channel].scale = scale;
|
||||
calibration[channel].offset = offset;
|
||||
|
||||
saveCalibration(channel);
|
||||
|
||||
Serial.printf("✓ Calibration complete: scale=%.5f, offset=%.2f\n", scale, offset);
|
||||
|
||||
calibration_in_progress = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void vl53l0xClearCalibration(uint8_t channel) {
|
||||
if (channel >= TCA9548A_CHANNELS) return;
|
||||
|
||||
calibration[channel].valid = false;
|
||||
calibration[channel].scale = 1.0f;
|
||||
calibration[channel].offset = 0.0f;
|
||||
saveCalibration(channel);
|
||||
|
||||
Serial.printf("✓ Channel %d calibration cleared\n", channel);
|
||||
}
|
||||
|
||||
bool vl53l0xIsCalibrated(uint8_t channel) {
|
||||
return (channel < TCA9548A_CHANNELS && calibration[channel].valid);
|
||||
}
|
||||
|
||||
CalibrationData vl53l0xGetCalibration(uint8_t channel) {
|
||||
if (channel >= TCA9548A_CHANNELS) {
|
||||
CalibrationData empty = {false, 0, 0, 0, 0, 1.0f, 0.0f};
|
||||
return empty;
|
||||
}
|
||||
return calibration[channel];
|
||||
}
|
||||
|
||||
void vl53l0xEnableChannel(uint8_t channel, bool enable) {
|
||||
if (channel >= TCA9548A_CHANNELS) return;
|
||||
|
||||
if (!sensor_present[channel]) {
|
||||
Serial.printf("ERROR: Channel %d not present\n", channel);
|
||||
return;
|
||||
}
|
||||
|
||||
channel_enabled[channel] = enable;
|
||||
Serial.printf("✓ Channel %d %s\n", channel, enable ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
bool vl53l0xIsChannelEnabled(uint8_t channel) {
|
||||
return (channel < TCA9548A_CHANNELS && channel_enabled[channel]);
|
||||
}
|
||||
|
||||
bool vl53l0xIsChannelPresent(uint8_t channel) {
|
||||
return (channel < TCA9548A_CHANNELS && sensor_present[channel]);
|
||||
}
|
||||
|
||||
void sensorTask(void *parameter) {
|
||||
vl53l0xInit();
|
||||
|
||||
for (;;) {
|
||||
vl53l0xLoop();
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
}
|
||||
68
arduino_code/Test/src/vl53l0x_sensor.h
Normal file
68
arduino_code/Test/src/vl53l0x_sensor.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#ifndef VL53L0X_SENSOR_H
|
||||
#define VL53L0X_SENSOR_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <VL53L0X.h>
|
||||
#include <Wire.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
// Режимы измерения
|
||||
enum MeasurementMode {
|
||||
MODE_HIGH_ACCURACY = 0,
|
||||
MODE_PRECISION = 1,
|
||||
MODE_DEFAULT = 2,
|
||||
MODE_LONG_RANGE = 3,
|
||||
MODE_ULTRA_LONG = 4,
|
||||
MODE_COUNT = 5
|
||||
};
|
||||
|
||||
struct ModeProfile {
|
||||
const char* name;
|
||||
uint32_t timing_budget_us;
|
||||
uint8_t vcsel_prerange;
|
||||
uint8_t vcsel_final;
|
||||
float signal_rate_limit;
|
||||
uint16_t max_range_mm;
|
||||
uint8_t accuracy_mm;
|
||||
};
|
||||
|
||||
struct CalibrationData {
|
||||
bool valid;
|
||||
uint16_t near_raw;
|
||||
uint16_t near_known;
|
||||
uint16_t far_raw;
|
||||
uint16_t far_known;
|
||||
float scale;
|
||||
float offset;
|
||||
};
|
||||
|
||||
// Инициализация и цикл
|
||||
void vl53l0xInit();
|
||||
void vl53l0xLoop();
|
||||
|
||||
// Управление режимами
|
||||
bool vl53l0xSetMode(MeasurementMode mode);
|
||||
MeasurementMode vl53l0xGetMode();
|
||||
const ModeProfile* vl53l0xGetModeProfile();
|
||||
|
||||
// Чтение данных
|
||||
uint16_t vl53l0xReadDistance(uint8_t channel);
|
||||
uint16_t vl53l0xReadRawDistance(uint8_t channel);
|
||||
bool vl53l0xIsChannelActive(uint8_t channel);
|
||||
int vl53l0xGetChannelCount();
|
||||
|
||||
// Калибровка
|
||||
bool vl53l0xStartCalibration(uint8_t channel, uint16_t near_known_mm);
|
||||
bool vl53l0xFinishCalibration(uint8_t channel, uint16_t far_known_mm);
|
||||
void vl53l0xClearCalibration(uint8_t channel);
|
||||
bool vl53l0xIsCalibrated(uint8_t channel);
|
||||
CalibrationData vl53l0xGetCalibration(uint8_t channel);
|
||||
|
||||
// Управление каналами
|
||||
void vl53l0xEnableChannel(uint8_t channel, bool enable);
|
||||
bool vl53l0xIsChannelEnabled(uint8_t channel);
|
||||
bool vl53l0xIsChannelPresent(uint8_t channel);
|
||||
|
||||
void sensorTask(void *parameter);
|
||||
|
||||
#endif // VL53L0X_SENSOR_H
|
||||
11
arduino_code/Test/test/README
Normal file
11
arduino_code/Test/test/README
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Test Runner and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||
1
backend_control/.gitignore
vendored
Normal file
1
backend_control/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/testing
|
||||
61
backend_control/README.md
Normal file
61
backend_control/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Интерфейс управления
|
||||
|
||||

|
||||
|
||||
## Область Управления
|
||||
|
||||
`Целевой RPM` - Задается уставка скорости в диапозоне от -1000 до 1000 оборотов в минуту
|
||||
|
||||
`Ток` - Уставка тока в диапозоне 0 - 100%
|
||||
|
||||
`StallGuard` -
|
||||
|
||||
`Микрошаг` - Уставка микрошага
|
||||
|
||||
`Сброс счетчика шагов` - Сброс счетчика шагов
|
||||
|
||||
## Область Режимы
|
||||
|
||||
`Аппаратное вкл. (Driver EN)` - Аппаратное Включение/Выключение драйвера tmc2209
|
||||
|
||||
`Программное вкл. (TMC Chip)` - Программное Включение/Выключение драйвера tmc2209
|
||||
|
||||
`StealthChop (Тихий)` - Включение/Выключение тихого режима работы шагового двигателя.
|
||||
|
||||
`CoolStep (Энергосбер.)` - Включение/Выключение энергосберегающего режима работы шагового двигателя.
|
||||
|
||||
## Область Телеметрия и Статусы
|
||||
|
||||
`Текущий RPM (Факт)`
|
||||
|
||||
`Всего шагов`
|
||||
|
||||
`Вращение`
|
||||
|
||||
`SG Result` - Для отслеживания нагрузки на вал или момента срыва шагов. Резкое падение значения sg_result при движении обычно означает столкновение или заклинивание механизма.
|
||||
|
||||
`Interstep (ns)`
|
||||
|
||||
`Current Scaling`
|
||||
|
||||
## Область Ошибки и Флаги
|
||||
|
||||
`Перегрев`
|
||||
|
||||
`КЗ на землю`
|
||||
|
||||
`Обрыв нагрузки`
|
||||
|
||||
`StealthChop активен`
|
||||
|
||||
`Остановка (Standstill)`
|
||||
|
||||

|
||||
|
||||
## Область Управление Серво
|
||||
|
||||
Тут можно задать угол сервопривода и отключить сервопривод
|
||||
|
||||
## Облать Статус и телеметрия
|
||||
|
||||
Вывод информации о сервоприводе включен или выключен. Так же выведен текущий угол.
|
||||
6
backend_control/create_venv.sh
Normal file
6
backend_control/create_venv.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
python -m venv testing
|
||||
|
||||
source testing/bin/activate
|
||||
pip install -r requirements.txt
|
||||
460
backend_control/gui.py
Normal file
460
backend_control/gui.py
Normal file
@@ -0,0 +1,460 @@
|
||||
import customtkinter as ctk
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
# ================= НАСТРОЙКИ =================
|
||||
MQTT_BROKER = "192.168.0.200"
|
||||
MQTT_PORT = 1883
|
||||
MQTT_USER = "test"
|
||||
MQTT_PASSWORD = "1234"
|
||||
|
||||
MAX_SERVOS = 4
|
||||
MAX_SENSORS = 8 # Количество каналов VL53L0X
|
||||
|
||||
# Цвета
|
||||
COLOR_OK = "#28a745"
|
||||
COLOR_ERR = "#dc3545"
|
||||
COLOR_OFF = "#555555"
|
||||
COLOR_ACTIVE = "#00d2ff"
|
||||
COLOR_PENDING = "#ffaa00"
|
||||
COLOR_WARN = "#ff9900" # Для out_of_range
|
||||
|
||||
class MotorSCADA:
|
||||
def __init__(self):
|
||||
ctk.set_appearance_mode("Dark")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
self.root = ctk.CTk()
|
||||
self.root.title("🚀 Motor, Servo & Sensor SCADA")
|
||||
self.root.geometry("1200x900")
|
||||
self.root.minsize(1100, 800)
|
||||
|
||||
# Флаги ожидания
|
||||
self.driver_pending = False
|
||||
self.tmc_pending = False
|
||||
self.servo_pending = {i: False for i in range(MAX_SERVOS)}
|
||||
self.sensor_pending = {i: False for i in range(MAX_SENSORS)}
|
||||
|
||||
self.setup_gui()
|
||||
self.setup_mqtt()
|
||||
|
||||
def setup_gui(self):
|
||||
# --- Шапка ---
|
||||
header = ctk.CTkFrame(self.root, height=60)
|
||||
header.pack(fill="x", padx=20, pady=(20, 10))
|
||||
header.pack_propagate(False)
|
||||
ctk.CTkLabel(header, text="Motor, Servo & Sensor SCADA", font=ctk.CTkFont(size=24, weight="bold")).pack(side="left", padx=20)
|
||||
self.lbl_status = ctk.CTkLabel(header, text="● Отключено", text_color=COLOR_ERR, font=ctk.CTkFont(size=16, weight="bold"))
|
||||
self.lbl_status.pack(side="right", padx=20)
|
||||
|
||||
# --- Вкладки ---
|
||||
self.tabview = ctk.CTkTabview(self.root)
|
||||
self.tabview.pack(fill="both", expand=True, padx=20, pady=10)
|
||||
|
||||
self.tab_motor = self.tabview.add("Шаговый двигатель (TMC2209)")
|
||||
self.tab_servo = self.tabview.add(f"Сервоприводы (0-{MAX_SERVOS-1})")
|
||||
self.tab_sensor = self.tabview.add(f"Датчики VL53L0X (0-{MAX_SENSORS-1})")
|
||||
|
||||
self.create_motor_tab(self.tab_motor)
|
||||
self.create_servo_tab(self.tab_servo)
|
||||
self.create_sensor_tab(self.tab_sensor)
|
||||
|
||||
# ================= ВКЛАДКА ШАГОВОГО ДВИГАТЕЛЯ =================
|
||||
# (Код для мотора остался без изменений, чтобы не раздувать ответ,
|
||||
# но в реальном файле он должен быть здесь полностью)
|
||||
def create_motor_tab(self, parent):
|
||||
grid = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
grid.pack(fill="both", expand=True)
|
||||
grid.grid_columnconfigure((0, 1, 2), weight=1, uniform="col")
|
||||
grid.grid_rowconfigure(0, weight=1)
|
||||
self.create_control_frame(grid)
|
||||
self.create_modes_frame(grid)
|
||||
self.create_telemetry_frame(grid)
|
||||
|
||||
def create_control_frame(self, parent):
|
||||
frame = ctk.CTkFrame(parent)
|
||||
frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
|
||||
ctk.CTkLabel(frame, text="⚙️ Управление", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 20))
|
||||
ctk.CTkLabel(frame, text="Целевой RPM:").pack(anchor="w", padx=20)
|
||||
rpm_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
rpm_frame.pack(fill="x", padx=20, pady=5)
|
||||
self.sld_rpm = ctk.CTkSlider(rpm_frame, from_=-1000, to=1000, command=self.on_rpm_slider_change)
|
||||
self.sld_rpm.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
||||
self.ent_rpm = ctk.CTkEntry(rpm_frame, width=80, justify="right")
|
||||
self.ent_rpm.insert(0, "0"); self.ent_rpm.pack(side="left", padx=(0, 10))
|
||||
self.ent_rpm.bind("<Return>", self.on_rpm_entry_apply); self.ent_rpm.bind("<FocusOut>", self.on_rpm_entry_apply)
|
||||
self.lbl_rpm_val = ctk.CTkLabel(rpm_frame, text="0", width=50); self.lbl_rpm_val.pack(side="right")
|
||||
|
||||
ctk.CTkLabel(frame, text="Ток (%):").pack(anchor="w", padx=20, pady=(15,0))
|
||||
cur_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
cur_frame.pack(fill="x", padx=20, pady=5)
|
||||
self.sld_current = ctk.CTkSlider(cur_frame, from_=0, to=100, command=self.on_current_change)
|
||||
self.sld_current.pack(side="left", fill="x", expand=True)
|
||||
self.lbl_cur_val = ctk.CTkLabel(cur_frame, text="50", width=50); self.lbl_cur_val.pack(side="right", padx=(10, 0))
|
||||
|
||||
ctk.CTkLabel(frame, text="StallGuard (0-255):").pack(anchor="w", padx=20, pady=(15,0))
|
||||
self.ent_sg = ctk.CTkEntry(frame, width=100); self.ent_sg.insert(0, "0")
|
||||
self.ent_sg.pack(anchor="w", padx=20, pady=5)
|
||||
ctk.CTkButton(frame, text="Применить SG", width=150, command=self.on_sg_apply).pack(pady=5)
|
||||
|
||||
ctk.CTkLabel(frame, text="Микрошаги:").pack(anchor="w", padx=20, pady=(15,0))
|
||||
self.opt_msteps = ctk.CTkOptionMenu(frame, values=["1", "2", "4", "8", "16", "32", "64", "128", "256"], command=self.on_msteps_change)
|
||||
self.opt_msteps.set("16"); self.opt_msteps.pack(anchor="w", padx=20, pady=5)
|
||||
ctk.CTkButton(frame, text="Сбросить счетчик шагов", fg_color="#dc3545", hover_color="#b02a37", command=self.on_reset_steps).pack(pady=20)
|
||||
|
||||
def create_modes_frame(self, parent):
|
||||
frame = ctk.CTkFrame(parent)
|
||||
frame.grid(row=0, column=1, sticky="nsew", padx=10)
|
||||
ctk.CTkLabel(frame, text="🔌 Режимы и Включение", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 20))
|
||||
self.sw_driver, self.led_driver_fb = self.create_switch_with_feedback(frame, "Аппаратное вкл. (Driver EN)", self.on_driver_change)
|
||||
self.sw_tmc_enable, self.led_tmc_fb = self.create_switch_with_feedback(frame, "Программное вкл. (TMC Chip)", self.on_tmc_enable_change)
|
||||
ctk.CTkFrame(frame, height=2, fg_color="#4a4a6a").pack(fill="x", padx=20, pady=15)
|
||||
self.sw_stealth = self.create_switch(frame, "StealthChop (Тихий)", self.on_stealth_change)
|
||||
self.sw_cool = self.create_switch(frame, "CoolStep (Энергосбер.)", self.on_cool_change)
|
||||
|
||||
def create_telemetry_frame(self, parent):
|
||||
frame = ctk.CTkFrame(parent)
|
||||
frame.grid(row=0, column=2, sticky="nsew", padx=(10, 0))
|
||||
ctk.CTkLabel(frame, text="📊 Телеметрия и Статусы", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 10))
|
||||
tel_frame = ctk.CTkFrame(frame); tel_frame.pack(fill="x", padx=10, pady=5)
|
||||
self.lbl_fb_rpm = self.create_telemetry_row(tel_frame, "Текущий RPM:")
|
||||
self.lbl_fb_steps = self.create_telemetry_row(tel_frame, "Всего шагов:")
|
||||
self.lbl_fb_run = self.create_telemetry_row(tel_frame, "Вращение:")
|
||||
self.lbl_fb_sg = self.create_telemetry_row(tel_frame, "SG Result:")
|
||||
self.lbl_fb_interstep = self.create_telemetry_row(tel_frame, "Interstep:")
|
||||
self.lbl_fb_cscale = self.create_telemetry_row(tel_frame, "Current Scaling:")
|
||||
ctk.CTkLabel(frame, text="🚨 Ошибки и Флаги", font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(15, 5))
|
||||
stat_frame = ctk.CTkFrame(frame); stat_frame.pack(fill="x", padx=10, pady=5)
|
||||
self.led_over_temp = self.create_led_row(stat_frame, "Перегрев:")
|
||||
self.led_short_gnd = self.create_led_row(stat_frame, "КЗ на землю:")
|
||||
self.led_open_load = self.create_led_row(stat_frame, "Обрыв нагрузки:")
|
||||
self.led_stealth_act = self.create_led_row(stat_frame, "StealthChop активен:")
|
||||
self.led_standstill = self.create_led_row(stat_frame, "Остановка:")
|
||||
|
||||
# ================= ВКЛАДКА СЕРВОПРИВОДОВ =================
|
||||
def create_servo_tab(self, parent):
|
||||
grid = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
grid.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
grid.grid_columnconfigure((0, 1), weight=1, uniform="col")
|
||||
grid.grid_rowconfigure((0, 1), weight=1, uniform="row")
|
||||
self.servo_ui = {}
|
||||
for i in range(MAX_SERVOS):
|
||||
row, col = divmod(i, 2)
|
||||
frame = ctk.CTkFrame(grid)
|
||||
frame.grid(row=row, column=col, sticky="nsew", padx=10, pady=10)
|
||||
self.servo_ui[i] = self.create_servo_card(frame, i)
|
||||
|
||||
def create_servo_card(self, parent, channel):
|
||||
ui = {}
|
||||
ctk.CTkLabel(parent, text=f"🦾 Сервопривод #{channel}", font=ctk.CTkFont(size=16, weight="bold")).pack(pady=(10, 10))
|
||||
ctk.CTkLabel(parent, text="Угол (0-180°):").pack(anchor="w", padx=20)
|
||||
ang_frame = ctk.CTkFrame(parent, fg_color="transparent"); ang_frame.pack(fill="x", padx=20, pady=5)
|
||||
sld = ctk.CTkSlider(ang_frame, from_=0, to=180, command=lambda val, ch=channel: self.on_servo_ang_slider(ch, val))
|
||||
sld.pack(side="left", fill="x", expand=True, padx=(0, 10)); sld.set(90)
|
||||
ent = ctk.CTkEntry(ang_frame, width=60, justify="right"); ent.insert(0, "90"); ent.pack(side="left", padx=(0, 10))
|
||||
ent.bind("<Return>", lambda event, ch=channel: self.on_servo_ang_entry(ch, event))
|
||||
ent.bind("<FocusOut>", lambda event, ch=channel: self.on_servo_ang_entry(ch, event))
|
||||
lbl_val = ctk.CTkLabel(ang_frame, text="90", width=40); lbl_val.pack(side="right")
|
||||
ui['slider_ang'], ui['entry_ang'], ui['lbl_ang_val'] = sld, ent, lbl_val
|
||||
|
||||
sw, led_fb = self.create_switch_with_feedback(parent, "Включить серво", lambda ch=channel: self.on_servo_enable_change(ch))
|
||||
ui['switch_en'], ui['led_fb'] = sw, led_fb
|
||||
|
||||
tel_frame = ctk.CTkFrame(parent); tel_frame.pack(fill="x", padx=10, pady=15)
|
||||
ui['lbl_fb_ang'] = self.create_telemetry_row(tel_frame, "Текущий угол:")
|
||||
stat_frame = ctk.CTkFrame(parent); stat_frame.pack(fill="x", padx=10, pady=5)
|
||||
ui['led_status'] = self.create_led_row(stat_frame, "Статус:")
|
||||
return ui
|
||||
|
||||
# ================= ВКЛАДКА ДАТЧИКОВ VL53L0X =================
|
||||
def create_sensor_tab(self, parent):
|
||||
main_frame = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# --- Глобальное управление ---
|
||||
global_frame = ctk.CTkFrame(main_frame)
|
||||
global_frame.pack(fill="x", padx=10, pady=(0, 10))
|
||||
ctk.CTkLabel(global_frame, text="📏 Глобальные настройки VL53L0X", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(10, 5))
|
||||
|
||||
info_frame = ctk.CTkFrame(global_frame, fg_color="transparent")
|
||||
info_frame.pack(fill="x", padx=20, pady=10)
|
||||
|
||||
self.lbl_sensor_mode = self.create_telemetry_row(info_frame, "Режим:")
|
||||
self.lbl_sensor_mode_id = self.create_telemetry_row(info_frame, "ID режима:")
|
||||
self.lbl_sensor_max_range = self.create_telemetry_row(info_frame, "Макс. дальность (мм):")
|
||||
|
||||
ctrl_frame = ctk.CTkFrame(global_frame, fg_color="transparent")
|
||||
ctrl_frame.pack(fill="x", padx=20, pady=(0, 10))
|
||||
|
||||
ctk.CTkLabel(ctrl_frame, text="Выбрать режим:").pack(side="left", padx=(0, 10))
|
||||
# Предполагаем, что режимов от 0 до 4 (Default, HighAccuracy, LongRange, HighSpeed)
|
||||
self.opt_sensor_mode = ctk.CTkOptionMenu(ctrl_frame, values=["0", "1", "2", "3", "4"], width=100, command=self.on_sensor_mode_change)
|
||||
self.opt_sensor_mode.set("0"); self.opt_sensor_mode.pack(side="left", padx=(0, 20))
|
||||
|
||||
ctk.CTkButton(ctrl_frame, text="🔄 Принудительно обновить все", fg_color="#007bff", hover_color="#0056b3", command=self.on_sensor_publish_all).pack(side="right")
|
||||
|
||||
# --- Сетка каналов (Scrollable) ---
|
||||
scroll_frame = ctk.CTkScrollableFrame(main_frame)
|
||||
scroll_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
scroll_frame.grid_columnconfigure((0, 1, 2, 3), weight=1, uniform="col")
|
||||
|
||||
self.sensor_ui = {}
|
||||
for i in range(MAX_SENSORS):
|
||||
row, col = divmod(i, 4)
|
||||
frame = ctk.CTkFrame(scroll_frame)
|
||||
frame.grid(row=row, column=col, sticky="nsew", padx=5, pady=5)
|
||||
self.sensor_ui[i] = self.create_sensor_card(frame, i)
|
||||
|
||||
def create_sensor_card(self, parent, channel):
|
||||
ui = {}
|
||||
ctk.CTkLabel(parent, text=f"📡 Канал #{channel}", font=ctk.CTkFont(size=14, weight="bold")).pack(pady=(5, 5))
|
||||
|
||||
# Включение и калибровка
|
||||
sw, led_fb = self.create_switch_with_feedback(parent, "Включить", lambda ch=channel: self.on_sensor_enable_change(ch))
|
||||
ui['switch_en'], ui['led_fb'] = sw, led_fb
|
||||
|
||||
cal_stat_frame = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
cal_stat_frame.pack(fill="x", padx=10, pady=5)
|
||||
ctk.CTkLabel(cal_stat_frame, text="Калибровка:").pack(side="left")
|
||||
ui['led_calibrated'] = ctk.CTkLabel(cal_stat_frame, text="●", font=ctk.CTkFont(size=16), text_color=COLOR_OFF)
|
||||
ui['led_calibrated'].pack(side="right")
|
||||
|
||||
# Поля калибровки
|
||||
cal_ctrl_frame = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
cal_ctrl_frame.pack(fill="x", padx=10, pady=5)
|
||||
|
||||
ctk.CTkLabel(cal_ctrl_frame, text="Ближняя (мм):").pack(anchor="w")
|
||||
ent_near = ctk.CTkEntry(cal_ctrl_frame, width=60, justify="right"); ent_near.insert(0, "50"); ent_near.pack(side="left", padx=(0, 5))
|
||||
btn_start = ctk.CTkButton(cal_ctrl_frame, text="Старт", width=60, height=28, command=lambda ch=channel, e=ent_near: self.on_sensor_cal_start(ch, e))
|
||||
btn_start.pack(side="right")
|
||||
|
||||
ctk.CTkLabel(cal_ctrl_frame, text="Дальняя (мм):").pack(anchor="w", pady=(5,0))
|
||||
ent_far = ctk.CTkEntry(cal_ctrl_frame, width=60, justify="right"); ent_far.insert(0, "500"); ent_far.pack(side="left", padx=(0, 5), pady=(5,0))
|
||||
btn_finish = ctk.CTkButton(cal_ctrl_frame, text="Финиш", width=60, height=28, command=lambda ch=channel, e=ent_far: self.on_sensor_cal_finish(ch, e))
|
||||
btn_finish.pack(side="right")
|
||||
|
||||
ctk.CTkButton(parent, text="Сбросить калибровку", fg_color="#6c757d", hover_color="#5a6268", height=28, command=lambda ch=channel: self.on_sensor_clear_cal(ch)).pack(pady=5)
|
||||
|
||||
ui['ent_near'], ui['ent_far'] = ent_near, ent_far
|
||||
|
||||
# Телеметрия
|
||||
tel_frame = ctk.CTkFrame(parent)
|
||||
tel_frame.pack(fill="x", padx=5, pady=5)
|
||||
ui['lbl_dist'] = self.create_telemetry_row(tel_frame, "Дист. (мм):")
|
||||
ui['lbl_raw'] = self.create_telemetry_row(tel_frame, "Сырое (мм):")
|
||||
|
||||
return ui
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ GUI =================
|
||||
def create_switch(self, parent, text, command):
|
||||
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", padx=10, pady=5)
|
||||
ctk.CTkLabel(frame, text=text).pack(side="left")
|
||||
switch = ctk.CTkSwitch(frame, text="", command=command); switch.pack(side="right")
|
||||
return switch
|
||||
|
||||
def create_switch_with_feedback(self, parent, text, command):
|
||||
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", padx=10, pady=5)
|
||||
ctk.CTkLabel(frame, text=text).pack(side="left")
|
||||
feedback_led = ctk.CTkLabel(frame, text="●", font=ctk.CTkFont(size=18), text_color=COLOR_OFF)
|
||||
feedback_led.pack(side="right", padx=(10, 0))
|
||||
switch = ctk.CTkSwitch(frame, text="", command=command); switch.pack(side="right", padx=(10, 0))
|
||||
return switch, feedback_led
|
||||
|
||||
def create_telemetry_row(self, parent, text):
|
||||
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", pady=2)
|
||||
ctk.CTkLabel(frame, text=text, anchor="w", font=ctk.CTkFont(size=12)).pack(side="left")
|
||||
val_lbl = ctk.CTkLabel(frame, text="-", font=ctk.CTkFont(weight="bold", size=12), text_color=COLOR_ACTIVE, anchor="e")
|
||||
val_lbl.pack(side="right")
|
||||
return val_lbl
|
||||
|
||||
def create_led_row(self, parent, text):
|
||||
frame = ctk.CTkFrame(parent, fg_color="transparent"); frame.pack(fill="x", pady=2)
|
||||
ctk.CTkLabel(frame, text=text, anchor="w", font=ctk.CTkFont(size=12)).pack(side="left")
|
||||
led_lbl = ctk.CTkLabel(frame, text="●", font=ctk.CTkFont(size=16), text_color=COLOR_OFF)
|
||||
led_lbl.pack(side="right")
|
||||
return led_lbl
|
||||
|
||||
# ================= MQTT =================
|
||||
def setup_mqtt(self):
|
||||
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="python_scada")
|
||||
self.client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
|
||||
self.client.on_connect = self.on_mqtt_connect
|
||||
self.client.on_disconnect = self.on_mqtt_disconnect
|
||||
self.client.on_message = self.on_mqtt_message
|
||||
try:
|
||||
self.client.connect(MQTT_BROKER, MQTT_PORT, 60)
|
||||
self.client.loop_start()
|
||||
except Exception as e:
|
||||
print(f"Ошибка подключения: {e}")
|
||||
self.update_status(False)
|
||||
|
||||
def on_mqtt_connect(self, client, userdata, flags, reason_code, properties):
|
||||
if reason_code == 0:
|
||||
self.root.after(0, self.update_status, True)
|
||||
client.subscribe("motor/feedback/#")
|
||||
client.subscribe("servo/+/feedback/#")
|
||||
client.subscribe("sensor/feedback/#") # Подписка на датчики
|
||||
else:
|
||||
self.root.after(0, self.update_status, False)
|
||||
|
||||
def on_mqtt_disconnect(self, client, userdata, flags, reason_code, properties):
|
||||
self.root.after(0, self.update_status, False)
|
||||
|
||||
def on_mqtt_message(self, client, userdata, msg):
|
||||
self.root.after(0, self.process_feedback, msg.topic, msg.payload.decode('utf-8'))
|
||||
|
||||
def process_feedback(self, topic, val):
|
||||
parts = topic.split('/')
|
||||
|
||||
# --- MOTOR ---
|
||||
if parts[0] == 'motor' and parts[1] == 'feedback':
|
||||
if topic == "motor/feedback/rpm": self.lbl_fb_rpm.configure(text=val)
|
||||
elif topic == "motor/feedback/totalsteps": self.lbl_fb_steps.configure(text=val)
|
||||
elif topic == "motor/feedback/is_run":
|
||||
is_run = val == "true"
|
||||
self.lbl_fb_run.configure(text="Да" if is_run else "Нет", text_color=COLOR_OK if is_run else COLOR_ERR)
|
||||
elif topic == "motor/feedback/tmc/current_percent": self.lbl_cur_val.configure(text=val); self.sld_current.set(int(val))
|
||||
elif topic == "motor/feedback/tmc/microsteps": self.opt_msteps.set(val)
|
||||
elif topic == "motor/feedback/tmc/sg_result": self.lbl_fb_sg.configure(text=val)
|
||||
elif topic == "motor/feedback/tmc/interstep_duration": self.lbl_fb_interstep.configure(text=val)
|
||||
elif topic == "motor/feedback/tmc/status/current_scaling": self.lbl_fb_cscale.configure(text=val)
|
||||
elif topic == "motor/feedback/tmc/status/over_temp": self.update_led(self.led_over_temp, val, True)
|
||||
elif topic == "motor/feedback/tmc/status/short_to_ground": self.update_led(self.led_short_gnd, val, True)
|
||||
elif topic == "motor/feedback/tmc/status/open_load": self.update_led(self.led_open_load, val, True)
|
||||
elif topic == "motor/feedback/tmc/status/stealth_chop_active": self.update_led(self.led_stealth_act, val, False)
|
||||
elif topic == "motor/feedback/tmc/status/standstill": self.update_led(self.led_standstill, val, False)
|
||||
elif topic == "motor/feedback/driver/status":
|
||||
is_on = val == "on"; self.driver_pending = False
|
||||
if self.sw_driver.get() != is_on: self.sw_driver.select() if is_on else self.sw_driver.deselect()
|
||||
self.led_driver_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF)
|
||||
elif topic == "motor/feedback/tmc/status":
|
||||
is_on = val == "on"; self.tmc_pending = False
|
||||
if self.sw_tmc_enable.get() != is_on: self.sw_tmc_enable.select() if is_on else self.sw_tmc_enable.deselect()
|
||||
self.led_tmc_fb.configure(text_color=COLOR_OK if is_on else COLOR_OFF)
|
||||
|
||||
# --- SERVO ---
|
||||
elif parts[0] == 'servo' and len(parts) == 4 and parts[2] == 'feedback':
|
||||
try:
|
||||
ch = int(parts[1]); param = parts[3]
|
||||
if ch in self.servo_ui:
|
||||
ui = self.servo_ui[ch]
|
||||
if param == 'angle': ui['lbl_fb_ang'].configure(text=val)
|
||||
elif param == 'status':
|
||||
is_on = val == "on"; self.servo_pending[ch] = False
|
||||
if ui['switch_en'].get() != is_on: ui['switch_en'].select() if is_on else ui['switch_en'].deselect()
|
||||
ui['led_fb'].configure(text_color=COLOR_OK if is_on else COLOR_OFF)
|
||||
self.update_led(ui['led_status'], val, False)
|
||||
except ValueError: pass
|
||||
|
||||
# --- SENSOR ---
|
||||
elif parts[0] == 'sensor' and parts[1] == 'feedback':
|
||||
if len(parts) == 3: # Глобальные sensor/feedback/mode...
|
||||
param = parts[2]
|
||||
if param == 'mode': self.lbl_sensor_mode.configure(text=val)
|
||||
elif param == 'mode_id': self.lbl_sensor_mode_id.configure(text=val); self.opt_sensor_mode.set(val)
|
||||
elif param == 'max_range': self.lbl_sensor_max_range.configure(text=val)
|
||||
elif len(parts) == 4: # Канальные sensor/feedback/{ch}/...
|
||||
try:
|
||||
ch = int(parts[2]); param = parts[3]
|
||||
if ch in self.sensor_ui:
|
||||
ui = self.sensor_ui[ch]
|
||||
if param == 'status':
|
||||
is_on = val == "on"; self.sensor_pending[ch] = False
|
||||
if ui['switch_en'].get() != is_on: ui['switch_en'].select() if is_on else ui['switch_en'].deselect()
|
||||
ui['led_fb'].configure(text_color=COLOR_OK if is_on else COLOR_OFF)
|
||||
elif param == 'calibrated':
|
||||
is_cal = val == "true"
|
||||
ui['led_calibrated'].configure(text_color=COLOR_OK if is_cal else COLOR_OFF)
|
||||
elif param == 'distance':
|
||||
if val == "out_of_range":
|
||||
ui['lbl_dist'].configure(text="Вне диапазона", text_color=COLOR_WARN)
|
||||
else:
|
||||
ui['lbl_dist'].configure(text=val, text_color=COLOR_ACTIVE)
|
||||
elif param == 'raw':
|
||||
ui['lbl_raw'].configure(text=val, text_color=COLOR_ACTIVE)
|
||||
except ValueError: pass
|
||||
|
||||
# ================= ОБРАБОТЧИКИ СОБЫТИЙ =================
|
||||
def publish(self, topic, payload):
|
||||
if self.client.is_connected(): self.client.publish(topic, str(payload), qos=1)
|
||||
|
||||
def update_status(self, is_online):
|
||||
self.lbl_status.configure(text="● Подключено" if is_online else "● Отключено", text_color=COLOR_OK if is_online else COLOR_ERR)
|
||||
|
||||
def update_led(self, label, val, is_error):
|
||||
is_true = val in ["true", "1", "on"]
|
||||
label.configure(text_color=COLOR_ERR if (is_true and is_error) else (COLOR_OK if is_true else COLOR_OFF))
|
||||
|
||||
# --- Motor Handlers ---
|
||||
def on_rpm_slider_change(self, value):
|
||||
int_val = int(value); self.ent_rpm.delete(0, ctk.END); self.ent_rpm.insert(0, str(int_val))
|
||||
self.lbl_rpm_val.configure(text=str(int_val)); self.publish("motor/control/rpm", int_val)
|
||||
def on_rpm_entry_apply(self, event=None):
|
||||
try:
|
||||
int_val = max(-1000, min(1000, int(self.ent_rpm.get())))
|
||||
self.sld_rpm.set(int_val); self.lbl_rpm_val.configure(text=str(int_val)); self.publish("motor/control/rpm", int_val)
|
||||
except ValueError: self.ent_rpm.delete(0, ctk.END); self.ent_rpm.insert(0, str(int(self.sld_rpm.get())))
|
||||
def on_current_change(self, value):
|
||||
int_val = int(value); self.lbl_cur_val.configure(text=str(int_val)); self.publish("motor/control/tmc/current_percent", int_val)
|
||||
def on_sg_apply(self):
|
||||
val = self.ent_sg.get()
|
||||
if val.isdigit() and 0 <= int(val) <= 255: self.publish("motor/control/tmc/stallguard", int(val))
|
||||
def on_msteps_change(self, choice): self.publish("motor/control/tmc/microsteps", int(choice))
|
||||
def on_reset_steps(self): self.publish("motor/control/totalsteps/reset", "1")
|
||||
def on_driver_change(self):
|
||||
is_on = self.sw_driver.get(); self.driver_pending = True; self.led_driver_fb.configure(text_color=COLOR_PENDING)
|
||||
self.publish("motor/control/driver", "on" if is_on else "off")
|
||||
def on_tmc_enable_change(self):
|
||||
is_on = self.sw_tmc_enable.get(); self.tmc_pending = True; self.led_tmc_fb.configure(text_color=COLOR_PENDING)
|
||||
self.publish("motor/control/tmc/enable", "on" if is_on else "off")
|
||||
def on_stealth_change(self): self.publish("motor/control/tmc/stealthchop", "on" if self.sw_stealth.get() else "off")
|
||||
def on_cool_change(self): self.publish("motor/control/tmc/coolstep", "on" if self.sw_cool.get() else "off")
|
||||
|
||||
# --- Servo Handlers ---
|
||||
def on_servo_ang_slider(self, channel, value):
|
||||
int_val = int(value); ui = self.servo_ui[channel]
|
||||
ui['entry_ang'].delete(0, ctk.END); ui['entry_ang'].insert(0, str(int_val))
|
||||
ui['lbl_ang_val'].configure(text=str(int_val)); self.publish(f"servo/control/{channel}/angle", int_val)
|
||||
def on_servo_ang_entry(self, channel, event=None):
|
||||
ui = self.servo_ui[channel]
|
||||
try:
|
||||
int_val = max(0, min(180, int(ui['entry_ang'].get())))
|
||||
ui['slider_ang'].set(int_val); ui['lbl_ang_val'].configure(text=str(int_val)); self.publish(f"servo/control/{channel}/angle", int_val)
|
||||
except ValueError: ui['entry_ang'].delete(0, ctk.END); ui['entry_ang'].insert(0, str(int(ui['slider_ang'].get())))
|
||||
def on_servo_enable_change(self, channel):
|
||||
ui = self.servo_ui[channel]; is_on = ui['switch_en'].get()
|
||||
self.servo_pending[channel] = True; ui['led_fb'].configure(text_color=COLOR_PENDING)
|
||||
self.publish(f"servo/control/{channel}/enable", "on" if is_on else "off")
|
||||
|
||||
# --- Sensor Handlers ---
|
||||
def on_sensor_mode_change(self, choice):
|
||||
self.publish("sensor/control/mode", int(choice))
|
||||
|
||||
def on_sensor_publish_all(self):
|
||||
self.publish("sensor/control/publish_all", "1")
|
||||
|
||||
def on_sensor_enable_change(self, channel):
|
||||
ui = self.sensor_ui[channel]; is_on = ui['switch_en'].get()
|
||||
self.sensor_pending[channel] = True; ui['led_fb'].configure(text_color=COLOR_PENDING)
|
||||
self.publish(f"sensor/control/enable/{channel}", "on" if is_on else "off")
|
||||
|
||||
def on_sensor_cal_start(self, channel, entry_widget):
|
||||
val = entry_widget.get()
|
||||
if val.isdigit():
|
||||
self.publish(f"sensor/control/calibrate/start/{channel}", int(val))
|
||||
|
||||
def on_sensor_cal_finish(self, channel, entry_widget):
|
||||
val = entry_widget.get()
|
||||
if val.isdigit():
|
||||
self.publish(f"sensor/control/calibrate/finish/{channel}", int(val))
|
||||
|
||||
def on_sensor_clear_cal(self, channel):
|
||||
self.publish(f"sensor/control/clear_cal/{channel}", "1")
|
||||
|
||||
def run(self):
|
||||
self.root.mainloop()
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MotorSCADA()
|
||||
app.run()
|
||||
BIN
backend_control/img/gui_interface.png
Normal file
BIN
backend_control/img/gui_interface.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
BIN
backend_control/img/servo_interface.png
Normal file
BIN
backend_control/img/servo_interface.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
2
backend_control/requirements.txt
Normal file
2
backend_control/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
paho-mqtt
|
||||
customtkinter
|
||||
2
kicad/ozon/.gitignore
vendored
Normal file
2
kicad/ozon/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.lck
|
||||
.history
|
||||
1
kicad/ozon/.history
Submodule
1
kicad/ozon/.history
Submodule
Submodule kicad/ozon/.history added at 2a8a462227
2
kicad/ozon/ozon.kicad_pcb
Normal file
2
kicad/ozon/ozon.kicad_pcb
Normal file
@@ -0,0 +1,2 @@
|
||||
(kicad_pcb (version 20260206) (generator "pcbnew") (generator_version "10.0")
|
||||
)
|
||||
105
kicad/ozon/ozon.kicad_prl
Normal file
105
kicad/ozon/ozon.kicad_prl
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"board": {
|
||||
"active_layer": 0,
|
||||
"active_layer_preset": "",
|
||||
"auto_track_width": true,
|
||||
"hidden_netclasses": [],
|
||||
"hidden_nets": [],
|
||||
"high_contrast_mode": 0,
|
||||
"net_color_mode": 1,
|
||||
"opacity": {
|
||||
"images": 0.6,
|
||||
"pads": 1.0,
|
||||
"shapes": 1.0,
|
||||
"tracks": 1.0,
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"prototype_zone_fills": false,
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
"graphics": true,
|
||||
"keepouts": true,
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pads": true,
|
||||
"text": true,
|
||||
"tracks": true,
|
||||
"vias": true,
|
||||
"zones": true
|
||||
},
|
||||
"visible_items": [
|
||||
"vias",
|
||||
"footprint_text",
|
||||
"footprint_anchors",
|
||||
"ratsnest",
|
||||
"grid",
|
||||
"footprints_front",
|
||||
"footprints_back",
|
||||
"footprint_values",
|
||||
"footprint_references",
|
||||
"tracks",
|
||||
"drc_errors",
|
||||
"drawing_sheet",
|
||||
"bitmaps",
|
||||
"pads",
|
||||
"zones",
|
||||
"drc_warnings",
|
||||
"drc_exclusions",
|
||||
"locked_item_shadows",
|
||||
"conflict_shadows",
|
||||
"shapes",
|
||||
"board_outline_area",
|
||||
"ly_points"
|
||||
],
|
||||
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
|
||||
"zone_display_mode": 0
|
||||
},
|
||||
"git": {
|
||||
"integration_disabled": false,
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
"ssh_key": ""
|
||||
},
|
||||
"meta": {
|
||||
"filename": "ozon.kicad_prl",
|
||||
"version": 5
|
||||
},
|
||||
"net_inspector_panel": {
|
||||
"col_hidden": [],
|
||||
"col_order": [],
|
||||
"col_widths": [],
|
||||
"custom_group_rules": [],
|
||||
"expanded_rows": [],
|
||||
"filter_by_net_name": true,
|
||||
"filter_by_netclass": true,
|
||||
"filter_text": "",
|
||||
"group_by_constraint": false,
|
||||
"group_by_netclass": false,
|
||||
"show_time_domain_details": false,
|
||||
"show_unconnected_nets": false,
|
||||
"show_zero_pad_nets": false,
|
||||
"sort_ascending": true,
|
||||
"sorting_column": -1
|
||||
},
|
||||
"open_jobsets": [],
|
||||
"project": {
|
||||
"files": []
|
||||
},
|
||||
"schematic": {
|
||||
"hierarchy_collapsed": [],
|
||||
"selection_filter": {
|
||||
"graphics": true,
|
||||
"images": true,
|
||||
"labels": true,
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pins": true,
|
||||
"ruleAreas": true,
|
||||
"symbols": true,
|
||||
"text": true,
|
||||
"wires": true
|
||||
}
|
||||
}
|
||||
}
|
||||
435
kicad/ozon/ozon.kicad_pro
Normal file
435
kicad/ozon/ozon.kicad_pro
Normal file
@@ -0,0 +1,435 @@
|
||||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"ipc2581": {
|
||||
"bom_rev": "",
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": "",
|
||||
"sch_revision": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"component_class_settings": {
|
||||
"assignments": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"sheet_component_classes": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"field_name_whitespace": "warning",
|
||||
"footprint_filter": "ignore",
|
||||
"footprint_link_issues": "warning",
|
||||
"four_way_junction": "ignore",
|
||||
"ground_pin_not_ground": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"isolated_pin_label": "warning",
|
||||
"label_dangling": "error",
|
||||
"label_multiple_wires": "warning",
|
||||
"lib_symbol_issues": "warning",
|
||||
"lib_symbol_mismatch": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"same_local_global_label": "warning",
|
||||
"similar_label_and_power": "warning",
|
||||
"similar_labels": "warning",
|
||||
"similar_power": "warning",
|
||||
"simulation_model_issue": "ignore",
|
||||
"single_global_label": "ignore",
|
||||
"stacked_pin_name": "warning",
|
||||
"unannotated": "error",
|
||||
"unconnected_wire_endpoint": "warning",
|
||||
"undefined_netclass": "error",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "ozon.kicad_pro",
|
||||
"version": 3
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 2147483647,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 5
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": []
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"plot": "",
|
||||
"specctra_dsn": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"annotation": {
|
||||
"method": 0,
|
||||
"sort_order": 0
|
||||
},
|
||||
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from BOM",
|
||||
"name": "${EXCLUDE_FROM_BOM}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Board",
|
||||
"name": "${EXCLUDE_FROM_BOARD}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"filter_string": "",
|
||||
"group_symbols": true,
|
||||
"include_excluded_from_bom": true,
|
||||
"name": "Default Editing",
|
||||
"sort_asc": true,
|
||||
"sort_field": "Обозначение"
|
||||
},
|
||||
"bus_aliases": {},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"hop_over_size_choice": 0,
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.375,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 25.0,
|
||||
"text_offset_ratio": 0.15
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"reuse_designators": true,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0,
|
||||
"top_level_sheets": [
|
||||
{
|
||||
"filename": "ozon.kicad_sch",
|
||||
"name": "Корневой лист",
|
||||
"uuid": "8eb30ef0-8ef0-4108-b9d0-4c3ae489af42"
|
||||
}
|
||||
],
|
||||
"used_designators": "R1,M1-5,#PWR1-10,U1-6",
|
||||
"variants": []
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"8eb30ef0-8ef0-4108-b9d0-4c3ae489af42",
|
||||
"Корневой лист"
|
||||
]
|
||||
],
|
||||
"text_variables": {},
|
||||
"tuning_profiles": {
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"tuning_profiles_impedance_geometric": []
|
||||
}
|
||||
}
|
||||
6460
kicad/ozon/ozon.kicad_sch
Normal file
6460
kicad/ozon/ozon.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
26
specification/README.md
Normal file
26
specification/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Наброски будущей схемы конвейера
|
||||
|
||||

|
||||
|
||||
## Список материалов:
|
||||
|
||||
В этот список необходимо доложить энкодер для двигателя!
|
||||
|
||||
| Название | Сумма, ₽ | Количество |
|
||||
| :--- | :--- | :--- |
|
||||
| Медная лента для удаления припоя / Оплетка для выпайки диаметр 2 мм длина 1.5 м | 164 | 1 |
|
||||
| Набор проводов для пайки | 855 | 1 |
|
||||
| Флюс гель универсальный безотмывочный, для пайки микросхем и компонентов Flux RMA-223-UV-10г | 162 | 1 |
|
||||
| Припой для пайки с канифолью 1мм 50гр ПОС-61 на катушке (ГОСТ) | 423 | 1 |
|
||||
| ALIENTEK Паяльник 140 Вт, 7 предметов | 4 985 | 1 |
|
||||
| Преобразователь DC-DC понижающий с 8-60V до 1-36V 15A max | 805 | 1 |
|
||||
| Беспаечная макетная плата (breadboard) MB-102, 830 точек, для Arduino и прочих устройств | 1 112 | 2 |
|
||||
| 120 шт. Провода перемычки для макетных плат, соединительные провода для модулей, контроллеров arduino (10 см) 3 вида по 40 шт папа-папа, мама-мама, папа-мама | 636 | 2 |
|
||||
| Homeled, Блок питания, 12V, 200W, 180-265 вольт. С клеммами. Импульсный для светодиодных лент и светильников | 599 | 1 |
|
||||
| Сервопривод MG996R, 10 шт, 180 , металлические шестерни, для Arduino, роботов и RC, размеры стандарт | 2 990 | 1 |
|
||||
| PWM PCA9685 драйвер на 16 сервоприводов расширитель портов с I2C интерфейсом для Led и Servo (12 bit, I2C) в IIC/I2C/TWI/SPI c тестером сервоприводов 3 режима, набор | 1 178 | 2 |
|
||||
| Модуль I2C-мультиплексора CJMCU-9548 на базе TCA9548A / PCA9548A | 532 | 2 |
|
||||
| Модуль VL53L0X лазерный дальномер GY-530 (до 2м, питание 3-5В, I2C) | 1064 | 4 |
|
||||
| Модуль ESP32 TYPE-C CH340C + плата расширения ESP32 | 1040 | 2 |
|
||||
|
||||
[Ссылка на товары](https://www.ozon.ru/cart?share=wGhPkQd)
|
||||
54
specification/mindmap.md
Normal file
54
specification/mindmap.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Алгоритм работы сортировщика
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((Алгоритм работы))
|
||||
Этап 1: Ожидание и Движение
|
||||
Arduino запускает шаговый двигатель
|
||||
Лента движется с постоянной скоростью
|
||||
Этап 2: Детекция и Анализ
|
||||
Объект проходит под датчиком и камерой
|
||||
Предмет соответствует габаритам?
|
||||
Да
|
||||
Есть признак круга в сечении?
|
||||
Да - Требуется доупаковка
|
||||
Секция 2
|
||||
Нет - Подходит для сортировки
|
||||
Секция 3
|
||||
Нет
|
||||
Не подходит по габаритам
|
||||
Секция 1
|
||||
Этап 3: Трекинг Синхронизация
|
||||
Arduino отсчитывает шаги двигателя
|
||||
Вычисляется момент времени T
|
||||
Этап 4: Маршрутизация
|
||||
В момент T сервопривод поворачивает барьер на 45°
|
||||
Объект смещается в выбранную зону
|
||||
Сервопривод возвращается в исходное положение
|
||||
Этап 5: Завершение
|
||||
Объект попадает в накопитель
|
||||
Система возвращается в состояние Ожидание
|
||||
```
|
||||
|
||||
## Блок схема:
|
||||
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Этап 1: Ожидание и Движение] --> Detect[Этап 2: Детекция и Анализ]
|
||||
Detect --> SizeCheck{Предмет соответствует<br/>габаритам?}
|
||||
|
||||
SizeCheck -->|Нет| Reject[Секция 1:<br/>Не габарит]
|
||||
SizeCheck -->|Да| CircleCheck{Есть признак<br/>круга?}
|
||||
|
||||
CircleCheck -->|Да| Repack[Секция 2:<br/>Требуется доупаковка]
|
||||
CircleCheck -->|Нет| Sort[Секция 3:<br/>Подходит для сортировки]
|
||||
|
||||
Reject --> Track[Этап 3: Трекинг]
|
||||
Repack --> Track
|
||||
Sort --> Track
|
||||
|
||||
Track --> Route[Этап 4: Маршрутизация]
|
||||
Route --> End[Этап 5: Завершение]
|
||||
End --> Start
|
||||
```
|
||||
277
specification/mvp_1.drawio
Normal file
277
specification/mvp_1.drawio
Normal file
@@ -0,0 +1,277 @@
|
||||
<mxfile host="server.home">
|
||||
<diagram name="Страница-1" id="mppyHKg2Vbg7W8e0-Fd9">
|
||||
<mxGraphModel dx="3902" dy="2031" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-2" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="37" y="773" as="sourcePoint" />
|
||||
<mxPoint x="1547" y="773" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-6" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="257" y="720" as="sourcePoint" />
|
||||
<mxPoint x="1617" y="720" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-7" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="37" y="770" as="sourcePoint" />
|
||||
<mxPoint x="107" y="720" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-8" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="1547" y="774" as="sourcePoint" />
|
||||
<mxPoint x="1617" y="724" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-9" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="<font style="font-size: 30px;">1</font>" vertex="1">
|
||||
<mxGeometry height="80" width="160" x="847" y="830" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-10" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="<font style="font-size: 30px;">2</font>" vertex="1">
|
||||
<mxGeometry height="80" width="160" x="1097" y="830" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-11" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="<font style="font-size: 30px;">3</font>" vertex="1">
|
||||
<mxGeometry height="80" width="160" x="1377" y="830" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-12" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="" vertex="1">
|
||||
<mxGeometry height="80" width="160" x="67" y="680" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-13" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="227" y="680" as="sourcePoint" />
|
||||
<mxPoint x="257" y="660" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-14" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="227" y="760" as="sourcePoint" />
|
||||
<mxPoint x="257" y="740" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-16" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="67" y="680" as="sourcePoint" />
|
||||
<mxPoint x="97" y="660" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-17" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="97" y="660" as="sourcePoint" />
|
||||
<mxPoint x="257" y="660" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-18" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="257" y="740" as="sourcePoint" />
|
||||
<mxPoint x="257" y="660" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-19" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="" vertex="1">
|
||||
<mxGeometry height="80" width="50" x="477" y="390" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-20" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-19" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="567" y="620" as="sourcePoint" />
|
||||
<mxPoint x="527" y="530" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-21" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-19" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="487" y="520" as="sourcePoint" />
|
||||
<mxPoint x="477" y="530" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-22" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="477" y="530" as="sourcePoint" />
|
||||
<mxPoint x="527" y="530" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-23" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;dashed=1;dashPattern=12 12;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="477" y="530" as="sourcePoint" />
|
||||
<mxPoint x="417" y="750" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-24" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;dashed=1;dashPattern=12 12;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="527" y="530" as="sourcePoint" />
|
||||
<mxPoint x="587" y="750" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-26" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
|
||||
<mxGeometry height="80" width="50" x="307" y="390" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-27" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-26" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;dashed=1;dashPattern=8 8;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="357" y="580" as="sourcePoint" />
|
||||
<mxPoint x="332" y="740" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-29" parent="1" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;fillColor=#f0a30a;strokeColor=#BD7000;fillStyle=solid;direction=west;gradientDirection=north;shadow=1;size=20;autosizeText=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontColor=#000000;fontSize=17;align=left;" value="Лазерный датчик расстояния.<br>В нашем случае он измеряет высоту объекта.<br>Так как камера у нас расположена перпендикулярно движению ленты. И корректно не сможет определить высоту объектов" vertex="1">
|
||||
<mxGeometry height="160" width="315" x="42" y="210" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-30" parent="1" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;fillColor=#f0a30a;strokeColor=#BD7000;fillStyle=solid;direction=west;gradientDirection=north;shadow=1;size=20;autosizeText=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontColor=#000000;fontSize=24;align=left;" value="Тут у нас установлена камера, первично мы ее откалибруем и сможем визуально оценить размер объекта" vertex="1">
|
||||
<mxGeometry height="160" width="315" x="387" y="210" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-32" parent="1" style="ellipse;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="" vertex="1">
|
||||
<mxGeometry height="40" width="40" x="804" y="640" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-37" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-38" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=12 12;" target="jfPUc6vKa1uVjcLF1lrC-32" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="847" y="370" as="sourcePoint" />
|
||||
<mxPoint x="967" y="710" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-38" parent="1" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;fillColor=#f0a30a;strokeColor=#BD7000;fillStyle=solid;direction=west;gradientDirection=north;shadow=1;size=20;autosizeText=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontColor=#000000;fontSize=26;align=left;" value="Датчик ультразвуковой, определят что объект приехал на сортировку" vertex="1">
|
||||
<mxGeometry height="160" width="315" x="737" y="190" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-39" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-40" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.542;exitY=0.018;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=12 12;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="1197" y="360" as="sourcePoint" />
|
||||
<mxPoint x="937" y="610" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-40" parent="1" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;fillColor=#f0a30a;strokeColor=#BD7000;fillStyle=solid;direction=west;gradientDirection=north;shadow=1;size=20;autosizeText=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontColor=#000000;fontSize=25;align=left;" value="Сервопривод с ограничителем движения который не дает объекту двигаться дальше." vertex="1">
|
||||
<mxGeometry height="160" width="315" x="1062" y="190" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-42" connectable="0" parent="1" style="group" value="" vertex="1">
|
||||
<mxGeometry height="80" width="90" x="882" y="620" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-31" parent="jfPUc6vKa1uVjcLF1lrC-42" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter;" value="" vertex="1">
|
||||
<mxGeometry height="80" width="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-34" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-42" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="45" y="80" as="sourcePoint" />
|
||||
<mxPoint x="95" y="150" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-35" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-42" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="95" y="150" as="sourcePoint" />
|
||||
<mxPoint x="95" y="40" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-36" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-42" source="jfPUc6vKa1uVjcLF1lrC-31" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="45" y="-20" as="sourcePoint" />
|
||||
<mxPoint x="95" y="40" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-48" parent="1" style="ellipse;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
|
||||
<mxGeometry height="40" width="40" x="1062" y="640" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-49" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-38" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=12 12;" target="jfPUc6vKa1uVjcLF1lrC-48" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="948" y="430" as="sourcePoint" />
|
||||
<mxPoint x="877" y="720" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-50" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-40" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.542;exitY=-0.004;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;dashPattern=12 12;" target="jfPUc6vKa1uVjcLF1lrC-44" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="1436" y="410" as="sourcePoint" />
|
||||
<mxPoint x="1167" y="673" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-51" connectable="0" parent="1" style="group" value="" vertex="1">
|
||||
<mxGeometry height="80" width="90" x="1137" y="620" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-44" parent="jfPUc6vKa1uVjcLF1lrC-51" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
|
||||
<mxGeometry height="80" width="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-45" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-51" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="45" y="80" as="sourcePoint" />
|
||||
<mxPoint x="140" y="90" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-46" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-51" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="140" y="90" as="sourcePoint" />
|
||||
<mxPoint x="140" y="-10" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-47" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-51" source="jfPUc6vKa1uVjcLF1lrC-44" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="45" y="-20" as="sourcePoint" />
|
||||
<mxPoint x="140" y="-10" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-52" connectable="0" parent="1" style="group" value="" vertex="1">
|
||||
<mxGeometry height="80" width="90" x="1407" y="620" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-53" parent="jfPUc6vKa1uVjcLF1lrC-52" style="rounded=0;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
|
||||
<mxGeometry height="80" width="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-54" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-52" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="45" y="80" as="sourcePoint" />
|
||||
<mxPoint x="140" y="90" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-55" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-52" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="140" y="90" as="sourcePoint" />
|
||||
<mxPoint x="140" y="-10" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-56" edge="1" parent="jfPUc6vKa1uVjcLF1lrC-52" source="jfPUc6vKa1uVjcLF1lrC-53" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=none;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;endFill=0;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="45" y="-20" as="sourcePoint" />
|
||||
<mxPoint x="140" y="-10" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-57" parent="1" style="ellipse;whiteSpace=wrap;html=1;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;" value="" vertex="1">
|
||||
<mxGeometry height="40" width="40" x="1337" y="640" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-58" edge="1" parent="1" source="jfPUc6vKa1uVjcLF1lrC-38" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=12 12;" target="jfPUc6vKa1uVjcLF1lrC-57" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="957" y="530" as="sourcePoint" />
|
||||
<mxPoint x="1144" y="820" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jfPUc6vKa1uVjcLF1lrC-59" edge="1" parent="1" style="edgeStyle=none;orthogonalLoop=1;jettySize=auto;html=1;rounded=0;fontFamily=Architects Daughter;fontSource=https://fonts.googleapis.com/css?family=Architects+Daughter;fontSize=16;startSize=14;endArrow=open;endSize=14;sourcePerimeterSpacing=8;targetPerimeterSpacing=8;curved=1;dashed=1;dashPattern=12 12;" value="">
|
||||
<mxGeometry relative="1" width="140" as="geometry">
|
||||
<Array as="points" />
|
||||
<mxPoint x="1207" y="350" as="sourcePoint" />
|
||||
<mxPoint x="1447" y="620" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
BIN
specification/mvp_1.jpg
Normal file
BIN
specification/mvp_1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
97
specification/specification.md
Normal file
97
specification/specification.md
Normal file
@@ -0,0 +1,97 @@
|
||||
Привет! Как инженер-конструктор и эксперт по электронике, я с удовольствием помогу тебе спроектировать этот макет. Задача ясна: создаем систему автоматизированной сортировки с точным управлением скоростью и маршрутизацией по высоте.
|
||||
|
||||
Давай разберем твои вопросы по порядку.
|
||||
|
||||
### 1. Выбор двигателя: Шаговый vs Коллекторный
|
||||
|
||||
Для твоей задачи — **максимально точного выдерживания скорости** и управления с микроконтроллера — выбор очевиден.
|
||||
|
||||
| Характеристика | Коллекторный двигатель (DC) | Шаговый двигатель (Stepper) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Точность скорости** | Низкая. Скорость зависит от напряжения и нагрузки. Для точности нужен энкодер (обратная связь). | **Высокая**. Скорость задается частотой импульсов. Постоянна при заданной нагрузке. |
|
||||
| **Управление** | Простое (PWM/ШИМ). | Требует специальный драйвер (например, A4988 или TMC2208). |
|
||||
| **Пуск/Стоп** | Плавный разгон, сложно точно позиционировать. | **Идеальный**. Можно точно задать количество шагов и мгновенно остановиться. |
|
||||
| **Крутящий момент** | Высокий на высоких оборотах. | Высокий на низких скоростях (идеально для ленты). |
|
||||
| **Сложность реализации** | Очень низкая. | Средняя (нужен драйвер и настройка тока). |
|
||||
|
||||
**Мой вердикт:** Используй **шаговый двигатель (например, NEMA 17)**.
|
||||
Если ты хочешь «выдерживать скорость максимально точно», обычный DC-мотор тебя разочарует: при увеличении веса объекта на ленте скорость будет падать. Шаговый же двигатель будет поддерживать заданный темп, пока не будет достигнут предел его крутящего момента.
|
||||
|
||||
---
|
||||
|
||||
### 2. Селекторы (Сервоприводы)
|
||||
|
||||
Использование сервоприводов с барьером под 45° — это классическое и эффективное решение для макета.
|
||||
|
||||
**Советы по реализации:**
|
||||
* **Механика:** Чтобы объект не застревал и не «зажевывался», сделай барьер обтекаемым или используй гладкий пластик/алюминий.
|
||||
* **Угол:** 45 градусов — оптимально, но убедись, что при срабатывании сервопривод не толкает объект слишком резко, чтобы тот не перевернулся.
|
||||
* **Логика:** В промышленном подходе, как отмечено в контексте, важно отслеживать «корректность маршрутизации» [1]. Рекомендую добавить в код проверку: действительно ли объект ушел в нужную зону, или произошла ошибка.
|
||||
|
||||
---
|
||||
|
||||
### 3. Датчик определения высоты
|
||||
|
||||
Для измерения высоты объекта сверху, когда лента движется, тебе подойдут три варианта в зависимости от требуемой точности:
|
||||
|
||||
1. **Ультразвуковой датчик (HC-SR04):** Самый дешевый и популярный.
|
||||
* *Минус:* Широкий луч (может «цеплять» борта конвейера) и низкая скорость обновления.
|
||||
2. **ИК-датчик расстояния (Sharp GP2Y0A21YK0F):** Более точный, узкий луч.
|
||||
* *Минус:* Аналоговый сигнал, чувствителен к цвету объекта (черный поглощает ИК).
|
||||
3. **Лазерный ToF-датчик (VL53L0X / VL53L1X):** **Мой выбор для этого проекта.**
|
||||
* *Плюсы:* Измеряет время полета фотона (Time-of-Flight), работает независимо от цвета объекта, очень высокая точность (до миллиметров) и очень узкий луч.
|
||||
|
||||
---
|
||||
|
||||
### Итоговая архитектура (предложение):
|
||||
|
||||
* **Контроллер:** Arduino (Uno или Mega, если планируешь расширять количество датчиков).
|
||||
* **Привод ленты:** Шаговый двигатель NEMA 17 $\rightarrow$ Драйвер A4988/TMC2208 $\rightarrow$ Arduino.
|
||||
* **Сортировка:** 3 $\times$ Сервоприводы SG90 или MG90S (с металлическим редуктором надежнее).
|
||||
* **Датчик высоты:** VL53L0X (I2C интерфейс).
|
||||
* **Питание:** Отдельный блок питания 12В для двигателя и 5В для Arduino/сервоприводов (общий минус/земля).
|
||||
|
||||
**Важное замечание из спецификации:**
|
||||
Не забудь предусмотреть в логике категорию «Не подходит для сортировки» [1]. Если датчик зафиксирует объект, который либо слишком высокий, либо слишком низкий (выходит за допустимые габариты), система должна отправить его в отдельный поток или оставить на ленте до конца [1].
|
||||
|
||||
Поскольку мы проектируем макет, важно учитывать не только функциональность, но и ограничения, упомянутые в техническом контексте: доступную площадь участка [1], разнообразие форм товаров (включая круглые в сечении) и необходимость бережного обращения с хрупкими или ценными объектами [1].
|
||||
|
||||
|
||||
# Сервоприводы с барьером — это надежный вариант, но в зависимости от того, какие объекты вы будете сортировать, рассмотрите следующие альтернативы:
|
||||
|
||||
### 1. Электромагнитные толкатели (Solenoids)
|
||||
Вместо плавного поворота барьера, соленоид дает резкий линейный толчок.
|
||||
* **Плюсы:** Очень высокая скорость срабатывания, компактность [1].
|
||||
* **Минусы:** Резкий удар. Если ваши объекты хрупкие, этот вариант **не рекомендуется**, так как он противоречит требованию к «мягкости обращения» [1].
|
||||
* **Когда использовать:** Для прочных, тяжелых или однотипных деталей.
|
||||
|
||||
### 2. Откидная платформа (Tipping Platform)
|
||||
Это участок конвейера, который может наклоняться в сторону или под ним открывается люк.
|
||||
* **Плюсы:** Позволяет перемещать объекты за счет гравитации. Это один из самых бережных способов перемещения, что важно для ценных или хрупких товаров [1].
|
||||
* **Минусы:** Требует более сложной механической интеграции в саму ленту.
|
||||
* **Когда использовать:** Если приоритетом является сохранность объекта и минимизация механических ударов.
|
||||
|
||||
### 3. Поворотный диск-распределитель (Rotating Disc)
|
||||
В конце основной ленты устанавливается диск, который поворачивается на определенный угол, направляя объект в один из трех лотков.
|
||||
* **Плюсы:** Занимает минимум места (оптимально для ограниченной площади участка [1]). Отлично работает с объектами, имеющими круг в сечении [1].
|
||||
* **Минусы:** Требует точной синхронизации остановки ленты и поворота диска.
|
||||
* **Когда использовать:** Если макет должен быть максимально компактным, а объекты имеют схожую форму (например, цилиндры или сферы).
|
||||
|
||||
### 4. Боковой конвейер (Cross-Belt / Diverter)
|
||||
Это короткий перпендикулярный отрезок ленты, который включается только тогда, когда объект должен быть сброшен в зону.
|
||||
* **Плюсы:** Самый профессиональный и «мягкий» метод. Идеален для хрупких товаров [1].
|
||||
* **Минусы:** Высокая стоимость и сложность (нужно еще 3 маленьких мотора и дополнительные ленты).
|
||||
* **Когда использовать:** Если вы хотите создать максимально приближенный к реальному промышленному производству макет.
|
||||
|
||||
### Сводная таблица для выбора:
|
||||
|
||||
| Вариант | Бережность (для хрупких [1]) | Компактность (площадь [1]) | Сложность реализации | Работа с круглыми объектами [1] |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Серво-барьер** | Средняя | Высокая | Низкая | Хорошо |
|
||||
| **Соленоид** | Низкая | Очень высокая | Низкая | Средне |
|
||||
| **Платформа** | Высокая | Средняя | Средняя | Хорошо |
|
||||
| **Диск** | Средняя | Очень высокая | Средняя | Идеально |
|
||||
| **Боковой конвейер**| Очень высокая | Низкая | Высокая | Хорошо |
|
||||
|
||||
**Моя рекомендация:**
|
||||
Если вы хотите сохранить простоту управления через Arduino, но при этом учесть требования к бережности и компактности [1], я рекомендую остаться на **сервоприводах**, но изменить конструкцию барьера: вместо «толкателя» сделать **«поворотный желоб»** или **«шлюз»**. Это позволит объекту плавно скатываться в нужную зону, не подвергаясь резким ударам.
|
||||
Reference in New Issue
Block a user