From 739df2502b05687388ed121635c9e8b472c79ca9 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 22 Jan 2026 12:54:20 +0300 Subject: [PATCH] first commit --- README.md | 116 ++++++++++++++++++++++++++ src/main.c | 235 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 268 insertions(+), 83 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..578e5de --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# KgeuTraining + +Приложение для ESP32, отслеживающее положение груза с помощью датчиков Холла и передающее данные по TCP. + +## Описание + +Устройство создаёт точку доступа WiFi и TCP-сервер. При изменении положения груза (верх, низ, середина) информация отправляется всем подключённым клиентам в формате JSON. + +## Оборудование + +- **Плата**: Wemos D1 UNO32 (ESP32) +- **Датчики**: 2x NJK-5002C (датчики Холла, NPN, нормально разомкнутые) +- **Питание датчиков**: 5-30В DC + +## Схема подключения + +| Компонент | Пин ESP32 | +|-----------|-----------| +| Датчик верха (NJK-5002C) | GPIO 4 | +| Датчик низа (NJK-5002C) | GPIO 5 | + +### Подключение датчика NJK-5002C + +- **Коричневый провод**: +5В (или до 30В) +- **Синий провод**: GND +- **Чёрный провод**: Сигнал → GPIO ESP32 + +Датчик имеет выход с открытым коллектором (NPN). Внутренняя подтяжка ESP32 включена в коде. + +## Параметры WiFi + +- **SSID**: `KgeuTraining_AP` +- **Пароль**: `12345678` +- **IP адрес**: `192.168.4.1` +- **TCP порт**: `8080` + +## Формат данных + +Сервер отправляет JSON-сообщения при изменении положения: + +```json +{"position":"top"} +{"position":"middle"} +{"position":"bottom"} +``` + +## Установка и загрузка + +### Требования + +- [PlatformIO](https://platformio.org/) (расширение для VS Code или CLI) +- USB-кабель для подключения платы + +### Шаги + +1. **Клонируйте или скопируйте проект** + +2. **Откройте проект в PlatformIO** + + В VS Code: File → Open Folder → выберите папку `KgeuTraining` + +3. **Подключите плату** к компьютеру через USB + +4. **Соберите проект** + + Нажмите кнопку "Build" (галочка) на панели PlatformIO или выполните: + ```bash + pio run + ``` + +5. **Загрузите прошивку на плату** + + Нажмите кнопку "Upload" (стрелка) на панели PlatformIO или выполните: + ```bash + pio run --target upload + ``` + +6. **Откройте монитор порта** (опционально) + + Для просмотра логов нажмите "Serial Monitor" или выполните: + ```bash + pio device monitor + ``` + +### Возможные проблемы + +- **Порт не определяется**: Установите драйверы CH340 или CP2102 (в зависимости от платы) +- **Ошибка загрузки**: Попробуйте удерживать кнопку BOOT на плате во время загрузки +- **Не удаётся подключиться к WiFi**: Убедитесь, что плата запустилась (проверьте логи в мониторе) + +## Использование + +1. Включите устройство +2. Подключитесь к WiFi сети `KgeuTraining_AP` с паролем `12345678` +3. Подключитесь к TCP серверу: `192.168.4.1:8080` +4. Получайте данные о положении в реальном времени + +### Пример подключения (Python) + +```python +import socket + +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.connect(('192.168.4.1', 8080)) + +while True: + data = sock.recv(1024) + if data: + print(data.decode()) +``` + +### Пример подключения (netcat) + +```bash +nc 192.168.4.1 8080 +``` \ No newline at end of file diff --git a/src/main.c b/src/main.c index 0d4192a..8a87e3d 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,7 @@ #include #include #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" @@ -22,7 +23,7 @@ static const char *TAG = "KgeuTraining"; #define WIFI_CHANNEL 1 #define MAX_STA_CONN 4 -// Hall Sensor GPIO pins (KY-003) +// Hall Sensor GPIO pins (NJK-5002C) #define HALL_SENSOR_TOP_PIN GPIO_NUM_4 #define HALL_SENSOR_BOTTOM_PIN GPIO_NUM_5 @@ -49,116 +50,163 @@ static int tcp_server_socket = -1; // Mutex for client list access static SemaphoreHandle_t client_mutex = NULL; -// Forward declaration -static void send_position_to_client(int sock_fd, const char *position); - /** - * Add TCP client to the list + * Get current position as string (helper function) */ -static void add_tcp_client(int sock_fd) { - xSemaphoreTake(client_mutex, portMAX_DELAY); - for (int i = 0; i < MAX_CLIENTS; i++) { - if (tcp_clients[i] == -1) { - tcp_clients[i] = sock_fd; - tcp_client_count++; - ESP_LOGI(TAG, "TCP client connected, total: %d", tcp_client_count); - - // Send current position immediately - const char *current_pos_str; - switch (current_position) { - case POSITION_TOP: - current_pos_str = "top"; - break; - case POSITION_BOTTOM: - current_pos_str = "bottom"; - break; - case POSITION_MIDDLE: - current_pos_str = "middle"; - break; - default: - current_pos_str = "middle"; - break; - } - send_position_to_client(sock_fd, current_pos_str); - - xSemaphoreGive(client_mutex); - return; - } +static const char* get_position_string(position_state_t pos) { + switch (pos) { + case POSITION_TOP: + return "top"; + case POSITION_BOTTOM: + return "bottom"; + case POSITION_MIDDLE: + default: + return "middle"; } - ESP_LOGW(TAG, "Maximum clients reached"); - xSemaphoreGive(client_mutex); } /** - * Remove TCP client from the list + * Remove TCP client from the list (must be called with mutex held) + * Returns true if client was found and removed */ -static void remove_tcp_client(int sock_fd) { - xSemaphoreTake(client_mutex, portMAX_DELAY); +static bool remove_tcp_client_locked(int sock_fd) { for (int i = 0; i < MAX_CLIENTS; i++) { if (tcp_clients[i] == sock_fd) { tcp_clients[i] = -1; tcp_client_count--; ESP_LOGI(TAG, "TCP client disconnected, total: %d", tcp_client_count); - xSemaphoreGive(client_mutex); - return; + return true; } } + return false; +} + +/** + * Remove TCP client from the list (thread-safe wrapper) + */ +static void remove_tcp_client(int sock_fd) { + xSemaphoreTake(client_mutex, portMAX_DELAY); + remove_tcp_client_locked(sock_fd); xSemaphoreGive(client_mutex); } /** - * Send position to a specific TCP client + * Add TCP client to the list and send current position + * Returns true if client was added successfully */ -static void send_position_to_client(int sock_fd, const char *position) { - // Simple JSON formatting: {"position": "top"} - char json_string[64]; - snprintf(json_string, sizeof(json_string), "{\"position\":\"%s\"}\n", position); +static bool add_tcp_client(int sock_fd) { + bool added = false; - ESP_LOGI(TAG, "Sending to client %d: %s", sock_fd, json_string); + xSemaphoreTake(client_mutex, portMAX_DELAY); + for (int i = 0; i < MAX_CLIENTS; i++) { + if (tcp_clients[i] == -1) { + tcp_clients[i] = sock_fd; + tcp_client_count++; + added = true; + ESP_LOGI(TAG, "TCP client connected, total: %d", tcp_client_count); + break; + } + } + xSemaphoreGive(client_mutex); + + if (!added) { + ESP_LOGW(TAG, "Maximum clients reached, rejecting connection"); + close(sock_fd); + return false; + } + + // Send current position (outside of mutex to avoid deadlock) + char json_string[64]; + snprintf(json_string, sizeof(json_string), "{\"position\":\"%s\"}\n", + get_position_string(current_position)); + + ESP_LOGI(TAG, "Sending initial position to client %d: %s", sock_fd, json_string); int len = strlen(json_string); int sent = send(sock_fd, json_string, len, 0); if (sent < 0) { - ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + ESP_LOGE(TAG, "Error sending initial position: errno %d", errno); remove_tcp_client(sock_fd); close(sock_fd); - } else if (sent != len) { - ESP_LOGW(TAG, "Sent %d bytes instead of %d", sent, len); + return false; } + + return true; } /** * Broadcast JSON message to all connected TCP clients */ static void broadcast_position(const char *position) { - if (tcp_client_count == 0) { - return; // No clients connected - } - // Simple JSON formatting: {"position": "top"} char json_string[64]; snprintf(json_string, sizeof(json_string), "{\"position\":\"%s\"}\n", position); - ESP_LOGI(TAG, "Broadcasting: %s", json_string); + // Collect clients to send to and failed clients + int clients_to_send[MAX_CLIENTS]; + int failed_clients[MAX_CLIENTS]; + int send_count = 0; + int fail_count = 0; + // Copy client list under mutex xSemaphoreTake(client_mutex, portMAX_DELAY); for (int i = 0; i < MAX_CLIENTS; i++) { if (tcp_clients[i] != -1) { - int sock_fd = tcp_clients[i]; - int len = strlen(json_string); - int sent = send(sock_fd, json_string, len, 0); - - if (sent < 0) { - ESP_LOGE(TAG, "Error sending to client %d: errno %d", sock_fd, errno); - remove_tcp_client(sock_fd); - close(sock_fd); - } else if (sent != len) { - ESP_LOGW(TAG, "Sent %d bytes instead of %d to client %d", sent, len, sock_fd); - } + clients_to_send[send_count++] = tcp_clients[i]; } } xSemaphoreGive(client_mutex); + + if (send_count == 0) { + return; // No clients connected + } + + ESP_LOGI(TAG, "Broadcasting: %s", json_string); + + // Send to all clients (outside of mutex) + int len = strlen(json_string); + for (int i = 0; i < send_count; i++) { + int sock_fd = clients_to_send[i]; + int sent = send(sock_fd, json_string, len, 0); + + if (sent < 0) { + ESP_LOGE(TAG, "Error sending to client %d: errno %d", sock_fd, errno); + failed_clients[fail_count++] = sock_fd; + } else if (sent != len) { + ESP_LOGW(TAG, "Sent %d bytes instead of %d to client %d", sent, len, sock_fd); + } + } + + // Remove failed clients under mutex + if (fail_count > 0) { + xSemaphoreTake(client_mutex, portMAX_DELAY); + for (int i = 0; i < fail_count; i++) { + remove_tcp_client_locked(failed_clients[i]); + } + xSemaphoreGive(client_mutex); + + // Close sockets outside of mutex + for (int i = 0; i < fail_count; i++) { + close(failed_clients[i]); + } + } +} + +/** + * Check if socket is still in client list (thread-safe) + */ +static bool is_client_connected(int sock_fd) { + bool connected = false; + xSemaphoreTake(client_mutex, portMAX_DELAY); + for (int i = 0; i < MAX_CLIENTS; i++) { + if (tcp_clients[i] == sock_fd) { + connected = true; + break; + } + } + xSemaphoreGive(client_mutex); + return connected; } /** @@ -177,6 +225,14 @@ static void tcp_client_task(void *pvParameters) { setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); while (1) { + // Check if we've been removed by broadcast_position due to send failure + if (!is_client_connected(sock_fd)) { + ESP_LOGI(TAG, "Client %d was removed, exiting task", sock_fd); + // Socket already closed by whoever removed us + vTaskDelete(NULL); + return; + } + int len = recv(sock_fd, rx_buffer, sizeof(rx_buffer) - 1, 0); if (len < 0) { @@ -199,9 +255,18 @@ static void tcp_client_task(void *pvParameters) { // For now, we just log it } - ESP_LOGI(TAG, "Closing client socket %d", sock_fd); - remove_tcp_client(sock_fd); - close(sock_fd); + // Only close if we're still in the list (not already closed by broadcast) + xSemaphoreTake(client_mutex, portMAX_DELAY); + bool was_connected = remove_tcp_client_locked(sock_fd); + xSemaphoreGive(client_mutex); + + if (was_connected) { + ESP_LOGI(TAG, "Closing client socket %d", sock_fd); + close(sock_fd); + } else { + ESP_LOGI(TAG, "Client socket %d already closed", sock_fd); + } + vTaskDelete(NULL); } @@ -258,11 +323,12 @@ static void tcp_server_task(void *pvParameters) { ESP_LOGI(TAG, "New client connected from %s", inet_ntoa(client_addr.sin_addr)); - // Add client to list - add_tcp_client(client_sock); - - // Create task for this client - xTaskCreate(tcp_client_task, "tcp_client", 4096, (void*)(intptr_t)client_sock, 5, NULL); + // Add client to list and send initial position + if (add_tcp_client(client_sock)) { + // Create task for this client only if successfully added + xTaskCreate(tcp_client_task, "tcp_client", 4096, (void*)(intptr_t)client_sock, 5, NULL); + } + // If add_tcp_client failed, it already closed the socket } close(tcp_server_socket); @@ -324,7 +390,8 @@ static void wifi_init_softap(void) { } /** - * Initialize GPIO for hall sensors + * Initialize GPIO for hall sensors (NJK-5002C) + * NJK-5002C is an NPN open-collector sensor, requires pull-up resistor */ static void init_hall_sensors(void) { gpio_config_t io_conf = { @@ -332,25 +399,27 @@ static void init_hall_sensors(void) { .mode = GPIO_MODE_INPUT, .pin_bit_mask = (1ULL << HALL_SENSOR_TOP_PIN) | (1ULL << HALL_SENSOR_BOTTOM_PIN), .pull_down_en = GPIO_PULLDOWN_DISABLE, - .pull_up_en = GPIO_PULLUP_ENABLE // KY-003 has pull-up resistor, enable internal pull-up + .pull_up_en = GPIO_PULLUP_ENABLE // NJK-5002C is open-collector, requires pull-up }; gpio_config(&io_conf); - ESP_LOGI(TAG, "Hall sensors initialized on GPIO %d (top) and %d (bottom)", + ESP_LOGI(TAG, "NJK-5002C hall sensors initialized on GPIO %d (top) and %d (bottom)", HALL_SENSOR_TOP_PIN, HALL_SENSOR_BOTTOM_PIN); } /** - * Read position from hall sensors + * Read position from hall sensors (NJK-5002C) + * NJK-5002C: LOW (0) when magnet is detected, HIGH (1) when no magnet + * Open-collector output pulls to ground when activated */ static position_state_t read_position(void) { int top_sensor = gpio_get_level(HALL_SENSOR_TOP_PIN); int bottom_sensor = gpio_get_level(HALL_SENSOR_BOTTOM_PIN); - // KY-003: LOW when magnet is near, HIGH when no magnet - // Top sensor LOW = weight at top - // Bottom sensor LOW = weight at bottom - // Both HIGH = weight in middle + // NJK-5002C: LOW when magnet is detected (sensor activated), HIGH when no magnet + // Top sensor LOW = weight at top (magnet detected) + // Bottom sensor LOW = weight at bottom (magnet detected) + // Both HIGH = weight in middle (no magnet detected) if (top_sensor == 0) { return POSITION_TOP; @@ -393,7 +462,7 @@ static void position_monitor_task(void *pvParameters) { } // Check every 100ms - vTaskDelay(pdMS_TO_TICKS(100)); + vTaskDelay(pdMS_TO_TICKS(5)); } }