first commit
This commit is contained in:
116
README.md
Normal file
116
README.md
Normal 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
|
||||
```
|
||||
235
src/main.c
235
src/main.c
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user