From ca7a53ee1cb3866fd1ffacedd00035a031be61e5 Mon Sep 17 00:00:00 2001 From: Edward Emelianov Date: Fri, 17 Oct 2025 00:32:56 +0300 Subject: [PATCH] ESP simplest server --- ESP8266/CMakeLists.txt | 34 ++++++++ ESP8266/Readme | 2 + ESP8266/esp8266.c | 170 ++++++++++++++++++++++++++++++++++++++ ESP8266/esp8266.h | 53 ++++++++++++ ESP8266/main.c | 182 +++++++++++++++++++++++++++++++++++++++++ ESP8266/serial.c | 178 ++++++++++++++++++++++++++++++++++++++++ ESP8266/serial.h | 40 +++++++++ 7 files changed, 659 insertions(+) create mode 100644 ESP8266/CMakeLists.txt create mode 100644 ESP8266/Readme create mode 100644 ESP8266/esp8266.c create mode 100644 ESP8266/esp8266.h create mode 100644 ESP8266/main.c create mode 100644 ESP8266/serial.c create mode 100644 ESP8266/serial.h diff --git a/ESP8266/CMakeLists.txt b/ESP8266/CMakeLists.txt new file mode 100644 index 0000000..ce24972 --- /dev/null +++ b/ESP8266/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) +set(PROJ esp8266) + +project(${PROJ} LANGUAGES C) + +set(CMAKE_COLOR_MAKEFILE ON) + +aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} SOURCES) + +option(DEBUG "Compile in debug mode" OFF) + +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} -Og -g3 -ggdb -Werror") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} -O3 -march=native -fdata-sections -ffunction-sections -flto=auto") + +if(DEBUG) + add_definitions(-DEBUG) + set(CMAKE_BUILD_TYPE DEBUG) + set(CMAKE_VERBOSE_MAKEFILE "ON") +else() + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections -flto=auto") + set(CMAKE_BUILD_TYPE RELEASE) +endif() + + +find_package(PkgConfig REQUIRED) +pkg_check_modules(${PROJ} REQUIRED usefull_macros) +add_executable(esp8266 ${SOURCES}) +target_link_libraries(${PROJ} ${${PROJ}_LIBRARIES} -lm) + +include(GNUInstallDirs) +install(TARGETS ${PROJ} +# LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/ESP8266/Readme b/ESP8266/Readme new file mode 100644 index 0000000..deeb812 --- /dev/null +++ b/ESP8266/Readme @@ -0,0 +1,2 @@ +Simplest server based on AT-commands using ESP8266. +I tried to make code as nearest to MCU as possible. diff --git a/ESP8266/esp8266.c b/ESP8266/esp8266.c new file mode 100644 index 0000000..da2d45b --- /dev/null +++ b/ESP8266/esp8266.c @@ -0,0 +1,170 @@ +/* + * This file is part of the esp8266 project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "esp8266.h" +#include "serial.h" + +#define ESPMSG(x) do{if(!serial_send_msg(x)) return FALSE;}while(0) + +// sometimes ESP hangs with message "WIFI GOT IP" and I can do nothing except waiting + +const char *msgptr = NULL; // pointer to received message +static int receivedlen = 0; + +// check module working +int esp_check(){ + // try simplest check for three times + for(int i = 0; i < 3; ++i) if( ANS_OK == serial_sendwans("AT")) break; + //esp_close(); + // now try next even if no answer for "AT" + if( ANS_OK == serial_sendwans("ATE0") // echo off + && ANS_OK == serial_sendwans("AT+CWMODE_CUR=1") // station mode + ) return TRUE; + return FALSE; +} + +// check established wifi connection +esp_cipstatus_t esp_cipstatus(){ + if(ANS_OK != serial_sendwans("AT+CIPSTATUS")) return ESP_CIP_FAILED; + char *l = serial_getline(NULL, NULL); + const char *n = NULL; + if(!l || !(n = serial_tidx(l, "STATUS:"))) return ESP_CIP_FAILED; + return (esp_cipstatus_t)serial_s2i(n); +} + +// connect to AP +int esp_connect(const char *SSID, const char *pass){ + ESPMSG("AT+CWJAP_CUR=\""); + ESPMSG(SSID); + ESPMSG("\",\""); + ESPMSG(pass); + if(ANS_OK != serial_sendwans("\"")) return FALSE; + return TRUE; +} + +// just send AT+CIFSR +int esp_myip(){ + return (ANS_OK == serial_sendwans("AT+CIFSR")); +} + +// start server on given port +int esp_start_server(const char *port){ + if(ANS_OK != serial_sendwans("AT+CIPMUX=1")){ // can't start server without mux + // what if already ready? + char *x = serial_getline(NULL, NULL); + if(!x || !serial_tidx(x, "link is builded")) return FALSE; + } + ESPMSG("AT+CIPSERVER=1,"); + if(ANS_OK != serial_sendwans(port)) return FALSE; + return TRUE; +} + +// stop server +int ep_stop_server(){ + return (ANS_OK == serial_sendwans("AT+CIPSERVER=0")); +} + +// next (or only) line of received data +const char *esp_msgline(){ + DBG("receivedlen=%d", receivedlen); + if(msgptr){ + const char *p = msgptr; + msgptr = NULL; + return p; + } + if(receivedlen < 1) return NULL; + int l, d; + const char *got = serial_getline(&l, &d); + receivedlen -= l + d; + return got; +} + +// process connection/disconnection/messages +// fd - file descriptor of opened/closed connections +esp_clientstat_t esp_process(int *fd){ + msgptr = NULL; + receivedlen = 0; + int l, d; + char *got = serial_getline(&l, &d); + if(!got) return ESP_CLT_IDLE; + const char *x = serial_tidx(got, "+IPD,"); + if(x){ + if(fd) *fd = serial_s2i(x); + x = serial_tidx(x, ","); + if(!x) return ESP_CLT_ERROR; + int r = serial_s2i(x); + x = serial_tidx(x, ":"); + if(!x) return ESP_CLT_ERROR; + receivedlen = r - d - (l - (x-got)); // this is a rest of data (if any) + msgptr = x; + return ESP_CLT_GETMESSAGE; + } + // check for CONNECT/CLOSE + if((x = serial_tidx(got, ",CONNECT"))){ + if(fd) *fd = serial_s2i(got); + return ESP_CLT_CONNECTED; + } + if((x = serial_tidx(got, ",CLOSED"))){ + if(fd) *fd = serial_s2i(got); + return ESP_CLT_DISCONNECTED; + } + DBG("Unknown message: '%s'", got); + return ESP_CLT_IDLE; +} + +int esp_send(int fd, const char *msg){ + DBG("send '%s' to %d", msg, fd); + ESPMSG("AT+CIPSENDEX="); + if(!serial_putchar('0' + fd)) return FALSE; + if(ANS_OK != serial_sendwans(",2048")) return FALSE; + int got = 0; + // try several times + for(int i = 0; i < 10; ++i){ + got = serial_getch(); + if(got == '>') break; + } + if(got != '>'){ + DBG("Didn't found '>'"); + serial_send_msg("\\0"); // terminate message + serial_clr(); + return FALSE; // go into terminal mode + } + serial_clr(); // remove space after '>' + ESPMSG(msg); + if(ANS_OK == serial_sendwans("\\0")) return TRUE; + DBG("Didn't sent"); + return FALSE; +} + +void esp_reset(){ + serial_sendwans("AT+RST"); +} + +void esp_close(){ + serial_sendwans("AT+CIPMUX=0"); + serial_sendwans("AT+CIPCLOSE"); +} + + +int esp_listAP(){ + if(ANS_OK == serial_sendwans("AT+CWLAP")) return TRUE; + return FALSE; +} diff --git a/ESP8266/esp8266.h b/ESP8266/esp8266.h new file mode 100644 index 0000000..889eb0d --- /dev/null +++ b/ESP8266/esp8266.h @@ -0,0 +1,53 @@ +/* + * This file is part of the esp8266 project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +// really only 5 clients allowed +#define ESP_MAX_CLT_NUMBER 8 + +typedef enum{ + ESP_CIP_0, + ESP_CIP_1, + ESP_CIP_GOTIP, + ESP_CIP_CONNECTED, + ESP_CIP_DISCONNECTED, + ESP_CIP_FAILED +} esp_cipstatus_t; + +typedef enum{ + ESP_CLT_IDLE, // nothing happened + ESP_CLT_CONNECTED, // new client connected + ESP_CLT_DISCONNECTED, // disconnected + ESP_CLT_ERROR, // error writing or other + ESP_CLT_GETMESSAGE, // receive message from client + ESP_CLT_OK, // sent OK +} esp_clientstat_t; + +int esp_check(); +void esp_close(); +void esp_reset(); +esp_cipstatus_t esp_cipstatus(); +int esp_connect(const char *SSID, const char *pass); +int esp_myip(); +int esp_start_server(const char *port); +int ep_stop_server(); +esp_clientstat_t esp_process(int *fd); +int esp_send(int fd, const char *msg); +const char *esp_msgline(); +int esp_listAP(); diff --git a/ESP8266/main.c b/ESP8266/main.c new file mode 100644 index 0000000..c494f93 --- /dev/null +++ b/ESP8266/main.c @@ -0,0 +1,182 @@ +/* + * This file is part of the esp8266 project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "esp8266.h" +#include "serial.h" + +static struct{ + int help; + char *serialdev; + char *SSID; + char *SSpass; + int speed; + char *port; + int reset; + int list; +} G = { + .speed = 115200, + .serialdev = "/dev/ttyUSB0", + .port = "1111", +}; + +static sl_option_t options[] = { + {"help", NO_ARGS, NULL, 'h', arg_int, APTR(&G.help), "show this help"}, + {"device", NEED_ARG, NULL, 'd', arg_string, APTR(&G.serialdev), "serial device path (default: /dev/ttyUSB0)"}, + {"baudrate",NEED_ARG, NULL, 'b', arg_int, APTR(&G.speed), "serial speed"}, + {"ssid", NEED_ARG, NULL, 0, arg_string, APTR(&G.SSID), "SSID to connect"}, + {"pass", NEED_ARG, NULL, 0, arg_string, APTR(&G.SSpass), "SSID password"}, + {"port", NEED_ARG, NULL, 'p', arg_string, APTR(&G.port), "servr port (default: 1111)"}, + {"reset", NO_ARGS, NULL, 0, arg_int, APTR(&G.reset), "reset ESP"}, + {"list", NO_ARGS, NULL, 'l', arg_int, APTR(&G.list), "list available APs"}, + end_option +}; + +void signals(int signo){ + esp_close(); + serial_close(); + exit(signo); +} + +static esp_clientstat_t parse_message(int fd){ + // this isn't a good idea to send data while not received all; so we use buffer + static sl_ringbuffer_t *rb = NULL; + if(!rb) rb = sl_RB_new(4096); + const char *msg = NULL; + char buf[4096]; + while((msg = esp_msgline())){ + printf("Received line from fd %d: %s\n", fd, msg); + // do something with this data + if(0 == strcmp(msg, "help")){ + sl_RB_writestr(rb, "Hey, we don't have any help yet, try `time`\n"); + }else if(0 == strcmp(msg, "time")){ + snprintf(buf, 64, "TIME=%.3f\n", sl_dtime()); + sl_RB_writestr(rb, buf); + }else{ + if(*msg){ + snprintf(buf, 127, "Part of your message: _%s_\n", msg); + sl_RB_writestr(rb, buf); + } + } + } + size_t L = 4094; + char *b = buf; + while(sl_RB_readline(rb, b, L) > 0){ // there's a bug in my library! Need to fix (sl_RB_readline returns NOT an amount of bytes) + size_t got = strlen(b); + L -= ++got; + b += got; + b[-1] = '\n'; + } + if(L == 4094) return ESP_CLT_OK; // nothing to send + return esp_send(fd, buf) ? ESP_CLT_OK : ESP_CLT_ERROR; +} + +static void processing(){ + uint8_t connected[ESP_MAX_CLT_NUMBER] = {0}; + esp_clientstat_t oldstat[ESP_MAX_CLT_NUMBER] = {0}; + double T0 = sl_dtime(); + int N = -1; + void chkerr(){ + if(oldstat[N] == ESP_CLT_ERROR){ + DBG("error again -> turn off"); + connected[N] = 0; + } + } + while(1){ + esp_clientstat_t s = esp_process(&N); + if(N > -1 && N < ESP_MAX_CLT_NUMBER){ // parsing + //if(s == ESP_CLT_IDLE){ usleep(1000); continue; } + switch(s){ + case ESP_CLT_CONNECTED: + connected[N] = 1; + green("Connection on fd=%d\n", N); + break; + case ESP_CLT_DISCONNECTED: + connected[N] = 0; + green("fd=%d disconnected\n", N); + break; + case ESP_CLT_ERROR: + DBG("Error from %d", N); + chkerr(); + break; + case ESP_CLT_GETMESSAGE: + DBG("%d have message", N); + s = parse_message(N); + if(s == ESP_CLT_ERROR) chkerr(); + break; + default: break; + } + oldstat[N] = s; + } + // and here we can do something for all + int R = rand() % 5000; + // example of `broadcast` message + if(R < 2){ + DBG("Send 'broadcasting' message"); + char buf[64]; + snprintf(buf, 63, "Hello, there's %.2f seconds from start\n", sl_dtime() - T0); + for(int i = 0; i < ESP_MAX_CLT_NUMBER; ++i){ + if(!connected[i]) continue; + if(ESP_CLT_ERROR == esp_send(i, buf)) chkerr(); + } + } + usleep(1000); + } +} + +int main(int argc, char **argv){ + sl_init(); + sl_parseargs(&argc, &argv, options); + if(G.help) sl_showhelp(-1, options); + if(!serial_init(G.serialdev, G.speed)) ERRX("Can't open %s at speed %d", G.serialdev, G.speed); + if(G.reset){ + red("Resetting, please wait!\n"); + esp_reset(); + usleep(500000); + DBG("Get buff"); + char *str; + while((str = serial_getline(NULL, NULL))) if(*str) printf("\t%s\n", str); + signals(0); + } + if(!esp_check()) ERRX("No answer from ESP"); + if(G.list){ + if(esp_listAP()){ + green("Available wifi:\n"); + char *str; + while(str = serial_getline(NULL, NULL)) printf("\t%s\n", str); + }else WARNX("Error listing"); + } + esp_cipstatus_t st = esp_cipstatus(); + if(st != ESP_CIP_GOTIP && st != ESP_CIP_CONNECTED){ + DBG("Need to connect"); + if(!esp_connect(G.SSID, G.SSpass)) ERRX("Can't connect"); + } + if(esp_myip()){ + green("Connected\n"); + char *str; + while(str = serial_getline(NULL, NULL)) if(*str) printf("\t%s\n", str); + } + ep_stop_server(); // stop just in case + if(!esp_start_server(G.port)) ERRX("Can't start server"); + processing(); + serial_close(); + return 0; +} diff --git a/ESP8266/serial.c b/ESP8266/serial.c new file mode 100644 index 0000000..4c5a3f8 --- /dev/null +++ b/ESP8266/serial.c @@ -0,0 +1,178 @@ +/* + * This file is part of the esp8266 project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "serial.h" + +// ALL functions here aren't thread-independent, as you can't use the same line simultaneously + +static sl_tty_t *device = NULL; +static double timeout = 30.; // timeout, s +static sl_ringbuffer_t *rbin = NULL; // input ring buffer + +// read all incoming +void serial_clr(){ + if(!device) return; + while(sl_tty_read(device) > 0); +} + +int serial_init(char *path, int speed){ + device = sl_tty_new(path, speed, 256); + if(!device) return FALSE; + device = sl_tty_open(device, 1); + if(!device) return FALSE; + rbin = sl_RB_new(4096); + if(!rbin){ + sl_tty_close(&device); + return FALSE; + } + sl_tty_tmout(1000); // set select() timeout to 1ms + // clear buffer + serial_clr(); + return TRUE; +} + +void serial_close(){ + if(device) sl_tty_close(&device); + if(rbin) sl_RB_delete(&rbin); +} + +int serial_set_timeout(double tms){ + if(tms < 0.1) return FALSE; + timeout = tms; + return TRUE; +} + +// send messages over serial, +// without EOL: +int serial_send_msg(const char *msg){ + if(!msg || !device) return FALSE; + int l = strlen(msg); + DBG("Write message `%s` (%d bytes)", msg, l); + if(sl_tty_write(device->comfd, msg, l)) return FALSE; + return TRUE; +} +// and with: +int serial_send_cmd(const char *msg){ + if(!msg || !device) return FALSE; + if(!serial_send_msg(msg)) return FALSE; + DBG("Write EOL"); + if(sl_tty_write(device->comfd, "\r\n", 2)) return FALSE; + return TRUE; +} +int serial_putchar(char ch){ + if(!device) return FALSE; + if(sl_tty_write(device->comfd, &ch, 1)) return FALSE; + return TRUE; +} + +static void fillinbuff(){ + ssize_t got; + while((got = sl_tty_read(device))){ + if(got < 0){ + WARNX("Serial device disconnected!"); + serial_close(); + }else if(got){ + if((size_t)got != sl_RB_write(rbin, (const uint8_t*)device->buf, got)){ + WARNX("Rinbguffer overflow?"); + sl_RB_clearbuf(rbin); + return; + } + } + } +} + +// read one string line from serial +// @arg deleted - N symbols deleted from rest of string (1 in case of '\n' and 2 in case of "\r\n") +// @arg len - strlen of data +char *serial_getline(int *len, int *deleted){ + if(!device) return NULL; + static char buf[BUFSIZ]; + fillinbuff(); + // read old records + if(!sl_RB_readline(rbin, buf, BUFSIZ-1)) return NULL; + // remove trailing '\r' + int l = strlen(buf), d = 1; + if(l > -1 && buf[l - 1] == '\r'){ + ++d; + buf[--l] = 0; + } + if(deleted) *deleted = d; + if(len) *len = l; + DBG("read: '%s'", buf); + return buf; +} + +// get symbol +int serial_getch(){ + if(!device) return -1; + fillinbuff(); + char C; + DBG("rb size: %zd", sl_RB_datalen(rbin)); + size_t rd = sl_RB_read(rbin, (uint8_t*)&C, 1); + DBG("got %zd : '%c'", rd, C); + if(rd != 1) return -1; + //if(1 != sl_RB_read(rbin, (uint8_t*)&C, 1)) return -1; + return (int) C; +} + +serial_ans_t serial_sendwans(const char *msg){ + if(!msg || !device) return ANS_FAILED; + if(!serial_send_cmd(msg)) return ANS_FAILED; + double t0 = sl_dtime(); + int ret = ANS_FAILED; + while(sl_dtime() - t0 < timeout && device){ + char *ans = NULL; + if(!(ans = serial_getline(NULL, NULL))){ usleep(500); continue; } + DBG("Get line: '%s' (%zd bytes)", ans, strlen(ans)); + if(!*ans) continue; // empty string + if(strcmp(ans, "OK") == 0 || strcmp(ans, "SEND OK") == 0){ ret = ANS_OK; goto rtn; } + if(strcmp(ans, "ERROR") == 0){ ret = ANS_ERR; goto rtn; } + if(strcmp(ans, "FAIL") == 0){ ret = ANS_FAILED; goto rtn; } + DBG("Return '%s' into buff", ans); + sl_RB_writestr(rbin, ans); // put other data into ringbuffer for further processing + sl_RB_putbyte(rbin, '\n'); + } +rtn: + DBG("proc time: %g", sl_dtime() - t0); + return ret; +} + +// return NULL if `s` don't contain `t`, else return next symbol in `s` +const char *serial_tidx(const char *s, const char *t){ + if(!s) return NULL; + DBG("check '%s' for '%s'", s, t); + int pos = 0; + if(t){ + const char *sub = strstr(s, t); + if(!sub) return NULL; + int l = strlen(t); + pos = (sub - s) + l; + } + DBG("pos = %d", pos); + return s + pos; +} + +int serial_s2i(const char *s){ + if(!s) return -1; + DBG("conv '%s' to %d", s, atoi(s)); + return atoi(s); +} diff --git a/ESP8266/serial.h b/ESP8266/serial.h new file mode 100644 index 0000000..754db84 --- /dev/null +++ b/ESP8266/serial.h @@ -0,0 +1,40 @@ +/* + * This file is part of the esp8266 project. + * Copyright 2025 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +typedef enum{ + ANS_FAILED, + ANS_OK, + ANS_ERR +} serial_ans_t; + +void serial_clr(); +int serial_set_timeout(double tms); +int serial_init(char *path, int speed); +void serial_close(); +int serial_send_msg(const char *msg); +int serial_send_cmd(const char *msg); +int serial_putchar(char ch); +serial_ans_t serial_sendwans(const char *msg); +char *serial_getline(int *len, int *deleted); +int serial_getch(); + +// conversion functions for ESP +const char *serial_tidx(const char *s, const char *t); +int serial_s2i(const char *s);