first commit

This commit is contained in:
2026-01-22 12:54:20 +03:00
parent ad40e245b7
commit 739df2502b
2 changed files with 268 additions and 83 deletions

116
README.md Normal file
View File

@@ -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
```

View File

@@ -1,6 +1,7 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#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));
}
}