From c2e87940f2e4e002eb25411ae916760c545c6600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Fri, 10 Dec 2021 00:54:25 +0100 Subject: [PATCH] Initial template version based of cspemu --- .gitignore | 4 + CMakeLists.txt | 6 + Makefile | 8 + components/common_utils/CMakeLists.txt | 8 + components/common_utils/README.txt | 2 + components/common_utils/component.mk | 3 + .../include/common_utils/base16.h | 75 + .../include/common_utils/datetime.h | 131 + .../include/common_utils/hexdump.h | 19 + .../common_utils/include/common_utils/utils.h | 80 + components/common_utils/src/base16.c | 62 + components/common_utils/src/common_utils.c | 52 + components/common_utils/src/datetime.c | 110 + components/common_utils/src/hexdump.c | 72 + components/dhcp_wd/CMakeLists.txt | 9 + components/dhcp_wd/Kconfig | 27 + components/dhcp_wd/README.txt | 5 + components/dhcp_wd/component.mk | 3 + components/dhcp_wd/include/dhcp_wd.h | 82 + components/dhcp_wd/src/dhcp_wd.c | 277 + components/fileserver/CMakeLists.txt | 16 + components/fileserver/README.txt | 2 + components/fileserver/component.mk | 3 + components/fileserver/files/embed/favicon.ico | Bin 0 -> 2238 bytes components/fileserver/files/embed/index.html | 106 + .../fileserver/files/rebuild_file_tables.php | 163 + components/fileserver/files/www_files_enum.c | 15 + components/fileserver/files/www_files_enum.h | 13 + .../include/fileserver/embedded_files.h | 51 + .../include/fileserver/token_subs.h | 216 + components/fileserver/readme/README.md | 29 + .../fileserver/readme/rebuild_file_tables.php | 170 + components/fileserver/src/embedded_files.c | 22 + components/fileserver/src/token_subs.c | 580 ++ components/httpd_utils/CMakeLists.txt | 9 + components/httpd_utils/README.txt | 4 + components/httpd_utils/component.mk | 3 + .../httpd_utils/include/httpd_utils/captive.h | 32 + .../include/httpd_utils/fd_to_ipv4.h | 13 + .../include/httpd_utils/redirect.h | 16 + .../httpd_utils/include/httpd_utils/session.h | 55 + .../include/httpd_utils/session_kvmap.h | 77 + .../include/httpd_utils/session_store.h | 100 + components/httpd_utils/src/captive.c | 106 + components/httpd_utils/src/fd_to_ipv4.c | 42 + components/httpd_utils/src/redirect.c | 20 + components/httpd_utils/src/session_kvmap.c | 181 + components/httpd_utils/src/session_store.c | 220 + components/httpd_utils/src/session_utils.c | 41 + components/ping/CMakeLists.txt | 4 + components/ping/README.txt | 1 + components/ping/component.mk | 3 + components/ping/include/ping.h | 58 + components/ping/src/ping.c | 262 + components/socket_server/CMakeLists.txt | 4 + components/socket_server/README.txt | 1 + components/socket_server/component.mk | 3 + .../socket_server/include/socket_server.h | 282 + components/socket_server/src/socket_server.c | 1015 ++++ components/vconsole/CMakeLists.txt | 18 + components/vconsole/libconsole/.gitignore | 54 + components/vconsole/libconsole/CMakeLists.txt | 73 + components/vconsole/libconsole/LICENSE.txt | 1 + .../libconsole/include/console/cmddef.h | 24 + .../libconsole/include/console/config.h.in | 22 + .../libconsole/include/console/console.h | 362 ++ .../libconsole/include/console/console_io.h | 196 + .../libconsole/include/console/prefix_match.h | 94 + .../libconsole/include/console/utils.h | 213 + .../libconsole/lib/argtable3/CMakeLists.txt | 9 + .../libconsole/lib/argtable3/README.txt | 3 + .../libconsole/lib/argtable3/argtable3.c | 4969 +++++++++++++++++ .../libconsole/lib/argtable3/argtable3.h | 306 + components/vconsole/libconsole/src/console.c | 1824 ++++++ .../vconsole/libconsole/src/console_filecap.c | 42 + .../vconsole/libconsole/src/console_io.c | 194 + .../libconsole/src/console_linenoise.c | 1129 ++++ .../libconsole/src/console_linenoise.h | 248 + .../libconsole/src/console_prefix_match.c | 196 + .../libconsole/src/console_split_argv.c | 120 + .../libconsole/src/console_split_argv.h | 36 + .../vconsole/libconsole/src/console_utils.c | 204 + components/vconsole/libconsole/src/queue.h | 645 +++ main/CMakeLists.txt | 46 + main/Kconfig.projbuild | 23 + main/app_main.c | 62 + main/application.h | 26 + main/component.mk | 4 + main/console/cmd_common.h | 48 + main/console/commands/cmd_dump.c | 34 + main/console/commands/cmd_factory_reset.c | 50 + main/console/commands/cmd_heap.c | 38 + main/console/commands/cmd_ip.c | 341 ++ main/console/commands/cmd_pw.c | 52 + main/console/commands/cmd_restart.c | 43 + main/console/commands/cmd_tasks.c | 51 + main/console/commands/cmd_version.c | 52 + main/console/commands/cmd_wifi.c | 360 ++ main/console/console_ioimpl.c | 418 ++ main/console/console_ioimpl.h | 77 + main/console/console_server.c | 155 + main/console/console_server.h | 29 + main/console/register_cmds.c | 29 + main/console/register_cmds.h | 6 + main/console/telnet_parser.c | 539 ++ main/console/telnet_parser.h | 47 + main/gitversion.h.in | 9 + main/settings.c | 160 + main/settings.h | 97 + main/shutdown_handlers.c | 31 + main/shutdown_handlers.h | 17 + main/sntp_cli.c | 63 + main/sntp_cli.h | 9 + main/tasks.h | 18 + main/utils.c | 71 + main/utils.h | 76 + main/web/websrv.c | 165 + main/web/websrv.h | 14 + main/wifi_conn.c | 190 + main/wifi_conn.h | 13 + sdkconfig | 1319 +++++ 121 files changed, 20737 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 components/common_utils/CMakeLists.txt create mode 100644 components/common_utils/README.txt create mode 100644 components/common_utils/component.mk create mode 100644 components/common_utils/include/common_utils/base16.h create mode 100644 components/common_utils/include/common_utils/datetime.h create mode 100644 components/common_utils/include/common_utils/hexdump.h create mode 100644 components/common_utils/include/common_utils/utils.h create mode 100644 components/common_utils/src/base16.c create mode 100644 components/common_utils/src/common_utils.c create mode 100644 components/common_utils/src/datetime.c create mode 100644 components/common_utils/src/hexdump.c create mode 100644 components/dhcp_wd/CMakeLists.txt create mode 100644 components/dhcp_wd/Kconfig create mode 100644 components/dhcp_wd/README.txt create mode 100644 components/dhcp_wd/component.mk create mode 100644 components/dhcp_wd/include/dhcp_wd.h create mode 100644 components/dhcp_wd/src/dhcp_wd.c create mode 100644 components/fileserver/CMakeLists.txt create mode 100644 components/fileserver/README.txt create mode 100644 components/fileserver/component.mk create mode 100644 components/fileserver/files/embed/favicon.ico create mode 100644 components/fileserver/files/embed/index.html create mode 100755 components/fileserver/files/rebuild_file_tables.php create mode 100644 components/fileserver/files/www_files_enum.c create mode 100644 components/fileserver/files/www_files_enum.h create mode 100644 components/fileserver/include/fileserver/embedded_files.h create mode 100644 components/fileserver/include/fileserver/token_subs.h create mode 100644 components/fileserver/readme/README.md create mode 100755 components/fileserver/readme/rebuild_file_tables.php create mode 100644 components/fileserver/src/embedded_files.c create mode 100644 components/fileserver/src/token_subs.c create mode 100644 components/httpd_utils/CMakeLists.txt create mode 100644 components/httpd_utils/README.txt create mode 100644 components/httpd_utils/component.mk create mode 100644 components/httpd_utils/include/httpd_utils/captive.h create mode 100644 components/httpd_utils/include/httpd_utils/fd_to_ipv4.h create mode 100644 components/httpd_utils/include/httpd_utils/redirect.h create mode 100644 components/httpd_utils/include/httpd_utils/session.h create mode 100644 components/httpd_utils/include/httpd_utils/session_kvmap.h create mode 100644 components/httpd_utils/include/httpd_utils/session_store.h create mode 100644 components/httpd_utils/src/captive.c create mode 100644 components/httpd_utils/src/fd_to_ipv4.c create mode 100644 components/httpd_utils/src/redirect.c create mode 100644 components/httpd_utils/src/session_kvmap.c create mode 100644 components/httpd_utils/src/session_store.c create mode 100644 components/httpd_utils/src/session_utils.c create mode 100644 components/ping/CMakeLists.txt create mode 100644 components/ping/README.txt create mode 100644 components/ping/component.mk create mode 100644 components/ping/include/ping.h create mode 100644 components/ping/src/ping.c create mode 100644 components/socket_server/CMakeLists.txt create mode 100644 components/socket_server/README.txt create mode 100644 components/socket_server/component.mk create mode 100644 components/socket_server/include/socket_server.h create mode 100644 components/socket_server/src/socket_server.c create mode 100644 components/vconsole/CMakeLists.txt create mode 100644 components/vconsole/libconsole/.gitignore create mode 100644 components/vconsole/libconsole/CMakeLists.txt create mode 100644 components/vconsole/libconsole/LICENSE.txt create mode 100644 components/vconsole/libconsole/include/console/cmddef.h create mode 100644 components/vconsole/libconsole/include/console/config.h.in create mode 100644 components/vconsole/libconsole/include/console/console.h create mode 100644 components/vconsole/libconsole/include/console/console_io.h create mode 100644 components/vconsole/libconsole/include/console/prefix_match.h create mode 100644 components/vconsole/libconsole/include/console/utils.h create mode 100644 components/vconsole/libconsole/lib/argtable3/CMakeLists.txt create mode 100644 components/vconsole/libconsole/lib/argtable3/README.txt create mode 100644 components/vconsole/libconsole/lib/argtable3/argtable3.c create mode 100644 components/vconsole/libconsole/lib/argtable3/argtable3.h create mode 100644 components/vconsole/libconsole/src/console.c create mode 100644 components/vconsole/libconsole/src/console_filecap.c create mode 100644 components/vconsole/libconsole/src/console_io.c create mode 100644 components/vconsole/libconsole/src/console_linenoise.c create mode 100644 components/vconsole/libconsole/src/console_linenoise.h create mode 100644 components/vconsole/libconsole/src/console_prefix_match.c create mode 100644 components/vconsole/libconsole/src/console_split_argv.c create mode 100644 components/vconsole/libconsole/src/console_split_argv.h create mode 100644 components/vconsole/libconsole/src/console_utils.c create mode 100644 components/vconsole/libconsole/src/queue.h create mode 100644 main/CMakeLists.txt create mode 100644 main/Kconfig.projbuild create mode 100644 main/app_main.c create mode 100644 main/application.h create mode 100644 main/component.mk create mode 100644 main/console/cmd_common.h create mode 100644 main/console/commands/cmd_dump.c create mode 100644 main/console/commands/cmd_factory_reset.c create mode 100644 main/console/commands/cmd_heap.c create mode 100644 main/console/commands/cmd_ip.c create mode 100644 main/console/commands/cmd_pw.c create mode 100644 main/console/commands/cmd_restart.c create mode 100644 main/console/commands/cmd_tasks.c create mode 100644 main/console/commands/cmd_version.c create mode 100644 main/console/commands/cmd_wifi.c create mode 100644 main/console/console_ioimpl.c create mode 100644 main/console/console_ioimpl.h create mode 100644 main/console/console_server.c create mode 100644 main/console/console_server.h create mode 100644 main/console/register_cmds.c create mode 100644 main/console/register_cmds.h create mode 100644 main/console/telnet_parser.c create mode 100644 main/console/telnet_parser.h create mode 100644 main/gitversion.h.in create mode 100644 main/settings.c create mode 100644 main/settings.h create mode 100644 main/shutdown_handlers.c create mode 100644 main/shutdown_handlers.h create mode 100644 main/sntp_cli.c create mode 100644 main/sntp_cli.h create mode 100644 main/tasks.h create mode 100644 main/utils.c create mode 100644 main/utils.h create mode 100644 main/web/websrv.c create mode 100644 main/web/websrv.h create mode 100644 main/wifi_conn.c create mode 100644 main/wifi_conn.h create mode 100644 sdkconfig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f39e7a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +build +cmake-build-* +*.old diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c0a9351 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(espnode) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5108515 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := hello-world + +include $(IDF_PATH)/make/project.mk diff --git a/components/common_utils/CMakeLists.txt b/components/common_utils/CMakeLists.txt new file mode 100644 index 0000000..065ee43 --- /dev/null +++ b/components/common_utils/CMakeLists.txt @@ -0,0 +1,8 @@ +set(COMPONENT_ADD_INCLUDEDIRS include) + +set(COMPONENT_SRCDIRS + "src") + +#set(COMPONENT_REQUIRES) + +register_component() diff --git a/components/common_utils/README.txt b/components/common_utils/README.txt new file mode 100644 index 0000000..48f10c0 --- /dev/null +++ b/components/common_utils/README.txt @@ -0,0 +1,2 @@ +General purpose, mostly platofrm-idependent utilities +that may be used by other components. diff --git a/components/common_utils/component.mk b/components/common_utils/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/common_utils/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/common_utils/include/common_utils/base16.h b/components/common_utils/include/common_utils/base16.h new file mode 100644 index 0000000..c3fc01d --- /dev/null +++ b/components/common_utils/include/common_utils/base16.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2010 Michael Brown . + * + * 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 2 of the + * License, or 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, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifndef BASE16_H_ +#define BASE16_H_ + +#include +#include + +/** + * Calculate length of base16-encoded data + * @param raw_len Raw data length + * @return Encoded string length (excluding NUL) + */ +static inline size_t base16_encoded_len(size_t raw_len) { + return (2 * raw_len); +} + +/** + * Calculate maximum length of base16-decoded string + * @param encoded Encoded string + * @return Maximum length of raw data + */ +static inline size_t base16_decoded_max_len(const char *encoded) { + return ((strlen(encoded) + 1) / 2); +} + +/** + * Base16-encode data + * + * The buffer must be the correct length for the encoded string. Use + * something like + * + * char buf[ base16_encoded_len ( len ) + 1 ]; + * + * (the +1 is for the terminating NUL) to provide a buffer of the + * correct size. + * + * @param raw Raw data + * @param len Length of raw data + * @param encoded Buffer for encoded string + */ +extern void base16_encode(uint8_t *raw, size_t len, char *encoded); + +/** + * Base16-decode data + * + * The buffer must be large enough to contain the decoded data. Use + * something like + * + * char buf[ base16_decoded_max_len ( encoded ) ]; + * + * to provide a buffer of the correct size. + * @param encoded Encoded string + * @param raw Raw data + * @return Length of raw data, or negative error + */ +extern int base16_decode(const char *encoded, uint8_t *raw); + +#endif /* BASE16_H_ */ diff --git a/components/common_utils/include/common_utils/datetime.h b/components/common_utils/include/common_utils/datetime.h new file mode 100644 index 0000000..814b779 --- /dev/null +++ b/components/common_utils/include/common_utils/datetime.h @@ -0,0 +1,131 @@ +/** + * TODO file description + * + * Created on 2019/09/13. + */ + +#ifndef CSPEMU_DATETIME_H +#define CSPEMU_DATETIME_H + +#include +#include + +enum weekday { + MONDAY = 1, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY, + SUNDAY +}; +_Static_assert(MONDAY==1, "enum weekday numbering Mon"); +_Static_assert(SUNDAY==7, "enum weekday numbering Sun"); + +enum month { + JANUARY = 1, + FEBRUARY, + MARCH, + APRIL, + MAY, + JUNE, + JULY, + AUGUST, + SEPTEMBER, + OCTOBER, + NOVEMBER, + DECEMBER +}; +_Static_assert(JANUARY==1, "enum month numbering Jan"); +_Static_assert(DECEMBER==12, "enum month numbering Dec"); + +/** Abbreviated weekday names */ +extern const char *DT_WKDAY_NAMES[]; +/** Full-length weekday names */ +extern const char *DT_WKDAY_NAMES_FULL[]; +/** Abbreviated month names */ +extern const char *DT_MONTH_NAMES[]; +/** Full-length month names */ +extern const char *DT_MONTH_NAMES_FULL[]; + +typedef struct datetime { + uint16_t year; + enum month month; + uint8_t day; + uint8_t hour; + uint8_t min; + uint8_t sec; + enum weekday wkday; // 1=monday +} datetime_t; + +// Templates for printf +#define DT_FORMAT_DATE "%d/%d/%d" +#define DT_SUBS_DATE(dt) (dt).year, (dt).month, (dt).day + +#define DT_FORMAT_TIME "%d:%02d:%02d" +#define DT_SUBS_TIME(dt) (dt).hour, (dt).min, (dt).sec + +#define DT_FORMAT_DATE_WK DT_FORMAT_WK " " DT_FORMAT_DATE +#define DT_SUBS_DATE_WK(dt) DT_SUBS_WK(dt), DT_SUBS_DATE(dt) + +#define DT_FORMAT_WK "%s" +#define DT_SUBS_WK(dt) DT_WKDAY_NAMES[(dt).wkday] + +#define DT_FORMAT_DATE_TIME DT_FORMAT_DATE " " DT_FORMAT_TIME +#define DT_SUBS_DATE_TIME(dt) DT_SUBS_DATE(dt), DT_SUBS_TIME(dt) + +#define DT_FORMAT DT_FORMAT_DATE_WK " " DT_FORMAT_TIME +#define DT_SUBS(dt) DT_SUBS_DATE_WK(dt), DT_SUBS_TIME(dt) + +// base century for two-digit year conversions +#define DT_CENTURY 2000 +// start year for weekday computation +#define DT_START_YEAR 2019 +// January 1st weekday of DT_START_YEAR +#define DT_START_WKDAY TUESDAY +// max date supported by 2-digit year RTC counters (it can't check Y%400==0 with only two digits) +#define DT_END_YEAR 2399 + +typedef union __attribute__((packed)) { + struct __attribute__((packed)) { + uint8_t ones : 4; + uint8_t tens : 4; + }; + uint8_t byte; +} bcd_t; +_Static_assert(sizeof(bcd_t) == 1, "Bad bcd_t len"); + +/** Check if a year is leap */ +static inline bool is_leap_year(int year) +{ + return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0); +} + +/** + * Check if a datetime could be valid (ignores leap years) + * + * @param[in] dt + * @return basic validations passed + */ +bool datetime_is_valid(const datetime_t *dt); + +/** + * Set weekday based on a date in a given datetime + * + * @param[in,out] dt + * @return success + */ +bool datetime_set_weekday(datetime_t *dt); + +/** + * Get weekday for given a date + * + * @param year - year number + * @param month - 1-based month number + * @param day - 1-based day number + * @return weekday + */ +enum weekday date_weekday(uint16_t year, enum month month, uint8_t day); + + +#endif //CSPEMU_DATETIME_H diff --git a/components/common_utils/include/common_utils/hexdump.h b/components/common_utils/include/common_utils/hexdump.h new file mode 100644 index 0000000..1420de1 --- /dev/null +++ b/components/common_utils/include/common_utils/hexdump.h @@ -0,0 +1,19 @@ +/** + * @file + * @brief A simple way of dumping memory to a hex output + * + * \addtogroup Hexdump + * + * @{ + */ + + +#include + +#define HEX_DUMP_LINE_BUFF_SIZ 16 + +extern void hex_dump(FILE * fp,void *src, int len); +extern void hex_dump_buff_line(FILE *fp, int addr_size, unsigned pos, char *line, unsigned len); +/** + * }@ + */ diff --git a/components/common_utils/include/common_utils/utils.h b/components/common_utils/include/common_utils/utils.h new file mode 100644 index 0000000..5ab5514 --- /dev/null +++ b/components/common_utils/include/common_utils/utils.h @@ -0,0 +1,80 @@ +/** + * General purpose, platform agnostic, reusable utils + */ + +#ifndef COMMON_UTILS_UTILS_H +#define COMMON_UTILS_UTILS_H + +#include +#include + +#include "base16.h" +#include "datetime.h" +#include "hexdump.h" + +/** Convert a value to BCD struct */ +static inline bcd_t num2bcd(uint8_t value) +{ + return (bcd_t) {.ones=value % 10, .tens=value / 10}; +} + +/** Convert unpacked BCD to value */ +static inline uint8_t bcd2num(uint8_t tens, uint8_t ones) +{ + return tens * 10 + ones; +} + +/** + * Append to a buffer. + * + * In case the buffer capacity is reached, it stays unchanged (up to the terminator) and NULL is returned. + * + * @param buf - buffer position to append at; if NULL is given, the function immediately returns NULL. + * @param appended - string to append + * @param pcap - pointer to a capacity variable + * @return the new end of the string (null byte); use as 'buf' for following appends + */ +char *append(char *buf, const char *appended, size_t *pcap); + +/** + * Test if a file descriptor is valid (e.g. when cleaning up after a failed select) + * + * @param fd - file descriptor number + * @return is valid + */ +bool fd_is_valid(int fd); + +/** + * parse user-provided string as boolean + * + * @param str + * @return 0 false, 1 true, -1 invalid + */ +int parse_boolean_arg(const char *str); + +/** Check equality of two strings; returns bool */ +#define streq(a, b) (strcmp((const char*)(a), (const char*)(b)) == 0) + +/** Check prefix equality of two strings; returns bool */ +#define strneq(a, b, n) (strncmp((const char*)(a), (const char*)(b), (n)) == 0) + +/** Check if a string starts with a substring; returns bool */ +#define strstarts(a, b) strneq((a), (b), (int)strlen((b))) + +#ifndef MIN +/** Get min of two numbers */ +#define MIN(a, b) ((a) > (b) ? (b) : (a)) +#endif + +#ifndef MAX +/** Get max of two values */ +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#endif + +#ifndef STR +#define STR_HELPER(x) #x +/** Stringify a token */ +#define STR(x) STR_HELPER(x) +#endif + +#endif //COMMON_UTILS_UTILS_H diff --git a/components/common_utils/src/base16.c b/components/common_utils/src/base16.c new file mode 100644 index 0000000..6f13d0a --- /dev/null +++ b/components/common_utils/src/base16.c @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 Michael Brown . + * + * 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 2 of the + * License, or 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, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include +#include +#include +#include +#include + +static const char *TAG = "base16"; + +void base16_encode(uint8_t *raw, size_t len, char *encoded) { + uint8_t *raw_bytes = raw; + char *encoded_bytes = encoded; + size_t remaining = len; + + for (; remaining--; encoded_bytes += 2) + snprintf(encoded_bytes, 3, "%02X", *(raw_bytes++)); + +} + +int base16_decode(const char *encoded, uint8_t *raw) { + const char *encoded_bytes = encoded; + uint8_t *raw_bytes = raw; + char buf[3]; + char *endp; + size_t len; + + while (encoded_bytes[0]) { + if (!encoded_bytes[1]) { + ESP_LOGE(TAG, "Base16-encoded string \"%s\" has invalid length\n", + encoded); + return -22; + } + memcpy(buf, encoded_bytes, 2); + buf[2] = '\0'; + *(raw_bytes++) = strtoul(buf, &endp, 16); + if (*endp != '\0') { + ESP_LOGE(TAG,"Base16-encoded string \"%s\" has invalid byte \"%s\"\n", + encoded, buf); + return -22; + } + encoded_bytes += 2; + } + len = (raw_bytes - raw); + return (len); +} diff --git a/components/common_utils/src/common_utils.c b/components/common_utils/src/common_utils.c new file mode 100644 index 0000000..f5a7b6a --- /dev/null +++ b/components/common_utils/src/common_utils.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include + +#include "common_utils/utils.h" + +char *append(char *buf, const char *appended, size_t *pcap) +{ + char c; + char *buf0 = buf; + size_t cap = *pcap; + + if (buf0 == NULL) return NULL; + if (appended == NULL || appended[0] == 0) return buf0; + + while (cap > 1 && 0 != (c = *appended++)) { + *buf++ = c; + cap--; + } + if (cap == 0) { + *buf0 = '\0'; + return NULL; + } + + *pcap = cap; + *buf = 0; + return buf; +} + +bool fd_is_valid(int fd) +{ + return fcntl(fd, F_GETFD) != -1 || errno != EBADF; +} + +int parse_boolean_arg(const char *str) +{ + if (0 == strcasecmp(str, "on")) return 1; + if (strstarts(str, "en")) return 1; + if (strstarts(str, "y")) return 1; + if (0 == strcmp(str, "1")) return 1; + if (0 == strcasecmp(str, "a")) return 1; + + if (0 == strcasecmp(str, "off")) return 0; + if (0 == strcmp(str, "0")) return 0; + if (strstarts(str, "dis")) return 0; + if (strstarts(str, "n")) return 0; + + return -1; +} + diff --git a/components/common_utils/src/datetime.c b/components/common_utils/src/datetime.c new file mode 100644 index 0000000..b2fede4 --- /dev/null +++ b/components/common_utils/src/datetime.c @@ -0,0 +1,110 @@ + +#include +#include +#include +#include "common_utils/datetime.h" + +const char *DT_WKDAY_NAMES[] = { + [MONDAY] = "Mon", + [TUESDAY] = "Tue", + [WEDNESDAY] = "Wed", + [THURSDAY] = "Thu", + [FRIDAY] = "Fri", + [SATURDAY] = "Sat", + [SUNDAY] = "Sun" +}; + +const char *DT_WKDAY_NAMES_FULL[] = { + [MONDAY] = "Monday", + [TUESDAY] = "Tuesday", + [WEDNESDAY] = "Wednesday", + [THURSDAY] = "Thursday", + [FRIDAY] = "Friday", + [SATURDAY] = "Saturday", + [SUNDAY] = "Sunday" +}; + +const char *DT_MONTH_NAMES[] = { + [JANUARY] = "Jan", + [FEBRUARY] = "Feb", + [MARCH] = "Mar", + [APRIL] = "Apr", + [MAY] = "May", + [JUNE] = "Jun", + [JULY] = "Jul", + [AUGUST] = "Aug", + [SEPTEMBER] = "Sep", + [OCTOBER] = "Oct", + [NOVEMBER] = "Nov", + [DECEMBER] = "Dec" +}; + +const char *DT_MONTH_NAMES_FULL[] = { + [JANUARY] = "January", + [FEBRUARY] = "February", + [MARCH] = "March", + [APRIL] = "April", + [MAY] = "May", + [JUNE] = "June", + [JULY] = "July", + [AUGUST] = "August", + [SEPTEMBER] = "September", + [OCTOBER] = "October", + [NOVEMBER] = "November", + [DECEMBER] = "December" +}; + +static const uint16_t MONTH_LENGTHS[] = { /* 1-based, normal year */ + [JANUARY]=31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 +}; +_Static_assert(sizeof(MONTH_LENGTHS) / sizeof(uint16_t) == 13, "Months array length"); + +static const uint16_t MONTH_YEARDAYS[] = { /* 1-based */ + [JANUARY]=0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 // // days until 1st of month +}; +_Static_assert(sizeof(MONTH_YEARDAYS) / sizeof(uint16_t) == 13, "Months array length"); + +_Static_assert(MONDAY < SUNDAY, "Weekday ordering"); + +bool datetime_is_valid(const datetime_t *dt) +{ + if (dt == NULL) return false; + + // check month first to avoid out-of-bounds read from the MONTH_LENGTHS table + if (!(dt->month >= JANUARY && dt->month <= DECEMBER)) return false; + + int monthlen = MONTH_LENGTHS[dt->month]; + if (dt->month == FEBRUARY && is_leap_year(dt->year)) { + monthlen = 29; + } + + return dt->sec < 60 && + dt->min < 60 && + dt->hour < 24 && + dt->wkday >= MONDAY && + dt->wkday <= SUNDAY && + dt->year >= DT_START_YEAR && + dt->year <= DT_END_YEAR && + dt->day >= 1 && + dt->day <= monthlen; +} + +bool datetime_set_weekday(datetime_t *dt) +{ + dt->wkday = MONDAY; // prevent the validator func erroring out on invalid weekday + if (!datetime_is_valid(dt)) return false; + dt->wkday = date_weekday(dt->year, dt->month, dt->day); + return true; +} + +enum weekday date_weekday(uint16_t year, enum month month, uint8_t day) +{ + uint16_t days = (DT_START_WKDAY - MONDAY) + (year - DT_START_YEAR) * 365 + MONTH_YEARDAYS[month] + (day - 1); + + for (uint16_t i = DT_START_YEAR; i <= year; i++) { + if (is_leap_year(i) && (i < year || month > FEBRUARY)) days++; + } + + return MONDAY + days % 7; +} + diff --git a/components/common_utils/src/hexdump.c b/components/common_utils/src/hexdump.c new file mode 100644 index 0000000..cbf90c2 --- /dev/null +++ b/components/common_utils/src/hexdump.c @@ -0,0 +1,72 @@ +/* + * util.c + * + * Created on: Aug 12, 2009 + * Author: johan + */ + +// adapted from libgomspace + +#include +#include +#include "common_utils/hexdump.h" + + +//! Dump memory to debugging output +/** + * Dumps a chunk of memory to the screen + */ +void hex_dump(FILE * fp, void *src, int len) { + int i, j=0, k; + char text[17]; + + text[16] = '\0'; + //printf("Hex dump:\r\n"); + fprintf(fp, "%p : ", src); + for(i=0; i 126)) { + text[k] = '.'; + } + } + fprintf(fp, " |%s|\n\r", text); + if(i= 32 && line[i] <= 126) + fprintf(fp, "%c", (unsigned char)line[i]); + else + fputc('.', fp); + } + fputs("|\r\n", fp); +} diff --git a/components/dhcp_wd/CMakeLists.txt b/components/dhcp_wd/CMakeLists.txt new file mode 100644 index 0000000..b07fa18 --- /dev/null +++ b/components/dhcp_wd/CMakeLists.txt @@ -0,0 +1,9 @@ +set(COMPONENT_ADD_INCLUDEDIRS + "include") + +set(COMPONENT_SRCDIRS + "src") + +set(COMPONENT_REQUIRES ping tcpip_adapter) + +register_component() diff --git a/components/dhcp_wd/Kconfig b/components/dhcp_wd/Kconfig new file mode 100644 index 0000000..c701475 --- /dev/null +++ b/components/dhcp_wd/Kconfig @@ -0,0 +1,27 @@ +menu "DHCP watchdog" + +config DHCPWD_PERIOD_GW_PING_S + int "Connectivity test interval (s)" + default 60 + help + Time between two connectivity tests (gateway ping) + +config DHCPWD_GETIP_TIMEOUT_S + int "Timeout to get IP (s)" + default 10 + help + Timeout after establishing connection to get an IP address from the DHCP server. + +config DHCPWD_TASK_STACK_SIZE + int "Task stack size (bytes)" + default 4096 + help + DHCP watchdog task stack size + +config DHCPWD_TASK_PRIORITY + int "Task priority" + default 3 + help + DHCP watchdog task priority + +endmenu diff --git a/components/dhcp_wd/README.txt b/components/dhcp_wd/README.txt new file mode 100644 index 0000000..a1a414f --- /dev/null +++ b/components/dhcp_wd/README.txt @@ -0,0 +1,5 @@ +DHCP ping watchdog. + +ESP32 sometimes loses wireless connectivity (expiring lease that fails to renew, +AP rebooting and forgetting us, etc). This module periodically pings the gateway +and triggers reconnect if the ping fails. diff --git a/components/dhcp_wd/component.mk b/components/dhcp_wd/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/dhcp_wd/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/dhcp_wd/include/dhcp_wd.h b/components/dhcp_wd/include/dhcp_wd.h new file mode 100644 index 0000000..b301085 --- /dev/null +++ b/components/dhcp_wd/include/dhcp_wd.h @@ -0,0 +1,82 @@ +/** + * DHCP watchdog + * + * This is a workaround for a rare case where we don't get + * any IP after connecting with STA. If it takes too long, + * try power-cycling the DHCP client. If that fails too, + * try cycling the WiFi stack too. + * + * This does not try to reboot, as there are valid cases when this + * can happen - e.g. no DHCP on the network + no static IP configured yet. + * + * The ping component is used as a dependency. + */ + +#ifndef _DHCP_WD_H_ +#define _DHCP_WD_H_ + +#include "esp_netif.h" +#include "esp_event.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +typedef struct dhcp_wd_instance * dhcp_wd_handle_t; + +/** + * Start the watchdog. Handle must remain valid until the task is deleted. + * + * @param[in] iface + * @param[out] handle - pointer to a handle variable (will be written to it) + * @return success + */ +esp_err_t dhcp_watchdog_start(esp_netif_t * netif, bool is_wifi, dhcp_wd_handle_t *pHandle); + +/** + * Check if a watchdog is running + * + * @param[in] handle + * @return is running + */ +bool dhcp_watchdog_is_running(dhcp_wd_handle_t handle); + +/** + * Stop the watchdog and free resources. + * The handle becomes invalid and is set to NULL. + * + * @param[in] handle + * @return success + */ +esp_err_t dhcp_watchdog_stop(dhcp_wd_handle_t *pHandle); + +enum dhcp_wd_event { + DHCP_WD_NOTIFY_CONNECTED, + DHCP_WD_NOTIFY_DISCONNECTED, + DHCP_WD_NOTIFY_GOT_IP, +}; + +/** + * @brief Notify the watchdog task about a wifi state change + * + * Call this from the WiFi event handler. + * + * @param[in] handle + * @param[in] event - detected event + */ +esp_err_t dhcp_watchdog_notify(dhcp_wd_handle_t handle, enum dhcp_wd_event); + +enum dhcp_wd_test_result { + DHCP_WD_RESULT_OK = 0, + DHCP_WD_RESULT_PING_LOST, + DHCP_WD_RESULT_NO_GATEWAY, +}; + +/** + * Manually trigger a connection test by pinging the gateway. + * This is independent on any watchdog tasks and can be run without starting the watchdog. + * + * @param[in] iface - network interface, typically TCPIP_ADAPTER_IF_STA + * @return test result + */ +enum dhcp_wd_test_result dhcp_wd_test_connection(esp_netif_t *iface); + +#endif //_DHCP_WD_H_ diff --git a/components/dhcp_wd/src/dhcp_wd.c b/components/dhcp_wd/src/dhcp_wd.c new file mode 100644 index 0000000..3281bba --- /dev/null +++ b/components/dhcp_wd/src/dhcp_wd.c @@ -0,0 +1,277 @@ +#include +#include "esp_log.h" +#include "dhcp_wd.h" +#include "esp_wifi.h" +//#include "esp_eth.h" +#include "ping.h" + +#define xstr(s) str(s) +#define str(s) #s + +#define PERIOD_GW_PING_S CONFIG_DHCPWD_PERIOD_GW_PING_S +#define GETIP_TIMEOUT_S CONFIG_DHCPWD_GETIP_TIMEOUT_S +#define TASK_STACK_SIZE CONFIG_DHCPWD_TASK_STACK_SIZE +#define TASK_PRIO CONFIG_DHCPWD_TASK_PRIORITY + +static const char *TAG = "dhcp_wd"; + +static void dhcp_watchdog_task(void *parm); + +struct dhcp_wd_instance { + TaskHandle_t task; + esp_netif_t * iface; + bool is_wifi; + bool running; +}; + +#define STATES_ENUM \ + X(DISCONECTED) \ + X(CONECTED_WAIT_IP) \ + X(CONECTED_WAIT_IP2) \ + X(CONECTED) + +enum dhcp_wd_state { +#undef X +#define X(s) STATE_##s, + STATES_ENUM +}; + +const char *state_names[] = { +#undef X +#define X(s) xstr(s), + STATES_ENUM +}; + +enum dhcp_wd_notify { + NOTIFY_CONNECTED = BIT0, + NOTIFY_DISCONNECTED = BIT1, + NOTIFY_GOT_IP = BIT2, + NOTIFY_SHUTDOWN = BIT3, // kills the task +}; + +/** Send a notification to the watchdog task */ +esp_err_t dhcp_watchdog_notify(dhcp_wd_handle_t handle, const enum dhcp_wd_event event) +{ + assert(handle != NULL); + assert(handle->task != NULL); + + uint32_t flag = 0; + + switch (event) { + case DHCP_WD_NOTIFY_CONNECTED: + flag = NOTIFY_CONNECTED; + break; + + case DHCP_WD_NOTIFY_DISCONNECTED: + flag = NOTIFY_DISCONNECTED; + break; + + case DHCP_WD_NOTIFY_GOT_IP: + flag = NOTIFY_GOT_IP; + break; + + default: + break; + } + + BaseType_t ret = pdPASS; + if (flag != 0) { + ret = xTaskNotify(handle->task, flag, eSetBits); + } + + return (pdPASS == ret) ? ESP_OK : ESP_FAIL; +} + +/** + * Start the watchdog + */ +esp_err_t dhcp_watchdog_start(esp_netif_t * netif, bool is_wifi, dhcp_wd_handle_t *pHandle) +{ + assert(pHandle != NULL); + + dhcp_wd_handle_t handle = calloc(1, sizeof(struct dhcp_wd_instance)); + if (!handle) return ESP_ERR_NO_MEM; + *pHandle = handle; + + handle->iface = netif; + handle->is_wifi = is_wifi; + + BaseType_t ret = xTaskCreate(dhcp_watchdog_task, "dhcp-wd", TASK_STACK_SIZE, (void *)handle, TASK_PRIO, &handle->task); + handle->running = true; + + return (pdPASS == ret) ? ESP_OK : ESP_FAIL; +} + +/** + * Check if a watchdog is still running + * + * @param handle + * @return is running + */ +bool dhcp_watchdog_is_running(dhcp_wd_handle_t handle) +{ + return handle->running; +} + +/** + * Stop the watchdog and free resources + */ +esp_err_t dhcp_watchdog_stop(dhcp_wd_handle_t *pHandle) +{ + assert(pHandle != NULL); + assert(*pHandle != NULL); + xTaskNotify((*pHandle)->task, NOTIFY_SHUTDOWN, eSetBits); + *pHandle = NULL; + return ESP_OK; +} + +/** + * @param parm - tcpip_adapter_if_t iface (cast to void *) - typically TCPIP_ADAPTER_IF_STA + */ +static void dhcp_watchdog_task(void *parm) +{ + enum dhcp_wd_state state = STATE_DISCONECTED; + + dhcp_wd_handle_t handle = parm; + assert(handle != NULL); + assert(handle->iface != NULL); + + ESP_LOGI(TAG, "Watchdog started"); + + while (1) { + uint32_t flags = 0; + uint32_t wait_s; + TickType_t waittime; + + switch (state) { + case STATE_DISCONECTED: + wait_s = waittime = portMAX_DELAY; + break; + + case STATE_CONECTED_WAIT_IP: + case STATE_CONECTED_WAIT_IP2: + wait_s = GETIP_TIMEOUT_S; + waittime = (GETIP_TIMEOUT_S * 1000) / portTICK_PERIOD_MS; + break; + + case STATE_CONECTED: + wait_s = PERIOD_GW_PING_S; + waittime = (PERIOD_GW_PING_S * 1000) / portTICK_PERIOD_MS; + break; + + default: + assert(0); + } + + ESP_LOGD(TAG, "State %s, wait %d s", state_names[state], wait_s); + BaseType_t rv = xTaskNotifyWait( + /* no clear on entry */ pdFALSE, + /* clear all on exit */ ULONG_MAX, + &flags, waittime); + + if (rv == pdPASS) { + // the order here is important in case we get multiple events at once + + if (flags & NOTIFY_DISCONNECTED) { + state = STATE_DISCONECTED; + } + + if (flags & NOTIFY_CONNECTED) { + state = STATE_CONECTED_WAIT_IP; + } + + if (flags & NOTIFY_GOT_IP) { + state = STATE_CONECTED; + } + + if (flags & NOTIFY_SHUTDOWN) { + // kill self + handle->running = false; + free(handle); + vTaskDelete(NULL); + return; + } + } else { + // a timeout occurred + + switch (state) { + case STATE_DISCONECTED: + // this shouldn't happen, we have infinite delay waiting for disconnected + ESP_LOGW(TAG, "dhcp_wd double discon evt"); + break; + + case STATE_CONECTED_WAIT_IP: + ESP_LOGW(TAG, "Get IP timeout, restarting DHCP client"); + // this is a bit suspicious + // try to restart the DHCPC client + ESP_ERROR_CHECK(esp_netif_dhcpc_stop(handle->iface)); + ESP_ERROR_CHECK(esp_netif_dhcpc_start(handle->iface)); + state = STATE_CONECTED_WAIT_IP2; + break; + + case STATE_CONECTED_WAIT_IP2: + ESP_LOGW(TAG, "Get IP timeout 2, restarting network stack"); + // well now this is weird. try flipping the whole WiFi/Eth stack + if (handle->is_wifi) { + ESP_ERROR_CHECK(esp_wifi_disconnect()); + } + // this will trigger the disconnected event and loop back into Disconnected + // the disconnect event handler calls connect again + state = STATE_DISCONECTED; + break; + + case STATE_CONECTED: { + // Ping gateway to check if we're still connected + enum dhcp_wd_test_result result = dhcp_wd_test_connection(handle->iface); + + if (result == DHCP_WD_RESULT_PING_LOST) { + // looks like the gateway silently dropped us + // try kicking the DHCP client, if it helps + ESP_ERROR_CHECK(esp_netif_dhcpc_stop(handle->iface)); + ESP_ERROR_CHECK(esp_netif_dhcpc_start(handle->iface)); + state = STATE_CONECTED_WAIT_IP2; + // if not, it'll flip the whole wifi stack + } else { + ESP_LOGD(TAG, "Gateway ping OK"); + } + break; + } + } + } + } +} + +enum dhcp_wd_test_result dhcp_wd_test_connection(esp_netif_t *iface) +{ + ESP_LOGD(TAG, "Ping Gateway to check if IP is valid"); + + ping_opts_t opts = PING_CONFIG_DEFAULT(); + opts.count = 3; + opts.interval_ms = 0; + opts.timeout_ms = 1000; + + esp_netif_ip_info_t ip_info = {}; + ESP_ERROR_CHECK(esp_netif_get_ip_info(iface, &ip_info)); + + opts.ip_addr.addr = ip_info.gw.addr; + + ping_result_t result = {}; + + if (ip_info.gw.addr != 0) { + esp_err_t ret = ping(&opts, &result); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "Ping error"); + return DHCP_WD_RESULT_PING_LOST; + } + ESP_LOGD(TAG, "Ping result: %d tx, %d rx", result.sent, result.received); + if (result.received == 0) { + ESP_LOGW(TAG, "Failed to ping GW"); + return DHCP_WD_RESULT_PING_LOST; + } else { + return DHCP_WD_RESULT_OK; + } + } else { + ESP_LOGW(TAG, "No GW IP to ping"); + return DHCP_WD_RESULT_NO_GATEWAY; + } +} diff --git a/components/fileserver/CMakeLists.txt b/components/fileserver/CMakeLists.txt new file mode 100644 index 0000000..0d00082 --- /dev/null +++ b/components/fileserver/CMakeLists.txt @@ -0,0 +1,16 @@ +set(COMPONENT_ADD_INCLUDEDIRS + "include" "files") + +set(COMPONENT_SRCDIRS + "src" "files") + +set(COMPONENT_REQUIRES tcpip_adapter esp_http_server httpd_utils common_utils) + +#begin staticfiles +# generated by rebuild_file_tables +set(COMPONENT_EMBED_FILES + "files/embed/favicon.ico" + "files/embed/index.html") +#end staticfiles + +register_component() diff --git a/components/fileserver/README.txt b/components/fileserver/README.txt new file mode 100644 index 0000000..588ad42 --- /dev/null +++ b/components/fileserver/README.txt @@ -0,0 +1,2 @@ +File and template serving support for the http_server bundled with ESP-IDF. + diff --git a/components/fileserver/component.mk b/components/fileserver/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/fileserver/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/fileserver/files/embed/favicon.ico b/components/fileserver/files/embed/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3c332d8459ba843a75f13f2ccc917e7d88c24939 GIT binary patch literal 2238 zcmeHIJ8pz95Pd92*(!8tO_^JeLvWDXz#X|tPJomyxd}(0q(C^&hMBbkOOz@Ng9eZ1 zz4`F8(gGQussi6vykCJQ0MB%$@Iv?bBWs!lZQH_q0A1G|QBUkt;8fsLV5>lZ%VnF@ zOi>&Whz5evbB%nkz`lc|6I|figglQ4iVK18K7rrB5dpqSP<(GhXt37D9dw1D(r*O8 zrS8*?MG+~ABPY}W%8thI`4g6nT zY_i}&AQ~)U1VXs;N47x;kf3yr>qUYHmsE1z%9HfJU!1o$dgk1e&2GeG2aLAIYu%$V x15l2@13jOp+|1MQPA?f-=E;Zcf%+3LJiV8`ZYWs$2s0Vj*?hUV<{V3p>>K7!OF;kt literal 0 HcmV?d00001 diff --git a/components/fileserver/files/embed/index.html b/components/fileserver/files/embed/index.html new file mode 100644 index 0000000..b16cd80 --- /dev/null +++ b/components/fileserver/files/embed/index.html @@ -0,0 +1,106 @@ + + + + +ESP node + + + +

ESP node {version}

+ +Restart node + + diff --git a/components/fileserver/files/rebuild_file_tables.php b/components/fileserver/files/rebuild_file_tables.php new file mode 100755 index 0000000..b0018e2 --- /dev/null +++ b/components/fileserver/files/rebuild_file_tables.php @@ -0,0 +1,163 @@ +#!/usr/bin/env php + 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + + 'pem' => 'application/x-pem-file', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + + // ms office + 'doc' => 'application/msword', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + + // open office + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + ); + + $parts = explode('.', $f); + $suffix = end($parts); + $mime = $mimes[$suffix] ?? 'application/octet-stream'; + + $len = filesize('embed/'.$f); + + $struct_array[] = "[FILE_$a] = {{$start}, {$end}, \"{$f}\", \"{$mime}\"},"; + + return + 'extern const uint8_t '.$start.'[];'."\n". + 'extern const uint8_t '.$end.'[];'; +}, $files); + +$externlist = implode("\n", $externs); +$structlist = implode("\n ", $struct_array); + + +file_put_contents('www_files_enum.h', << +#include "www_files_enum.h" + +$externlist + +const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = { + $structlist +}; + +const size_t EMBEDDED_FILE_LOOKUP_LEN = $files_count; + +FILE +); diff --git a/components/fileserver/files/www_files_enum.c b/components/fileserver/files/www_files_enum.c new file mode 100644 index 0000000..86b8421 --- /dev/null +++ b/components/fileserver/files/www_files_enum.c @@ -0,0 +1,15 @@ +// Generated by 'rebuild_file_tables' +#include +#include "www_files_enum.h" + +extern const uint8_t _binary_favicon_ico_start[]; +extern const uint8_t _binary_favicon_ico_end[]; +extern const uint8_t _binary_index_html_start[]; +extern const uint8_t _binary_index_html_end[]; + +const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = { + [FILE_FAVICON_ICO] = {_binary_favicon_ico_start, _binary_favicon_ico_end, "favicon.ico", "image/vnd.microsoft.icon"}, + [FILE_INDEX_HTML] = {_binary_index_html_start, _binary_index_html_end, "index.html", "text/html"}, +}; + +const size_t EMBEDDED_FILE_LOOKUP_LEN = 2; diff --git a/components/fileserver/files/www_files_enum.h b/components/fileserver/files/www_files_enum.h new file mode 100644 index 0000000..b81113b --- /dev/null +++ b/components/fileserver/files/www_files_enum.h @@ -0,0 +1,13 @@ +// Generated by 'rebuild_file_tables' + +#ifndef _EMBEDDED_FILES_ENUM_H +#define _EMBEDDED_FILES_ENUM_H + +#include "fileserver/embedded_files.h" + +enum embedded_file_id { + FILE_FAVICON_ICO = 0, + FILE_INDEX_HTML = 1 +}; + +#endif // _EMBEDDED_FILES_ENUM_H diff --git a/components/fileserver/include/fileserver/embedded_files.h b/components/fileserver/include/fileserver/embedded_files.h new file mode 100644 index 0000000..a86f8ae --- /dev/null +++ b/components/fileserver/include/fileserver/embedded_files.h @@ -0,0 +1,51 @@ +// +// Created on 2018/10/17 by Ondrej Hruska +// + +#ifndef FBNODE_EMBEDDED_FILES_H +#define FBNODE_EMBEDDED_FILES_H + +#include +#include +#include + +struct embedded_file_info { + const uint8_t * start; + const uint8_t * end; + const char * name; + const char * mime; +}; + +enum file_access_level { + /** Public = file accessed by a wildcard route */ + FILE_ACCESS_PUBLIC = 0, + /** Protected = file included in a template or explicitly specified in a route */ + FILE_ACCESS_PROTECTED = 1, + /** Files protected against read-out */ + FILE_ACCESS_PRIVATE = 2, +}; + +extern const struct embedded_file_info EMBEDDED_FILE_LOOKUP[]; +extern const size_t EMBEDDED_FILE_LOOKUP_LEN; + +/** + * Find an embedded file by its name. + * + * This function is weak. It crawls the EMBEDDED_FILE_LOOKUP table and checks for exact match, also + * testing with www_get_static_file_access_check if the access is allowed. + * + * @param name - file name + * @param access - access level (public - wildcard fallthrough, protected - files for the server, loaded explicitly) + * @param[out] file - the file struct is stored here if found, unchanged if not found. + * @return status code + */ +esp_err_t www_get_static_file(const char *name, enum file_access_level access, const struct embedded_file_info **file); + +/** + * Check file access permission (if using the default www_get_static_file implementation). + * + * This function is weak. The default implementation returns always true. + */ +bool www_get_static_file_access_check(const struct embedded_file_info *file, enum file_access_level access); + +#endif //FBNODE_EMBEDDED_FILES_H diff --git a/components/fileserver/include/fileserver/token_subs.h b/components/fileserver/include/fileserver/token_subs.h new file mode 100644 index 0000000..1e396b4 --- /dev/null +++ b/components/fileserver/include/fileserver/token_subs.h @@ -0,0 +1,216 @@ +// +// This module implements token substitution in files served by the server. +// +// Tokens are in the form {token}, or {escape:token}, where escape can be: +// - h ... html escape (plain text in a html file, attribute value) +// - j ... js escape (for use in JS strings) +// +// When no escape is specified, the token substitution is written verbatim into the response. +// +// var foo = "{j:foo}"; +// +// {generated-html-goes-here} +// +// Token can be made optional by adding '?' at the end (this can't be used for includes). +// Such token then simply becomes empty string when not substituted, as opposed to being included in the page verbatim. +// +// +// +// token names can contain alnum, dash, period and underscore, and are case sensitive. +// +// +// It is further possible to include a static file with optional key-value replacements. These serve as defaults. +// +// {@_subfile.html} +// {@_subfile.html|key=value lalala} +// {@_subfile.html|key=value lalala|other=value} +// +// File inclusion can be nested, and the files can use replacement tokens as specified by the include statement +// +// Created on 2019/01/24 by Ondrej Hruska +// + +#ifndef FBNODE_TOKEN_SUBS_H +#define FBNODE_TOKEN_SUBS_H + +#include "embedded_files.h" +#include +#include +#include + +/** Max length of a token buffer (must suffice for all included filenames) */ +#define MAX_TOKEN_LEN 32 + +/** Max length of a key-value substitution when using tpl_kv_replacer; + * This is also used internally for in-line replacements in file imports. */ +#define TPL_KV_KEY_LEN 24 +/** Max length of a substituion in tpl_kv_replacer */ +#define TPL_KV_SUBST_LEN 64 + +/** + * Escape type - argument for httpd_resp_send_chunk_escaped() + */ +typedef enum { + TPL_ESCAPE_NONE = 0, + TPL_ESCAPE_HTML, + TPL_ESCAPE_JS, +} tpl_escape_t; + +enum { + HTOPT_NONE = 0, + HTOPT_NO_HEADERS = 1 << 0, + HTOPT_NO_CLOSE = 1 << 1, + HTOPT_INCLUDE = HTOPT_NO_HEADERS|HTOPT_NO_CLOSE, +}; + +/** + * Send string using a given escaping scheme + * + * @param r + * @param buf - buf to send + * @param len - buf len, or HTTPD_RESP_USE_STRLEN + * @param escape - escaping type + * @return success + */ +esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape); + +/** + * Template substitution callback. Data shall be sent using httpd_resp_send_chunk_escaped(). + * + * @param[in,out] context - user-defined page state data + * @param[in] token - replacement token + * @return ESP_OK if the token was substituted, ESP_ERR_NOT_FOUND if it is unknown, other errors on e.g. send failure + */ +typedef esp_err_t (*template_subst_t)(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape); + +/** + * Send a template file as a response. The content type from the file struct will be used. + * + * Use HTOPT_INCLUDE when used to embed a file inside a template. + * + * @param r - request + * @param file_index - file index in EMBEDDED_FILE_LOOKUP + * @param replacer - substitution callback, can be NULL if only includes are to be processed + * @param context - arbitrary context, will be passed to the replacer function; can be NULL + * @param opts - flag options (HTOPT_*) + */ +esp_err_t httpd_send_template_file(httpd_req_t *r, int file_index, template_subst_t replacer, void *context, uint32_t opts); + +/** + * Same as httpd_send_template_file, but using an `embedded_file_info` struct. + */ +esp_err_t httpd_send_template_file_struct(httpd_req_t *r, const struct embedded_file_info *file, template_subst_t replacer, void *context, uint32_t opts); + +/** + * Process and send a string template. + * The content-type header should be set beforehand, if different from the default (text/html). + * + * Use HTOPT_INCLUDE when used to embed a file inside a template. + * + * @param r - request + * @param template - template string (does not have to be terminated by a null byte) + * @param template_len - length of the template string; -1 to use strlen() + * @param replacer - substitution callback, can be NULL if only includes are to be processed + * @param context - arbitrary context, will be passed to the replacer function; can be NULL + * @param opts - flag options (HTOPT_*) + */ +esp_err_t httpd_send_template(httpd_req_t *r, const char *template, ssize_t template_len, template_subst_t replacer, void *context, uint32_t opts); + +/** + * Send a static file. This can be used to just send a file, or to embed a static template as a token substitution. + * + * Use HTOPT_INCLUDE when used to embed a file inside a template. + * + * Note: use httpd_resp_send_chunk_escaped() or httpd_resp_send_chunk() to send a plain string. + * + * @param r - request + * @param file_index - file index in EMBEDDED_FILE_LOOKUP + * @param escape - escape option + * @param opts - flag options (HTOPT_*) + * @return + */ +esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts); + +/** + * Same as httpd_send_template_file, but using an `embedded_file_info` struct. + */ +esp_err_t httpd_send_static_file_struct(httpd_req_t *r, const struct embedded_file_info *file, tpl_escape_t escape, uint32_t opts); + +struct tpl_kv_entry { + char key[TPL_KV_KEY_LEN]; // copied here + char subst[TPL_KV_SUBST_LEN]; // copied here + SLIST_ENTRY(tpl_kv_entry) link; +}; + +SLIST_HEAD(tpl_kv_list, tpl_kv_entry); + +/** + * key-value replacer that works with a dynamically allocated SLIST. + * + * @param r - request + * @param context - context - must be a pointer to `struct tpl_kv_list` + * @param token - token to replace + * @param escape - escape option + * @return OK/not found/other + */ +esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape); + +/** + * Add a pair into the substitutions list + * + * @param head - list head + * @param key - key, copied + * @param subst - value, copied + * @return success (fails if malloc failed) + */ +esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst); + +/** + * Convenience function that converts an IP address to string and adds it as a substitution + * + * @param head - list head + * @param key - key, copied + * @param ip4h - host order ipv4 address + * @return success + */ +esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h); + +/** add int as a substitution; key is copied */ +esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num); + +/** add long as a substitution; key is copied */ +esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num); + +/** add printf-formatted value; key is copied */ +esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...) + __attribute__((format(printf,3,4))); + +/** + * Init a substitutions list (on the stack) + * + * @return the list + */ +static inline struct tpl_kv_list tpl_kv_init(void) +{ + return (struct tpl_kv_list) {.slh_first = NULL}; +} + +/** + * Free the list (head is left alone because it was allocated on the stack) + * @param head + */ +void tpl_kv_free(struct tpl_kv_list *head); + +/** + * Send the map as an ASCII table separated by Record Separator (30) and Unit Separator (31). + * Content type is set to application/octet-stream. + * + * key 31 value 30 + * key 31 value 30 + * key 31 value + * + * @param req + */ +esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head); + +#endif //FBNODE_TOKEN_SUBS_H diff --git a/components/fileserver/readme/README.md b/components/fileserver/readme/README.md new file mode 100644 index 0000000..42d6883 --- /dev/null +++ b/components/fileserver/readme/README.md @@ -0,0 +1,29 @@ +Place the `rebuild_file_tables.php` script in a `files` subfolder of the main component. +It requires PHP 7 to run. + +This is what the setup should look like + +``` +main/files/embed/index.html +main/files/rebuild_file_tables.php +main/CMakeLists.txt +main/main.c +``` + +Add this to your CMakeLists.txt before `register_component`: + +``` +#begin staticfiles +#end staticfiles +``` + +The script will update CMakeLists.txt and generate `files_enum.c` and `files_enum.h` when run. + +``` +main/files/files_enum.h +main/files/files_enum.c +``` + +Ensure `files/files_enum.c` is included in the build. + +`www_get_static_file()` is implemented as weak to let you provide custom access authentication logic. diff --git a/components/fileserver/readme/rebuild_file_tables.php b/components/fileserver/readme/rebuild_file_tables.php new file mode 100755 index 0000000..2d81c59 --- /dev/null +++ b/components/fileserver/readme/rebuild_file_tables.php @@ -0,0 +1,170 @@ +#!/usr/bin/env php + 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + + 'pem' => 'application/x-pem-file', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + + // ms office + 'doc' => 'application/msword', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + + // open office + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + ); + + $parts = explode('.', $f); + $suffix = end($parts); + $mime = $mimes[$suffix] ?? 'application/octet-stream'; + + $len = filesize('embed/'.$f); + + $struct_array[] = "[FILE_$a] = {{$start}, {$end}, \"{$f}\", \"{$mime}\"},"; + + return + 'extern const uint8_t '.$start.'[];'."\n". + 'extern const uint8_t '.$end.'[];'; +}, $files); + +$externlist = implode("\n", $externs); +$structlist = implode("\n ", $struct_array); + + +file_put_contents('files_enum.h', << +#include + +enum embedded_file_id { + $keylist, + FILE_MAX +}; + +struct embedded_file_info { + const uint8_t * start; + const uint8_t * end; + const char * name; + const char * mime; +}; + +$externlist + +extern const struct embedded_file_info EMBEDDED_FILE_LOOKUP[]; + +#endif // _EMBEDDED_FILES_ENUM_H + +FILE +); + +file_put_contents("files_enum.c", << +#include "files_enum.h" + +const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = { + $structlist +}; + +FILE +); diff --git a/components/fileserver/src/embedded_files.c b/components/fileserver/src/embedded_files.c new file mode 100644 index 0000000..e61f348 --- /dev/null +++ b/components/fileserver/src/embedded_files.c @@ -0,0 +1,22 @@ +#include +#include "fileserver/embedded_files.h" +#include "string.h" + +esp_err_t __attribute__((weak)) +www_get_static_file(const char *name, enum file_access_level access, const struct embedded_file_info **file) +{ + // simple search by name + for(int i = 0; i < EMBEDDED_FILE_LOOKUP_LEN; i++) { + if (0 == strcmp(EMBEDDED_FILE_LOOKUP[i].name, name)) { + *file = &EMBEDDED_FILE_LOOKUP[i]; + return ESP_OK; + } + } + + return ESP_ERR_NOT_FOUND; +} + +bool __attribute__((weak)) +www_get_static_file_access_check(const struct embedded_file_info *file, enum file_access_level access) { + return true; +} diff --git a/components/fileserver/src/token_subs.c b/components/fileserver/src/token_subs.c new file mode 100644 index 0000000..fd5b67a --- /dev/null +++ b/components/fileserver/src/token_subs.c @@ -0,0 +1,580 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include +#include +#include +#include + +#include "fileserver/embedded_files.h" +#include "fileserver/token_subs.h" + +#define ESP_TRY(x) \ + do { \ + esp_err_t try_er = (x); \ + if (try_er != ESP_OK) return try_er; \ + } while(0) + +static const char* TAG = "token_subs"; + +// TODO implement buffering to avoid sending many tiny chunks when escaping + +/* encode for HTML. returns 0 or 1 - 1 = success */ +static esp_err_t send_html_chunk(httpd_req_t *r, const char *data, ssize_t len) +{ + assert(r); + assert(data); + + int start = 0, end = 0; + char c; + if (len < 0) len = (int) strlen(data); + if (len==0) return ESP_OK; + + for (end = 0; end < len; end++) { + c = data[end]; + if (c == 0) { + // we found EOS + break; // not return - the last chunk is printed after the loop + } + + if (c == '"' || c == '\'' || c == '<' || c == '>') { + if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); + start = end + 1; + } + + if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, """, 5)); + else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "'", 5)); + else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "<", 4)); + else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, ">", 4)); + } + + if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); + return ESP_OK; +} + +/* encode for JS. returns 0 or 1 - 1 = success */ +static esp_err_t send_js_chunk(httpd_req_t *r, const char *data, ssize_t len) +{ + assert(r); + assert(data); + + int start = 0, end = 0; + char c; + if (len < 0) len = (int) strlen(data); + if (len==0) return ESP_OK; + + for (end = 0; end < len; end++) { + c = data[end]; + if (c == 0) { + // we found EOS + break; // not return - the last chunk is printed after the loop + } + + if (c == '"' || c == '\\' || c == '/' || c == '\'' || c == '<' || c == '>' || c == '\n' || c == '\r') { + if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); + start = end + 1; + } + + if (c == '"') ESP_TRY(httpd_resp_send_chunk(r, "\\\"", 2)); + else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "\\'", 2)); + else if (c == '\\') ESP_TRY(httpd_resp_send_chunk(r, "\\\\", 2)); + else if (c == '/') ESP_TRY(httpd_resp_send_chunk(r, "\\/", 2)); + else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "\\u003C", 6)); + else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "\\u003E", 6)); + else if (c == '\n') ESP_TRY(httpd_resp_send_chunk(r, "\\n", 2)); + else if (c == '\r') ESP_TRY(httpd_resp_send_chunk(r, "\\r", 2)); + } + + if (start < end) ESP_TRY(httpd_resp_send_chunk(r, data + start, end - start)); + return ESP_OK; +} + + +esp_err_t httpd_resp_send_chunk_escaped(httpd_req_t *r, const char *buf, ssize_t len, tpl_escape_t escape) +{ + switch (escape) { + default: // this enum should be exhaustive, but in case something went wrong, just print it verbatim + + case TPL_ESCAPE_NONE: + return httpd_resp_send_chunk(r, buf, len); + + case TPL_ESCAPE_HTML: + return send_html_chunk(r, buf, len); + + case TPL_ESCAPE_JS: + return send_js_chunk(r, buf, len); + } +} + +esp_err_t httpd_send_static_file(httpd_req_t *r, int file_index, tpl_escape_t escape, uint32_t opts) +{ + assert(file_index < EMBEDDED_FILE_LOOKUP_LEN); + const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index]; + + return httpd_send_static_file_struct(r, file, escape, opts); +} + +esp_err_t httpd_send_static_file_struct(httpd_req_t *r, const struct embedded_file_info *file, tpl_escape_t escape, uint32_t opts) +{ + if (0 == (opts & HTOPT_NO_HEADERS)) { + ESP_TRY(httpd_resp_set_type(r, file->mime)); + ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "max-age=86400, public, must-revalidate")); + } + + ESP_TRY(httpd_resp_send_chunk_escaped(r, (const char *) file->start, (size_t)(file->end - file->start), escape)); + + if (0 == (opts & HTOPT_NO_CLOSE)) { + ESP_TRY(httpd_resp_send_chunk(r, NULL, 0)); + } + + return ESP_OK; +} + +esp_err_t httpd_send_template_file(httpd_req_t *r, + int file_index, + template_subst_t replacer, + void *context, + uint32_t opts) +{ + assert(file_index < EMBEDDED_FILE_LOOKUP_LEN); + const struct embedded_file_info *file = &EMBEDDED_FILE_LOOKUP[file_index]; + return httpd_send_template_file_struct(r,file,replacer,context,opts); +} + +esp_err_t httpd_send_template_file_struct(httpd_req_t *r, + const struct embedded_file_info *file, + template_subst_t replacer, + void *context, + uint32_t opts) +{ + if (0 == (opts & HTOPT_NO_HEADERS)) { + ESP_TRY(httpd_resp_set_type(r, file->mime)); + ESP_TRY(httpd_resp_set_hdr(r, "Cache-Control", "no-cache, no-store, must-revalidate")); + } + + return httpd_send_template(r, (const char *) file->start, (size_t)(file->end - file->start), replacer, context, opts); +} + +esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape) +{ + assert(context); + assert(token); + + struct tpl_kv_entry *entry; + struct tpl_kv_list *head = context; + SLIST_FOREACH(entry, head, link) { + if (0==strcmp(entry->key, token)) { + return httpd_resp_send_chunk_escaped(r, entry->subst, -1, escape); + } + } + + return ESP_ERR_NOT_FOUND; +} + +struct stacked_replacer_context { + template_subst_t replacer0; + void *context0; + template_subst_t replacer1; + void *context1; +}; + +esp_err_t stacked_replacer(httpd_req_t *r, void *context, const char *token, tpl_escape_t escape) +{ + assert(context); + assert(token); + + struct stacked_replacer_context *combo = context; + + if (ESP_OK == combo->replacer0(r, combo->context0, token, escape)) { + return ESP_OK; + } + + if (ESP_OK == combo->replacer1(r, combo->context1, token, escape)) { + return ESP_OK; + } + + return ESP_ERR_NOT_FOUND; +} + +esp_err_t httpd_send_template(httpd_req_t *r, + const char *template, ssize_t template_len, + template_subst_t replacer, + void *context, + uint32_t opts) +{ + if (template_len < 0) template_len = strlen(template); + + // replacer and context may be NULL + assert(template); + assert(r); + + // data end + const char * const end = template + template_len; + + // start of to-be-processed data + const char * pos = template; + + // start position for finding opening braces, updated after a failed match to avoid infinite loop on the same bad token + const char * searchpos = pos; + + // tokens must be copied to a buffer to allow adding the terminating null byte + char token_buf[MAX_TOKEN_LEN]; + + while (pos < end) { + const char * openbr = strchr(searchpos, '{'); + if (openbr == NULL) { + // no more templates + ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos))); + break; + } + + // this brace could start a valid template. check if it seems valid... + const char * closebr = strchr(openbr, '}'); + if (closebr == NULL) { + // there are no further closing braces, so it can't be a template + + // we also know there can't be any more substitutions, because they would lack a closing } too + ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (end - pos))); + break; + } + + // see if the braces content looks like a token + const char *t = openbr + 1; + bool token_valid = true; + struct tpl_kv_list substitutions_head = tpl_kv_init(); + struct tpl_kv_entry *new_subst_pair = NULL; + + // a token can be either a name for replacement by the replacer func, or an include with static kv replacements + bool is_include = false; + bool token_is_optional = false; + const char *token_end = NULL; // points one char after the end of the token + + // parsing the token + { + if (*t == '@') { + ESP_LOGD(TAG, "Parsing an Include token"); + is_include = true; + t++; + } + + enum { + P_NAME, P_KEY, P_VALUE + } state = P_NAME; + + const char *kv_start = NULL; + while (t != closebr || state == P_VALUE) { + char c = *t; + + if (state == P_NAME) { + if (!((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '.' || c == '_' || c == '-' || c == ':')) { + + if (!is_include && c == '?') { + token_end = t; + token_is_optional = true; + } else { + if (is_include && c == '|') { + token_end = t; + state = P_KEY; + kv_start = t + 1; + // pipe separates the include's filename and literal substitutions + // we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct + } + else { + token_valid = false; + break; + } + } + } + } + else if (state == P_KEY) { + if (!((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '.' || c == '_' || c == '-')) { + if (c == '=') { + new_subst_pair = calloc(sizeof(struct tpl_kv_entry), 1); + const size_t klen = MIN(TPL_KV_KEY_LEN, t - kv_start); + strncpy(new_subst_pair->key, kv_start, klen); + new_subst_pair->key[klen] = 0; + + kv_start = t + 1; + + state = P_VALUE; + // pipe separates the include's filename and literal substitutions + // we know there is a closing } somewhere, and {@....| doesn't occur normally, so let's assume it's correct + } + } + } + else if (state == P_VALUE) { + if (c == '|' || c == '}') { + const size_t vlen = MIN(TPL_KV_SUBST_LEN, t - kv_start); + strncpy(new_subst_pair->subst, kv_start, vlen); + new_subst_pair->subst[vlen] = 0; + + // attach the kv pair to the list + SLIST_INSERT_HEAD(&substitutions_head, new_subst_pair, link); + ESP_LOGD(TAG, "Adding subs kv %s -> %s", new_subst_pair->key, new_subst_pair->subst); + new_subst_pair = NULL; + + kv_start = t + 1; // go past the pipe + state = P_KEY; + + if (t == closebr) { + break; // found the ending brace, so let's quit the kv parse loop + } + } + } + + t++; + } + // clean up after a messed up subs kv pairs syntax + if (new_subst_pair != NULL) { + free(new_subst_pair); + } + } + + if (!token_valid) { + // false match, include it in the block to send before the next token + searchpos = openbr + 1; + ESP_LOGD(TAG, "Skip invalid token near %10s", openbr); + continue; + } + + // now we know it looks like a substitution token + + // flush data before the token + if (pos != openbr) ESP_TRY(httpd_resp_send_chunk(r, pos, (size_t) (openbr - pos))); + + const char *token_start = openbr; + + tpl_escape_t escape = TPL_ESCAPE_NONE; + + // extract and terminate the token + size_t token_len = MIN(MAX_TOKEN_LEN-1, closebr - openbr - 1); + if (token_end) { + token_len = MIN(token_len, token_end - openbr - 1); + } + + if (is_include) { + token_start += 1; // skip the @ + token_len -= 1; + } else { + if (0 == strncmp("h:", openbr + 1, 2)) { + escape = TPL_ESCAPE_HTML; + token_start += 2; + token_len -= 2; + } + else if (0 == strncmp("j:", openbr + 1, 2)) { + escape = TPL_ESCAPE_JS; + token_start += 2; + token_len -= 2; + } + } + + strncpy(token_buf, token_start+1, token_len); + token_buf[token_len] = 0; + + ESP_LOGD(TAG, "Token: %s", token_buf); + + esp_err_t rv; + + if (is_include) { + ESP_LOGD(TAG, "Trying to include a sub-file"); + + const struct embedded_file_info *file = NULL; + rv = www_get_static_file(token_buf, FILE_ACCESS_PROTECTED, &file); + if (rv != ESP_OK) { + ESP_LOGE(TAG, "Failed to statically include \"%s\" in a template - %s", token_buf, esp_err_to_name(rv)); + // this will cause the token to be emitted verbatim + } else { + ESP_LOGD(TAG, "Descending..."); + + // combine the two replacers + struct stacked_replacer_context combo = { + .replacer0 = replacer, + .context0 = context, + .replacer1 = tpl_kv_replacer, + .context1 = &substitutions_head + }; + + rv = httpd_send_template_file_struct(r, file, stacked_replacer, &combo, HTOPT_INCLUDE); + ESP_LOGD(TAG, "...back in parent"); + } + + // tear down the list + tpl_kv_free(&substitutions_head); + + if (rv != ESP_OK) { + // just send it verbatim... + ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); + } + } else { + if (replacer) { + ESP_LOGD(TAG, "Running replacer for \"%s\" with escape %d", token_buf, escape); + rv = replacer(r, context, token_buf, escape); + + if (rv != ESP_OK) { + if (rv == ESP_ERR_NOT_FOUND) { + ESP_LOGD(TAG, "Token rejected"); + // optional token becomes empty string if not replaced + if (!token_is_optional) { + ESP_LOGD(TAG, "Not optional, keeping verbatim"); + // replacer rejected the token, keep it verbatim + ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); + } + } + else { + ESP_LOGE(TAG, "Unexpected error from replacer func: 0x%02x - %s", rv, esp_err_to_name(rv)); + return rv; + } + } + } else { + ESP_LOGD(TAG, "Not replacer"); + // no replacer, only includes - used for 'static' files + if (!token_is_optional) { + ESP_LOGD(TAG, "Token not optional, keeping verbatim"); + ESP_TRY(httpd_resp_send_chunk(r, openbr, (size_t) (closebr - openbr + 1))); + } + } + } + + searchpos = pos = closebr + 1; + } + + if (0 == (opts & HTOPT_NO_CLOSE)) { + ESP_TRY(httpd_resp_send_chunk(r, NULL, 0)); + } + + return ESP_OK; +} + + +esp_err_t tpl_kv_add_int(struct tpl_kv_list *head, const char *key, int32_t num) +{ + char buf[12]; + itoa(num, buf, 10); + return tpl_kv_add(head, key, buf); +} + +esp_err_t tpl_kv_add_long(struct tpl_kv_list *head, const char *key, int64_t num) +{ + char buf[21]; + sprintf(buf, "%"PRIi64, num); + return tpl_kv_add(head, key, buf); +} + +esp_err_t tpl_kv_add_ipv4str(struct tpl_kv_list *head, const char *key, uint32_t ip4h) +{ + char buf[IP4ADDR_STRLEN_MAX]; + ip4_addr_t addr; + addr.addr = lwip_htonl(ip4h); + ip4addr_ntoa_r(&addr, buf, IP4ADDR_STRLEN_MAX); + + return tpl_kv_add(head, key, buf); +} + +esp_err_t tpl_kv_add(struct tpl_kv_list *head, const char *key, const char *subst) +{ + ESP_LOGD(TAG, "kv add subs %s := %s", key, subst); + struct tpl_kv_entry *entry = calloc(sizeof(struct tpl_kv_entry), 1); + if (entry == NULL) return ESP_ERR_NO_MEM; + + assert(strlen(key) < TPL_KV_KEY_LEN); + assert(strlen(subst) < TPL_KV_SUBST_LEN); + + strncpy(entry->key, key, TPL_KV_KEY_LEN); + entry->key[TPL_KV_KEY_LEN - 1] = 0; + + strncpy(entry->subst, subst, TPL_KV_SUBST_LEN - 1); + entry->subst[TPL_KV_KEY_LEN - 1] = 0; + + SLIST_INSERT_HEAD(head, entry, link); + return ESP_OK; +} + +esp_err_t tpl_kv_sprintf(struct tpl_kv_list *head, const char *key, const char *format, ...) +{ + ESP_LOGD(TAG, "kv printf %s := %s", key, format); + struct tpl_kv_entry *entry = calloc(sizeof(struct tpl_kv_entry), 1); + if (entry == NULL) return ESP_ERR_NO_MEM; + + assert(strlen(key) < TPL_KV_KEY_LEN); + + strncpy(entry->key, key, TPL_KV_KEY_LEN); + entry->key[TPL_KV_KEY_LEN - 1] = 0; + + va_list list; + va_start(list, format); + vsnprintf(entry->subst, TPL_KV_SUBST_LEN, format, list); + va_end(list); + entry->subst[TPL_KV_KEY_LEN - 1] = 0; + + SLIST_INSERT_HEAD(head, entry, link); + return ESP_OK; +} + +void tpl_kv_free(struct tpl_kv_list *head) +{ + struct tpl_kv_entry *item, *next; + SLIST_FOREACH_SAFE(item, head, link, next) { + free(item); + } +} + +esp_err_t tpl_kv_send_as_ascii_map(httpd_req_t *req, struct tpl_kv_list *head) +{ + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + +#define BUF_CAP 512 + char *buf_head = calloc(BUF_CAP, 1); + if (!buf_head) { + ESP_LOGE(TAG, "Malloc err"); + return ESP_FAIL; + } + char *buf = buf_head; + size_t cap = BUF_CAP; + struct tpl_kv_entry *entry; + + // GCC nested function + esp_err_t send_part() { + esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap); + buf = buf_head; + cap = BUF_CAP; + if (suc != ESP_OK) { + ESP_LOGE(TAG, "Error sending buffer"); + free(buf_head); + httpd_resp_send_chunk(req, NULL, 0); + } + return suc; + } + + SLIST_FOREACH(entry, head, link) { + buf = append(buf, entry->key, &cap); + if (!buf) ESP_TRY(send_part()); + buf = append(buf, "\x1f", &cap); + if (!buf) ESP_TRY(send_part()); + buf = append(buf, entry->subst, &cap); + if (!buf) ESP_TRY(send_part()); + if (entry->link.sle_next) { + buf = append(buf, "\x1e", &cap); + if (!buf) ESP_TRY(send_part()); + } + } + // send leftovers + if (buf != buf_head) { + esp_err_t suc = httpd_resp_send_chunk(req, buf_head, BUF_CAP-cap); + if (suc != ESP_OK) { + ESP_LOGE(TAG, "Error sending buffer"); + } + } + + // Commit + httpd_resp_send_chunk(req, NULL, 0); + free(buf_head); + return ESP_OK; +} diff --git a/components/httpd_utils/CMakeLists.txt b/components/httpd_utils/CMakeLists.txt new file mode 100644 index 0000000..e5fe530 --- /dev/null +++ b/components/httpd_utils/CMakeLists.txt @@ -0,0 +1,9 @@ +set(COMPONENT_ADD_INCLUDEDIRS + "include") + +set(COMPONENT_SRCDIRS + "src") + +set(COMPONENT_REQUIRES tcpip_adapter esp_http_server common_utils) + +register_component() diff --git a/components/httpd_utils/README.txt b/components/httpd_utils/README.txt new file mode 100644 index 0000000..c3e034e --- /dev/null +++ b/components/httpd_utils/README.txt @@ -0,0 +1,4 @@ +Functions enriching the HTTP server bundled with ESP-IDF. +This package includes HTTP-related utilities, captive +portal implementation, and a cookie-based session system with a +key-value store and expirations. diff --git a/components/httpd_utils/component.mk b/components/httpd_utils/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/httpd_utils/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/httpd_utils/include/httpd_utils/captive.h b/components/httpd_utils/include/httpd_utils/captive.h new file mode 100644 index 0000000..d656b41 --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/captive.h @@ -0,0 +1,32 @@ +#ifndef HTTPD_UTILS_CAPTIVE_H +#define HTTPD_UTILS_CAPTIVE_H + +#include +#include + +/** + * Redirect if needed when a captive portal capture is detected + * + * @param r + * @return ESP_OK on redirect, ESP_ERR_NOT_FOUND if not needed, other error on failure + */ +esp_err_t httpd_captive_redirect(httpd_req_t *r); + +/** + * Get URL to redirect to. WEAK. + * + * @param r + * @param buf + * @param maxlen + * @return http(s)://foo.bar/ + */ +esp_err_t httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen); + +/** + * Get captive portal domain. WEAK. + * + * @return foo.bar + */ +const char * httpd_captive_redirect_get_domain(); + +#endif //HTTPD_UTILS_CAPTIVE_H diff --git a/components/httpd_utils/include/httpd_utils/fd_to_ipv4.h b/components/httpd_utils/include/httpd_utils/fd_to_ipv4.h new file mode 100644 index 0000000..6cb647d --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/fd_to_ipv4.h @@ -0,0 +1,13 @@ +#ifndef HTTPD_FDIPV4_H +#define HTTPD_FDIPV4_H + +/** + * Get IP address for a FD + * + * @param fd + * @param[out] ipv4 + * @return success + */ +esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4); + +#endif //HTTPD_FDIPV4_H diff --git a/components/httpd_utils/include/httpd_utils/redirect.h b/components/httpd_utils/include/httpd_utils/redirect.h new file mode 100644 index 0000000..77fdebc --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/redirect.h @@ -0,0 +1,16 @@ +#ifndef HTTPD_UTILS_REDIRECT_H +#define HTTPD_UTILS_REDIRECT_H + +#include +#include + +/** + * Redirect to other URI - sends a HTTP response from the http server + * + * @param r - request + * @param uri - target uri + * @return success + */ +esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri); + +#endif // HTTPD_UTILS_REDIRECT_H diff --git a/components/httpd_utils/include/httpd_utils/session.h b/components/httpd_utils/include/httpd_utils/session.h new file mode 100644 index 0000000..1ff409b --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/session.h @@ -0,0 +1,55 @@ +/** + * Session store system main include file + * + * Created on 2019/07/13. + */ + +#ifndef HTTPD_UTILS_SESSION_H +#define HTTPD_UTILS_SESSION_H + +#include "session_kvmap.h" +#include "session_store.h" + +// Customary keys + +/** Session key for OK flash message */ +#define SESS_FLASH_OK "flash_ok" +/** Session key for error flash message */ +#define SESS_FLASH_ERR "flash_err" +/** Session key for a "logged in" flag. Value is 1 if logged in. */ +#define SESS_AUTHED "authed" + +// .. + +/** + * Redirect to /login form if unauthed, + * but also retrieve the session key-value store for further use + */ +#define HTTP_GET_AUTHED_SESSION(kvstore, r) do { \ + kvstore = httpd_req_find_session_and((r), SESS_GET_DATA); \ + if (NULL == kvstore || NULL == sess_kv_map_get(kvstore, SESS_AUTHED)) { \ + return httpd_redirect_to((r), "/login"); \ + } \ +} while(0) + +/** + * Start or resume a session without checking for authed status. + * When started, the session cookie is added to the response immediately. + * + * @param[out] kvstore - a place to store the allocated or retrieved session kvmap + * @param[in] r - request + * @return success + */ +esp_err_t HTTP_GET_SESSION(sess_kv_map_t **kvstore, httpd_req_t *r); + +/** + * Redirect to the login form if unauthed. + * This is the same as `HTTP_GET_AUTHED_SESSION`, except the kvstore variable is + * not needed in the uri handler calling this, so it is declared internally. + */ +#define HTTP_REDIRECT_IF_UNAUTHED(r) do { \ + sess_kv_map_t *_kvstore; \ + HTTP_GET_AUTHED_SESSION(_kvstore, r); \ +} while(0) + +#endif // HTTPD_UTILS_SESSION_H diff --git a/components/httpd_utils/include/httpd_utils/session_kvmap.h b/components/httpd_utils/include/httpd_utils/session_kvmap.h new file mode 100644 index 0000000..e091bd5 --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/session_kvmap.h @@ -0,0 +1,77 @@ +/** + * Simple key-value map for session data storage. + * Takes care of dynamic allocation and cleanup. + * + * Created on 2019/01/28. + */ + +#ifndef SESSION_KVMAP_H +#define SESSION_KVMAP_H + +/** + * Prototype for a free() func to clean up session-held objects + */ +typedef void (*sess_kv_free_func_t)(void *obj); + +typedef struct sess_kv_map sess_kv_map_t; + +#define SESS_KVMAP_KEY_LEN 16 + +/** + * Allocate a new session key-value store + * + * @return the store, NULL on error + */ +sess_kv_map_t *sess_kv_map_alloc(void); + +/** + * Free the session kv store. + * + * @param head - store head + */ +void sess_kv_map_free(void *head); + +/** + * Get a value from the session kv store. + * + * @param head - store head + * @param key - key to get a value for + * @return the value, or NULL if not found + */ +void *sess_kv_map_get(sess_kv_map_t *head, const char *key); + +/** + * Get and remove a value from the session store. + * + * The free function is not called in this case and the recipient is + * responsible for cleaning it up correctly. + * + * @param head - store head + * @param key - key to get a value for + * @return the value, or NULL if not found + */ +void * sess_kv_map_take(sess_kv_map_t *head, const char *key); + +/** + * Remove an entry from the session by its key name. + * The slot is not free'd yet, but is made available for reuse. + * + * @param head - store head + * @param key - key to remove + * @return success + */ +esp_err_t sess_kv_map_remove(sess_kv_map_t *head, const char *key); + +/** + * Set a key value. If there is an old value stored, it will be freed by its free function and replaced by the new one. + * Otherwise a new slot is allocated for it, or a previously released one is reused. + * + * @param head - store head + * @param key - key to assign to + * @param value - new value + * @param free_fn - value free func + * @return success + */ +esp_err_t sess_kv_map_set(sess_kv_map_t *head, const char *key, void *value, sess_kv_free_func_t free_fn); + +#endif //SESSION_KVMAP_H diff --git a/components/httpd_utils/include/httpd_utils/session_store.h b/components/httpd_utils/include/httpd_utils/session_store.h new file mode 100644 index 0000000..2266944 --- /dev/null +++ b/components/httpd_utils/include/httpd_utils/session_store.h @@ -0,0 +1,100 @@ +/** + * Cookie-based session store + */ + +#ifndef SESSION_STORE_H +#define SESSION_STORE_H + +#include "esp_http_server.h" + +#define SESSION_EXPIRY_TIME_S 60*30 +#define SESSION_COOKIE_NAME "SESSID" + +/** function that frees a session data object */ +typedef void (*sess_data_free_fn_t)(void *); + +enum session_find_action { + SESS_DROP, SESS_GET_DATA +}; + +/** + * Find session and either get data, or drop it. + * + * @param cookie + * @param action + * @return + */ +void *session_find_and(const char *cookie, enum session_find_action action); + +/** + * Initialize the session store. + * Safely empty it if initialized + */ +void session_store_init(void); + +// placeholder for when no data is stored +#define SESSION_DUMMY ((void *) 1) + +/** + * Create a new session. Data must not be NULL, because it wouldn't be possible + * to distinguish between NULL value and session not found in return values. + * It can be e.g. 1 if no data storage is needed. + * + * @param data - data object to attach to the session + * @param free_fn - function that disposes of the data when the session expires + * @return NULL on error, or the new session ID. This is a live pointer into the session structure, + * must be copied if stored, as it can become invalid at any time + */ +const char *session_new(void *data, sess_data_free_fn_t free_fn); + +/** + * Find a session by it's ID (from a cookie) + * + * @param cookie - session ID string + * @return session data (void*), or NULL + */ +void *session_find(const char *cookie); + +/** + * Loop through all sessions and drop these that expired. + */ +void session_drop_expired(void); + +/** + * Drop a session by its ID. Does nothing if not found. + * + * @param cookie - session ID string + */ +void session_drop(const char *cookie); + +/** + * Parse the Cookie header from a request, and do something with the corresponding session. + * + * To also delete the cookie, use req_delete_session_cookie(r) + * + * @param r - request + * @param action - what to do with the session + * @return session data, NULL if removed or not found + */ +void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action); + +/** + * Add a header that deletes the session cookie + * + * @param r - request + */ +void httpd_resp_delete_session_cookie(httpd_req_t *r); + +/** + * Add a header that sets the session cookie. + * + * This must be called after creating a session (e.g. user logged in) to make it persistent. + * + * @attention NOT RE-ENTRANT, CAN'T BE USED AGAIN UNTIL THE REQUEST IT WAS CALLED FOR IS DISPATCHED. + * + * @param r - request + * @param cookie - cookie ID + */ +void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie); + +#endif //SESSION_STORE_H diff --git a/components/httpd_utils/src/captive.c b/components/httpd_utils/src/captive.c new file mode 100644 index 0000000..2be20d3 --- /dev/null +++ b/components/httpd_utils/src/captive.c @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include +#include + +#include "httpd_utils/captive.h" +#include "httpd_utils/fd_to_ipv4.h" +#include "httpd_utils/redirect.h" +#include + +static const char *TAG="captive"; + +const char * __attribute__((weak)) +httpd_captive_redirect_get_domain(void) +{ + return "fb_node.captive"; +} + +esp_err_t __attribute__((weak)) +httpd_captive_redirect_get_url(httpd_req_t *r, char *buf, size_t maxlen) +{ + buf = append(buf, "http://", &maxlen); + buf = append(buf, httpd_captive_redirect_get_domain(), &maxlen); + append(buf, "/", &maxlen); + + return ESP_OK; +} + +esp_err_t httpd_captive_redirect(httpd_req_t *r) +{ + // must be static to survive being used in the redirect header + static char s_buf[64]; + + wifi_mode_t mode = 0; + esp_wifi_get_mode(&mode); + + // Check if we have an softap interface. No point checking IPs and hostnames if the client can't be on AP. + if (mode == WIFI_MODE_STA || mode == WIFI_MODE_NULL) { + goto no_redirect; + } + + int fd = httpd_req_to_sockfd(r); + + tcpip_adapter_ip_info_t apip; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &apip); + + u32_t client_addr; + if(ESP_OK != fd_to_ipv4(fd, &client_addr)) { + return ESP_FAIL; + } + + ESP_LOGD(TAG, "[captive] Client addr = 0x%08x, ap addr 0x%08x, ap nmask 0x%08x", + client_addr, + apip.ip.addr, + apip.netmask.addr + ); + + // Check if client IP looks like from our AP dhcps + if ((client_addr & apip.netmask.addr) != (apip.ip.addr & apip.netmask.addr)) { + ESP_LOGD(TAG, "[captive] Client not in AP IP range"); + goto no_redirect; + } + + // Get requested hostname from the header + esp_err_t rv = httpd_req_get_hdr_value_str(r, "Host", s_buf, 64); + if (rv != ESP_OK) { + ESP_LOGW(TAG, "[captive] No host in request?"); + goto no_redirect; + } + + ESP_LOGD(TAG, "[captive] Candidate for redirect: %s%s", s_buf, r->uri); + + // Never redirect if host is an IP + if (strlen(s_buf)>7) { + bool isIP = 1; + for (int x = 0; x < strlen(s_buf); x++) { + if (s_buf[x] != '.' && (s_buf[x] < '0' || s_buf[x] > '9')) { + isIP = 0; + break; + } + } + + if (isIP) { + ESP_LOGD(TAG, "[captive] Access via IP, no redirect needed"); + goto no_redirect; + } + } + + // Redirect if host differs + // - this can be e.g. connectivitycheck.gstatic.com or the equivalent for ios + + if (0 != strcmp(s_buf, httpd_captive_redirect_get_domain())) { + ESP_LOGD(TAG, "[captive] Host differs, redirecting..."); + + httpd_captive_redirect_get_url(r, s_buf, 64); + return httpd_redirect_to(r, s_buf); + } else { + ESP_LOGD(TAG, "[captive] Host is OK"); + goto no_redirect; + } + + no_redirect: + return ESP_ERR_NOT_FOUND; +} diff --git a/components/httpd_utils/src/fd_to_ipv4.c b/components/httpd_utils/src/fd_to_ipv4.c new file mode 100644 index 0000000..7f6377f --- /dev/null +++ b/components/httpd_utils/src/fd_to_ipv4.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include + +#include "httpd_utils/fd_to_ipv4.h" + +static const char *TAG = "fd2ipv4"; + +/** + * Get IP address for a FD + * + * @param fd + * @param[out] ipv4 + * @return success + */ +esp_err_t fd_to_ipv4(int fd, in_addr_t *ipv4) +{ + struct sockaddr_in6 addr; + size_t len = sizeof(addr); + int rv = getpeername(fd, (struct sockaddr *) &addr, &len); + if (rv != 0) { + ESP_LOGE(TAG, "Failed to get IP addr for fd %d", fd); + return ESP_FAIL; + } + + uint32_t client_addr = 0; + if (addr.sin6_family == AF_INET6) { + // this would fail in a real ipv6 network + // with ipv4 the addr is simply in the last ipv6 byte + struct sockaddr_in6 *s = &addr; + client_addr = s->sin6_addr.un.u32_addr[3]; + } + else { + struct sockaddr_in *s = (struct sockaddr_in *) &addr; + client_addr = s->sin_addr.s_addr; + } + + *ipv4 = client_addr; + return ESP_OK; +} diff --git a/components/httpd_utils/src/redirect.c b/components/httpd_utils/src/redirect.c new file mode 100644 index 0000000..5a174df --- /dev/null +++ b/components/httpd_utils/src/redirect.c @@ -0,0 +1,20 @@ +#include +#include +#include +#include +#include + +#include "httpd_utils/redirect.h" + +static const char *TAG="redirect"; + +esp_err_t httpd_redirect_to(httpd_req_t *r, const char *uri) +{ + ESP_LOGD(TAG, "Redirect to %s", uri); + + httpd_resp_set_hdr(r, "Location", uri); + httpd_resp_set_status(r, "303 See Other"); + httpd_resp_set_type(r, HTTPD_TYPE_TEXT); + const char *msg = "Redirect"; + return httpd_resp_send(r, msg, -1); +} diff --git a/components/httpd_utils/src/session_kvmap.c b/components/httpd_utils/src/session_kvmap.c new file mode 100644 index 0000000..f96638f --- /dev/null +++ b/components/httpd_utils/src/session_kvmap.c @@ -0,0 +1,181 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include +#include +#include +#include "httpd_utils/session_kvmap.h" + +static const char *TAG = "sess_kvmap"; + +// this struct is opaque, a stub like this is sufficient for the head pointer. +struct sess_kv_entry; + +/** Session head structure, dynamically allocated */ +SLIST_HEAD(sess_kv_map, sess_kv_entry); + +struct sess_kv_entry { + SLIST_ENTRY(sess_kv_entry) link; + char key[SESS_KVMAP_KEY_LEN]; + void *value; + sess_kv_free_func_t free_fn; +}; + +struct sess_kv_map *sess_kv_map_alloc(void) +{ + ESP_LOGD(TAG, "kv store alloc"); + struct sess_kv_map *map = calloc(sizeof(struct sess_kv_map), 1); + assert(map); + SLIST_INIT(map); + return map; +} + +void sess_kv_map_free(void *head_v) +{ + struct sess_kv_map* head = head_v; + + ESP_LOGD(TAG, "kv store free"); + assert(head); + struct sess_kv_entry *item, *tmp; + SLIST_FOREACH_SAFE(item, head, link, tmp) { + if (item->free_fn) { + item->free_fn(item->value); + free(item); + } + } + free(head); +} + + +void * sess_kv_map_get(struct sess_kv_map *head, const char *key) +{ + assert(head); + assert(key); + ESP_LOGD(TAG, "kv store get %s", key); + + struct sess_kv_entry *item; + SLIST_FOREACH(item, head, link) { + if (0==strcmp(item->key, key)) { + ESP_LOGD(TAG, "got ok"); + return item->value; + } + } + + ESP_LOGD(TAG, "not found in store"); + return NULL; +} + +void * sess_kv_map_take(struct sess_kv_map *head, const char *key) +{ + assert(head); + assert(key); + ESP_LOGD(TAG, "kv store take %s", key); + + struct sess_kv_entry *item; + SLIST_FOREACH(item, head, link) { + if (0==strcmp(item->key, key)) { + item->key[0] = 0; + item->free_fn = NULL; + ESP_LOGD(TAG, "taken ok"); + return item->value; + } + } + + ESP_LOGD(TAG, "not found in store"); + return NULL; +} + +esp_err_t sess_kv_map_remove(struct sess_kv_map *head, const char *key) +{ + assert(head); + assert(key); + ESP_LOGD(TAG, "kv store remove %s", key); + + struct sess_kv_entry *item; + SLIST_FOREACH(item, head, link) { + if (0==strcmp(item->key, key)) { + if (item->free_fn) { + item->free_fn(item->value); + } + item->key[0] = 0; + item->value = NULL; + item->free_fn = NULL; + return ESP_OK; + } + } + + ESP_LOGD(TAG, "couldn't remove, not found: %s", key); + return ESP_ERR_NOT_FOUND; +} + + +esp_err_t sess_kv_map_set(struct sess_kv_map *head, const char *key, void *value, sess_kv_free_func_t free_fn) +{ + assert(head); + assert(key); + ESP_LOGD(TAG, "kv set value for key %s", key); + + size_t key_len = strlen(key); + if (key_len > SESS_KVMAP_KEY_LEN-1) { + ESP_LOGE(TAG, "Key too long: %s", key); + // discard illegal key + return ESP_FAIL; + } + + if (key_len == 0) { + ESP_LOGE(TAG, "Key too short: \"%s\"", key); + // discard illegal key + return ESP_FAIL; + } + + struct sess_kv_entry *item = NULL; + struct sess_kv_entry *empty_item = NULL; // found item with no content + SLIST_FOREACH(item, head, link) { + ESP_LOGD(TAG, "test item with key %s, ptr %p > %p", item->key, item, item->link.sle_next); + if (0 == item->key[0]) { + ESP_LOGD(TAG, "found an empty slot"); + empty_item = item; + } + else if (0==strcmp(item->key, key)) { + ESP_LOGD(TAG, "old value replaced"); + if (item->free_fn) { + item->free_fn(item->value); + } + item->value = value; + item->free_fn = free_fn; + return ESP_OK; + } else { + ESP_LOGD(TAG, "skip this one"); + } + } + + struct sess_kv_entry *new_item = NULL; + + // insert new or reuse an empty item + if (empty_item) { + new_item = empty_item; + ESP_LOGD(TAG, "empty item reused (%p)", new_item); + } else { + ESP_LOGD(TAG, "alloc new item"); + // key not found, add a new entry. + new_item = calloc(sizeof(struct sess_kv_entry), 1); + if (!new_item) { + ESP_LOGE(TAG, "New entry alloc failed"); + return ESP_ERR_NO_MEM; + } + } + + strcpy(new_item->key, key); + new_item->free_fn = free_fn; + new_item->value = value; + + if (!empty_item) { + ESP_LOGD(TAG, "insert new item into list"); + // this item was malloc'd + SLIST_INSERT_HEAD(head, new_item, link); + } + + return ESP_OK; +} diff --git a/components/httpd_utils/src/session_store.c b/components/httpd_utils/src/session_store.c new file mode 100644 index 0000000..3e00cef --- /dev/null +++ b/components/httpd_utils/src/session_store.c @@ -0,0 +1,220 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include +#include +#include + +#include "httpd_utils/session_store.h" + +// TODO add a limit on simultaneously open sessions (can cause memory exhaustion DoS) + +#define COOKIE_LEN 32 +static const char *TAG = "session"; + +struct session { + char cookie[COOKIE_LEN + 1]; + void *data; + TickType_t last_activity_time; + LIST_ENTRY(session) link; + sess_data_free_fn_t free_fn; +}; + +static LIST_HEAD(sessions_, session) s_store; + +static SemaphoreHandle_t sess_store_lock = NULL; +static bool sess_store_inited = false; + + +void session_store_init(void) +{ + if (sess_store_inited) { + xSemaphoreTake(sess_store_lock, portMAX_DELAY); + { + struct session *it, *tit; + LIST_FOREACH_SAFE(it, &s_store, link, tit) { + ESP_LOGW(TAG, "Session cookie expired: \"%s\"", it->cookie); + if (it->free_fn) it->free_fn(it->data); + // no relink, we dont care if the list breaks after this - we're removing all of it + free(it); + } + } + LIST_INIT(&s_store); + xSemaphoreGive(sess_store_lock); + } else { + LIST_INIT(&s_store); + sess_store_lock = xSemaphoreCreateMutex(); + sess_store_inited = true; + } +} + +/** + * Fill buffer with base64 symbols. Does not add a trailing null byte + * + * @param buf + * @param len + */ +static void esp_fill_random_alnum(char *buf, size_t len) +{ +#define alphabet_len 64 + static const char alphabet[alphabet_len] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"; + + unsigned int seed = xTaskGetTickCount(); + + assert(buf != NULL); + for (int i = 0; i < len; i++) { + int index = rand_r(&seed) % alphabet_len; + *buf++ = (uint8_t) alphabet[index]; + } +} + +const char *session_new(void *data, sess_data_free_fn_t free_fn) +{ + assert(data != NULL); + + struct session *item = calloc(sizeof(struct session), 1); + if (item == NULL) return NULL; + + item->data = data; + item->free_fn = free_fn; + esp_fill_random_alnum(item->cookie, COOKIE_LEN); + item->cookie[COOKIE_LEN] = 0; // add the terminator + + xSemaphoreTake(sess_store_lock, portMAX_DELAY); + { + item->last_activity_time = xTaskGetTickCount(); + + LIST_INSERT_HEAD(&s_store, item, link); + } + xSemaphoreGive(sess_store_lock); + + ESP_LOGD(TAG, "New HTTP session: %s", item->cookie); + + return item->cookie; +} + +void *session_find_and(const char *cookie, enum session_find_action action) +{ + // no point in searching if the length is wrong + if (strlen(cookie) != COOKIE_LEN) { + ESP_LOGW(TAG, "Wrong session cookie length: \"%s\"", cookie); + return NULL; + } + + struct session *it = NULL; + + bool found = false; + xSemaphoreTake(sess_store_lock, portMAX_DELAY); + { + LIST_FOREACH(it, &s_store, link) { + if (0==strcmp(it->cookie, cookie)) { + ESP_LOGD(TAG, "Session cookie matched: \"%s\"", cookie); + + it->last_activity_time = xTaskGetTickCount(); + found = true; + break; + } + } + if (found && action == SESS_DROP) { + if (it->free_fn) it->free_fn(it->data); + LIST_REMOVE(it, link); + free(it); + ESP_LOGD(TAG, "Dropped session: \"%s\"", cookie); + } + } + xSemaphoreGive(sess_store_lock); + if (found) { + if (action == SESS_DROP) { + // it was dropped inside the guarded block + // the return value is not used with DROP + return NULL; + } + else if(action == SESS_GET_DATA) { + return it->data; + } + } + + ESP_LOGW(TAG, "Session cookie not found: \"%s\"", cookie); + return NULL; +} + +void *session_find(const char *cookie) +{ + return session_find_and(cookie, SESS_GET_DATA); +} + +void session_drop(const char *cookie) +{ + session_find_and(cookie, SESS_DROP); +} + +void session_drop_expired(void) +{ + struct session *it; + struct session *tit; + + xSemaphoreTake(sess_store_lock, portMAX_DELAY); + { + TickType_t now = xTaskGetTickCount(); + + LIST_FOREACH_SAFE(it, &s_store, link, tit) { + TickType_t elapsed = now - it->last_activity_time; + if (elapsed > pdMS_TO_TICKS(SESSION_EXPIRY_TIME_S*1000)) { + ESP_LOGD(TAG, "Session cookie expired: \"%s\"", it->cookie); + if (it->free_fn) it->free_fn(it->data); + LIST_REMOVE(it, link); + free(it); + } + } + } + xSemaphoreGive(sess_store_lock); +} + + +void *httpd_req_find_session_and(httpd_req_t *r, enum session_find_action action) +{ + // this could be called periodically, but it's sufficient to run it at each request + // it won't slow anything down unless there are hundreds of sessions + session_drop_expired(); + + static char buf[256]; + esp_err_t rv = httpd_req_get_hdr_value_str(r, "Cookie", buf, 256); + if (rv == ESP_OK || rv == ESP_ERR_HTTPD_RESULT_TRUNC) { + ESP_LOGD(TAG, "Cookie header: %s", buf); + + // probably OK, see if we have a cookie + char *start = strstr(buf, SESSION_COOKIE_NAME"="); + if (start != 0) { + start += strlen(SESSION_COOKIE_NAME"="); + char *end = strchr(start, ';'); + if (end != NULL) *end = 0; + + ESP_LOGD(TAG, "Cookie is: %s", start); + return session_find_and(start, action); + } + } else { + ESP_LOGD(TAG, "No cookie."); + } + + return NULL; +} + +void httpd_resp_delete_session_cookie(httpd_req_t *r) +{ + httpd_resp_set_hdr(r, "Set-Cookie", SESSION_COOKIE_NAME"="); +} + + +// Static because the value is passed and stored by reference, so it wouldn't live long enough if it was on stack, +// and there also isn't any hook for freeing it if we used malloc(). This is an SDK bug. +static char cookie_hdr_buf[COOKIE_LEN + 10]; + +// !!! this must not be called concurrently from a different thread. +// That is no problem so long as the server stays single-threaded +void httpd_resp_set_session_cookie(httpd_req_t *r, const char *cookie) +{ + snprintf(cookie_hdr_buf, COOKIE_LEN + 10, "SESSID=%s", cookie); + httpd_resp_set_hdr(r, "Set-Cookie", cookie_hdr_buf); +} diff --git a/components/httpd_utils/src/session_utils.c b/components/httpd_utils/src/session_utils.c new file mode 100644 index 0000000..c1ef068 --- /dev/null +++ b/components/httpd_utils/src/session_utils.c @@ -0,0 +1,41 @@ +/** + * TODO file description + * + * Created on 2019/07/13. + */ + +#ifndef SESSION_UTILS_C_H +#define SESSION_UTILS_C_H + +#include +#include +#include "httpd_utils/session_kvmap.h" +#include "httpd_utils/session_store.h" + +/** + * Start or resume a session. + */ +esp_err_t HTTP_GET_SESSION(sess_kv_map_t **ppkvstore, httpd_req_t *r) +{ + sess_kv_map_t *kvstore; + kvstore = httpd_req_find_session_and((r), SESS_GET_DATA); + if (NULL == kvstore) { + kvstore = sess_kv_map_alloc(); + if (!kvstore) return ESP_ERR_NO_MEM; + + const char *cookie = session_new(kvstore, sess_kv_map_free); + if (cookie == NULL) { + // session alloc failed + sess_kv_map_free(kvstore); + *ppkvstore = NULL; + return ESP_ERR_NO_MEM; + } + httpd_resp_set_session_cookie(r, cookie); + } + + *ppkvstore = kvstore; + return ESP_OK; +} + + +#endif //SESSION_UTILS_C_H diff --git a/components/ping/CMakeLists.txt b/components/ping/CMakeLists.txt new file mode 100644 index 0000000..263e1c0 --- /dev/null +++ b/components/ping/CMakeLists.txt @@ -0,0 +1,4 @@ +set(COMPONENT_ADD_INCLUDEDIRS include) +set(COMPONENT_SRCS "src/ping.c") + +register_component() diff --git a/components/ping/README.txt b/components/ping/README.txt new file mode 100644 index 0000000..154891c --- /dev/null +++ b/components/ping/README.txt @@ -0,0 +1 @@ +ICMP ping implementation for connectivity testing diff --git a/components/ping/component.mk b/components/ping/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/ping/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/ping/include/ping.h b/components/ping/include/ping.h new file mode 100644 index 0000000..f47c2de --- /dev/null +++ b/components/ping/include/ping.h @@ -0,0 +1,58 @@ +/** + * Ping library, used to test connectivity + */ + +#ifndef _PING_H +#define _PING_H + +#include "lwip/ip.h" +#include "sdkconfig.h" + +typedef void (*ping_success_print_cb_t)(int bytes, const char *ip, int seq, int elapsed_ms); +typedef void (*ping_fail_print_cb_t)(int seq); + +/** Ping options */ +typedef struct { + ip4_addr_t ip_addr; // dest IP addr + uint16_t count; // number of requests to send + uint16_t interval_ms; // delay between requests + uint16_t payload_size; // extra payload size + uint16_t timeout_ms; // ping timeout in ms + ping_success_print_cb_t success_cb; + ping_fail_print_cb_t fail_cb; +} ping_opts_t; + +/** Ping response struct */ +typedef struct { + int sockfd; // fd of the used socket, may be closed externally to abort the operation + uint16_t sent; // number of sent echo requests + uint16_t received; // number of received responses + uint16_t min_time_ms; // shortest ping + uint16_t max_time_ms; // longest ping + float loss_pt; // loss ratio in percent +} ping_result_t; + +/** init all except the ip addr */ +#define PING_CONFIG_DEFAULT() { \ + .count = 5, \ + .interval_ms = 1000, \ + .payload_size = 32, \ + .timeout_ms = 1000, \ + .success_cb = NULL, \ + .fail_cb = NULL, \ +} + +/** + * Send a ping request to a remote server. + * Parameters, including the IPv4 address, are specified in the config struct. + * + * Ping is blocking. The operation may be interrupted by closing the socket from another task, + * its FD is exposed from the beginning in result->sockfd. + * + * @param opts + * @param result - response struct, required, for storing statistics + * @return success or error + */ +esp_err_t ping(const ping_opts_t *opts, ping_result_t *result); + +#endif //_PING_H diff --git a/components/ping/src/ping.c b/components/ping/src/ping.c new file mode 100644 index 0000000..4568138 --- /dev/null +++ b/components/ping/src/ping.c @@ -0,0 +1,262 @@ +// based on https://github.com/pbecchi/ESP32_ping/blob/master/Ping.cpp + +#include + +#include "ping.h" + +#include "esp_log.h" + +#include "lwip/inet_chksum.h" +#include "lwip/ip.h" +#include "lwip/ip4.h" +#include "lwip/err.h" +#include "lwip/icmp.h" +#include "lwip/sockets.h" +#include "lwip/sys.h" +#include "lwip/netdb.h" +#include "lwip/dns.h" + +static const char *TAG = "ping"; + +typedef struct { + ping_opts_t config; + + int sockfd; + uint16_t ping_seq_num; // sequence number for the next packet + uint16_t transmitted; // sent requests + uint16_t received; // received responses + uint16_t min_time_ms; + uint16_t max_time_ms; + uint16_t last_delay_ms; +} ping_session_t; + +#define PING_ID 0xABCD + +static void ping_prepare_echo(ping_session_t *session, struct icmp_echo_hdr *echohdr) +{ + const size_t hdr_len = sizeof(struct icmp_echo_hdr); + const size_t payload_len = session->config.payload_size; + + ICMPH_TYPE_SET(echohdr, ICMP_ECHO); // compatibility alias + ICMPH_CODE_SET(echohdr, 0); + echohdr->chksum = 0; + echohdr->id = PING_ID; + echohdr->seqno = htons(++session->ping_seq_num); + + // the packet is longer than the header, it was malloc'd with extra space + // at the end for the payload + + /* fill the rest of the buffer with dummy data */ + for (size_t i = 0; i < payload_len; i++) { + ((char *) echohdr)[hdr_len + i] = (char) (' ' + i); + } + + echohdr->chksum = inet_chksum(echohdr, (u16_t) (payload_len + hdr_len)); +} + +static err_t ping_send(ping_session_t *session) +{ + struct icmp_echo_hdr *echohdr; // we allocate a larger buffer to also fit a payload at the end + struct sockaddr_in addr_to; + const size_t packet_size = sizeof(struct icmp_echo_hdr) + session->config.payload_size; + + ESP_LOGD(TAG, "Send ICMP ECHO req to %s", ip4addr_ntoa(&session->config.ip_addr)); + + echohdr = (struct icmp_echo_hdr *) mem_malloc((mem_size_t) packet_size); + if (!echohdr) { + return ERR_MEM; + } + + ping_prepare_echo(session, echohdr); + + addr_to.sin_len = sizeof(addr_to); + addr_to.sin_family = AF_INET; + addr_to.sin_addr.s_addr = session->config.ip_addr.addr; // ? + + int ret = sendto(session->sockfd, echohdr, packet_size, 0, (struct sockaddr *) &addr_to, sizeof(addr_to)); + if (ret <= 0) { + ESP_LOGE(TAG, "ping sendto err %d", ret); + } + else { + session->transmitted++; + } + + free(echohdr); + return (ret > 0 ? ERR_OK : ERR_VAL); +} + +static void ping_recv(ping_session_t *session) +{ + char rxbuf[64]; + int len; + struct sockaddr_in addr_from; + struct ip_hdr *iphdr; + struct icmp_echo_hdr *echohdr = NULL; + struct timeval begin, end; + uint64_t micros_begin, micros_end, elapsed_ms; + + socklen_t fromlen = sizeof(struct sockaddr_in); + + // Register begin time + gettimeofday(&begin, NULL); // FIXME this will fail if they are in different days + + // Receive a response limit size to recv buffer - leftovers will be collected and discarded + while (0 < (len = recvfrom(session->sockfd, rxbuf, sizeof(rxbuf), 0, (struct sockaddr *) &addr_from, &fromlen))) { + if (len >= (int) (sizeof(struct ip_hdr) + sizeof(struct icmp_echo_hdr))) { + // Register end time + gettimeofday(&end, NULL); + + /// Get from IP address + ip4_addr_t fromaddr; + fromaddr.addr = addr_from.sin_addr.s_addr; // ??? + + // Get echo + iphdr = (struct ip_hdr *) rxbuf; + echohdr = (struct icmp_echo_hdr *) (rxbuf + (IPH_HL(iphdr) * 4)); + + // Print .... + if ((echohdr->id == PING_ID) && (echohdr->seqno == htons(session->ping_seq_num))) { + session->received++; + + // Get elapsed time in milliseconds + micros_begin = (uint64_t) begin.tv_sec * 1000000; + micros_begin += begin.tv_usec; + + micros_end = (uint64_t) end.tv_sec * 1000000; + micros_end += end.tv_usec; + + elapsed_ms = (micros_end - micros_begin) / 1000; + + session->last_delay_ms = (uint16_t) elapsed_ms; + + // Update statistics + if (elapsed_ms < session->min_time_ms) { + session->min_time_ms = (uint16_t) elapsed_ms; + } + + if (elapsed_ms > session->max_time_ms) { + session->max_time_ms = (uint16_t) elapsed_ms; + } + + // Print ... + + int seq = ntohs(echohdr->seqno); + const char *ipa = ip4addr_ntoa(&fromaddr); + + ESP_LOGD(TAG, "Rx %d bytes from %s: icmp_seq=%d time=%d ms", len, ipa, seq, (int) elapsed_ms); + + if (session->config.success_cb) { + session->config.success_cb(len, ipa, seq, (int) elapsed_ms); + } + + return; + } + else { + // junk, ignore + ESP_LOGD(TAG, "Rx %d bytes from %s: junk", len, ip4addr_ntoa(&fromaddr)); + } + } + } + + session->last_delay_ms = 0; + + if (len < 0) { + if (session->config.fail_cb) { + session->config.fail_cb(session->ping_seq_num); + } + + ESP_LOGW(TAG, "Request timeout for icmp_seq %d", session->ping_seq_num); + } +} + +esp_err_t ping(const ping_opts_t *opts, ping_result_t *result) +{ + ping_session_t session = { + .min_time_ms = UINT16_MAX, + }; + + if (opts == NULL) { + ESP_LOGE(TAG, "opts arg is null"); + return ESP_ERR_INVALID_ARG; + } + + if (result == NULL) { + ESP_LOGE(TAG, "result arg is null"); + return ESP_ERR_INVALID_ARG; + } + + if (opts->count == 0) { + ESP_LOGE(TAG, "ping count must be > 0"); + } + + memcpy(&session.config, opts, sizeof(ping_opts_t)); + memset(result, 0, sizeof(ping_result_t)); + + // Create socket + if ((session.sockfd = socket(AF_INET, SOCK_RAW, IP_PROTO_ICMP)) < 0) { + ESP_LOGE(TAG, "fail to open socket for ping"); + return ESP_FAIL; + } + + result->sockfd = session.sockfd; + + // Setup socket + struct timeval tout; + tout.tv_sec = opts->timeout_ms / 1000; + tout.tv_usec = (opts->timeout_ms % 1000) * 1000; + + if (setsockopt(session.sockfd, SOL_SOCKET, SO_RCVTIMEO, &tout, sizeof(tout)) < 0) { + closesocket(session.sockfd); + session.sockfd = -1; + result->sockfd = -1; + ESP_LOGE(TAG, "fail to set ping socket rx timeout"); + return ESP_FAIL; + } + + if (setsockopt(session.sockfd, SOL_SOCKET, SO_SNDTIMEO, &tout, sizeof(tout)) < 0) { + closesocket(session.sockfd); + session.sockfd = -1; + result->sockfd = -1; + ESP_LOGE(TAG, "fail to set ping socket tx timeout"); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "Pinging %s: %d data bytes", ip4addr_ntoa(&opts->ip_addr), opts->payload_size); + + while (session.ping_seq_num < opts->count) { + if (ping_send(&session) == ERR_OK) { + ping_recv(&session); + } + if (session.ping_seq_num < opts->count) { + // subtract the wait time from the requested wait interval + int wait_time = opts->interval_ms - session.last_delay_ms; + if (wait_time >= 0) { // if 0, just yields + vTaskDelay(wait_time / portTICK_PERIOD_MS); + } + } + } + + if (session.sockfd > 0) { + closesocket(session.sockfd); + session.sockfd = -1; + result->sockfd = -1; + } + + result->sent = session.transmitted; + result->received = session.received; + result->min_time_ms = session.min_time_ms; + result->max_time_ms = session.max_time_ms; + result->loss_pt = (float) (( + ((float) session.transmitted - (float) session.received) + / (float) session.transmitted + ) * 100.0); + + ESP_LOGD(TAG, "%d tx, %d rx, %.1f%% loss, latency min %d ms, max %d ms", + result->sent, + result->received, + result->loss_pt, + result->min_time_ms, + result->max_time_ms); + + return ESP_OK; +} diff --git a/components/socket_server/CMakeLists.txt b/components/socket_server/CMakeLists.txt new file mode 100644 index 0000000..7176542 --- /dev/null +++ b/components/socket_server/CMakeLists.txt @@ -0,0 +1,4 @@ +set(COMPONENT_ADD_INCLUDEDIRS include) +set(COMPONENT_SRCS "src/socket_server.c") + +register_component() diff --git a/components/socket_server/README.txt b/components/socket_server/README.txt new file mode 100644 index 0000000..68c3ac6 --- /dev/null +++ b/components/socket_server/README.txt @@ -0,0 +1 @@ +Generic TCP socket server that can be used e.g. for telnet diff --git a/components/socket_server/component.mk b/components/socket_server/component.mk new file mode 100644 index 0000000..87ae05a --- /dev/null +++ b/components/socket_server/component.mk @@ -0,0 +1,3 @@ + +COMPONENT_SRCDIRS := src +COMPONENT_ADD_INCLUDEDIRS := include diff --git a/components/socket_server/include/socket_server.h b/components/socket_server/include/socket_server.h new file mode 100644 index 0000000..d5f97db --- /dev/null +++ b/components/socket_server/include/socket_server.h @@ -0,0 +1,282 @@ +/** + * Generic implementation of a TCP socket server. + */ + +#ifndef _SOCKET_SERVER_H_ +#define _SOCKET_SERVER_H_ + +#include +#include +#include +#include +#include + +#ifndef ESP_PLATFORM +#define ESP_OK 0 /*!< esp_err_t value indicating success (no error) */ +#define ESP_FAIL (-1) /*!< Generic esp_err_t code indicating failure */ +#define ESP_ERR_NO_MEM 0x101 /*!< Out of memory */ +#define ESP_ERR_INVALID_ARG 0x102 /*!< Invalid argument */ +#define ESP_ERR_INVALID_STATE 0x103 /*!< Invalid state */ +#define ESP_ERR_INVALID_SIZE 0x104 /*!< Invalid size */ +#define ESP_ERR_NOT_FOUND 0x105 /*!< Requested resource not found */ +#define ESP_ERR_NOT_SUPPORTED 0x106 /*!< Operation or feature not supported */ +#define ESP_ERR_TIMEOUT 0x107 /*!< Operation timed out */ +#else +#include +#include "sdkconfig.h" +#endif + +typedef struct sockd_server *Tcpd_t; +typedef struct sockd_client *TcpdClient_t; +typedef int tcpd_err_t; + +#define FD_NONE (-1) + +/** + * Socket read handler + */ +typedef tcpd_err_t (*tcpd_read_fn_t)(Tcpd_t serv, TcpdClient_t client, int sockfd); + +/** + * Socket open handler + */ +typedef tcpd_err_t (*tcpd_open_fn_t)(Tcpd_t serv, TcpdClient_t client); + +/** + * Socket close handler + */ +typedef tcpd_err_t (*tcpd_close_fn_t)(Tcpd_t serv, TcpdClient_t client); + +/** + * Function called during server shutdown to free the server context + */ +typedef void (*sockd_sctx_free_fn_t)(void * sctx); + +/** + * Server config structure + */ +typedef struct tcpd_config { + uint16_t port; //!< Server port + uint16_t max_clients; //!< Max number of connected clients + bool close_lru; //!< Close the least recently used client when a new connection is received + bool start_immediately; //!< If true, start the server immediately after init, otherwise it starts paused + + void *sctx; //!< Server context (arbitrary user data accessible from the callbacks) + sockd_sctx_free_fn_t sctx_free_fn; //!< Context freeing function (no-op if NULL) + + tcpd_read_fn_t read_fn; //!< Callback to read data from a socket. + tcpd_open_fn_t open_fn; //!< Callback to init a new client connection. Can set the client tag or handle. + tcpd_close_fn_t close_fn; //!< Callback when a client left or is kicked. Can free the client context. + + const char *task_name; //!< Server task name + uint32_t task_stack; //!< Server stack size + uint8_t task_prio; //!< Server priority +} tcpd_config_t; + +#define TCPD_INIT_DEFAULT() \ +{ \ + .port = 23, \ + .max_clients = 1, \ + .close_lru = true, \ + .start_immediately = true, \ + \ + .sctx = NULL, \ + .sctx_free_fn = NULL, \ + \ + .read_fn = NULL, \ + .open_fn = NULL, \ + .close_fn = NULL, \ + \ + .task_name = "socksrv", \ + .task_stack = 2048, \ + .task_prio = 3, \ +} + +struct tcpd_client_iter { + Tcpd_t server; + uint16_t next; +}; + +/** Initializer for the client iterator */ +tcpd_err_t tcpd_iter_init(struct tcpd_client_iter *iter, Tcpd_t serv); + +/** Iterate active clients. Returns NULL if no more clients were found. */ +TcpdClient_t tcpd_client_iter_next(struct tcpd_client_iter *iterator); + +/** + * Get server context. The context was defined in the config object. + * + * @param serv - server handle + * @return server context + */ +void *tcpd_get_server_ctx(Tcpd_t serv); + +/** + * Get client context, set by socksrv_set_client_ctx() + * + * @param client - client handle + * @return context object + */ +void *tcpd_get_client_ctx(TcpdClient_t client); + +/** + * Set client context. If allocated, it should be freed by the client close function. + * + * @param client - client handle + * @param cctx - context object + */ +void tcpd_set_client_ctx(TcpdClient_t client, void *cctx); + +/** + * Get client tag. + * + * @param client - client handle + * @return tag value + */ +uint32_t tcpd_get_client_tag(TcpdClient_t client); + +/** + * Set client tag. Tag may be used alongside the client context e.g. to distinguish + * context type. + * + * @param client - client handle + * @param tag - tag value + */ +void tcpd_set_client_tag(TcpdClient_t client, uint32_t tag); + +/** + * Get client IP address + * + * @param client - client + * @return address struct or NULL + */ +const struct sockaddr_in * tcpd_get_client_addr(TcpdClient_t client); + +/** + * Get client FD + * + * @param client + * @return fd + */ +int tcpd_get_client_fd(TcpdClient_t client); + +/** + * Kick a single client. + * This may be called even when the server is stopped. + * + * The client handle should be considered invalid after this call, + * as it may be reused for another incoming connection. + * Set it to NULL for safety. + * + * @param client - client handle (obtained e.g. as an argument in the receive function) + */ +void tcpd_kick(TcpdClient_t client); + +/** + * Kick all connected clients. + * This may be called even when the server is stopped. + * + * @param serv - server handle + */ +void tcpd_kick_all(Tcpd_t serv, bool with_injected); + +/* Kick clients with tag. Returns kicked count, or -1 on err */ +int tcpd_kick_by_tag(Tcpd_t serv, uint32_t tag); + +/** Kick clients with a given IP. Returns kicked count, or -1 on err */ +int tcpd_kick_by_ip(Tcpd_t serv, const struct in_addr *addr); + +/** + * Inject a client with a custom FD (e.g. STDIN, other UART socket...). + * + * Injecting STDIN will automatically use STDOUT for outgoing messages. + * + * @param server + * @param fd + * @return the client, NULL on failure + */ +TcpdClient_t tcpd_inject_client(Tcpd_t server, int fd); + +/** + * Initialize and start the socket server. + * + * @param config - config struct (will be copied into the server handle, can be only on stack) + * @param handle - pointer where to store the server handle. + * @return success + */ +tcpd_err_t tcpd_init(const tcpd_config_t *config, Tcpd_t *handle); + +/** + * Shutdown the server, close open sockets and free all allocated memory. + * Client contexts can be freed in the close_fn, if it was defined in server config. + * It will be called for all still open sockets. + * + * The server context will be freed using the user-provided free function (set in config) + * + * The server handle should be considered invalid after calling this function. + * The same applies to all existing client handles. Set it to NULL for safety. + * + * @param serv - server handle + */ +void tcpd_shutdown(Tcpd_t serv); + +/** + * Stop the server loop. It won't accept any connection requests nor data. + * A stopped server can be resumed again. + * + * Does nothing if the server is already stopped. + * + * @param serv - server handle + */ +void tcpd_suspend(Tcpd_t serv); + +/** + * Start the server after it has been stopped. + * + * Does nothing if the server is already running. + * + * The server runs immediately after init, + * so this does not need to be called to start it. + * + * @param serv - server handle + */ +void tcpd_resume(Tcpd_t serv); + +/** + * Send data to all connected clients + * + * @param serv - server handle + * @param buffer - data to send + * @param len - data length; if negative, treat data as a string and use strlen() + */ +tcpd_err_t tcpd_broadcast(Tcpd_t serv, const uint8_t *data, ssize_t len); + +/** + * Send a message to a single client + * + * @param client - client handle + * @param data - bytes to send + * @param len - length or -1 for strlen + * @return success + */ +tcpd_err_t tcpd_send(TcpdClient_t client, const uint8_t *data, ssize_t len); + +/** + * Get client slot by FD. Returns NULL if not found. + * + * @param serv - server struct + * @param sockfd + * @return + */ +TcpdClient_t tcpd_client_by_fd(Tcpd_t serv, int sockfd); + +/** + * Get client by tag. Returns NULL if not found. + * + * @param serv - server struct + * @param sockfd + * @return + */ +TcpdClient_t tcpd_client_by_tag(Tcpd_t serv, uint32_t tag); + +#endif //_SOCKET_SERVER_H_ diff --git a/components/socket_server/src/socket_server.c b/components/socket_server/src/socket_server.c new file mode 100644 index 0000000..3d20190 --- /dev/null +++ b/components/socket_server/src/socket_server.c @@ -0,0 +1,1015 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include + +#ifndef ESP_PLATFORM + +/* Posix with libcsp */ + +#include +#include +#define ThreadHandle csp_thread_handle_t +#define Mutex csp_mutex_t +#define mutex_lock(mutex, timeout) csp_mutex_lock(&(mutex), (timeout)) +#define mutex_unlock(mutex) csp_mutex_unlock(&(mutex)) +#define mutex_remove(mutex) csp_mutex_remove(&(mutex)) +#define DEFINE_TASK(name) CSP_DEFINE_TASK(name) +#define thread_exit() do { csp_thread_exit(); return NULL; } while(0) +#define port_calloc(a,b) csp_calloc((a),(b)) +#define port_free(p) csp_free(p) +#define port_thread_create(routine, thread_name, stack_size, parameters, priority, return_handle) \ + csp_thread_create(routine, thread_name, stack_size, parameters, priority, return_handle) +#define PORT_THREAD_CREATE_OK CSP_ERR_NONE +#define mutex_create_ok(handle) (CSP_SEMAPHORE_OK == csp_mutex_create(&(handle))) + +#else + +/* ESP_IDF (cspemu) */ + +#define ThreadHandle TaskHandle_t +#define Mutex QueueHandle_t +#define mutex_lock(mutex, timeout) xSemaphoreTake((mutex), (timeout)) +#define mutex_unlock(mutex) xSemaphoreGive((mutex)) +#define mutex_remove(mutex) vSemaphoreDelete((mutex)) +#define DEFINE_TASK(name) void name(void *param) +#define thread_exit() do { vTaskDelete(NULL); return; } while(0) +#include +#include "esp_log.h" +#define port_calloc(a,b) calloc((a),(b)) +#define port_free(p) free(p) +#define port_thread_create(routine, thread_name, stack_size, parameters, priority, return_handle) \ + xTaskCreate(routine, thread_name, stack_size, parameters, priority, return_handle) +#define PORT_THREAD_CREATE_OK pdPASS +#define mutex_create_ok(handle) (NULL != (handle = xSemaphoreCreateMutex())) + +#endif + +#include "socket_server.h" + +/** + * Extra argument validations - can be turned off in production to slightly + * reduce code size and speed it up. + */ +#define TCPD_PARANOID_CHECKS 1 + +static const char *TAG = "tcpd"; + +#ifdef ESP_PLATFORM +#define TCPD_USE_RXLOCK 1 +#include "esp_log.h" +#else +#define TCPD_USE_RXLOCK 0 + +#include +#include +#include +#include +#include +#include +#include +#include + +/* compat with the original csp emu code */ + +// TODO disable spammy logging +#define ESP_LOGE(tag, format, ...) fprintf(stderr, "[error] %s: "format"\n", tag, ##__VA_ARGS__) +#define ESP_LOGW(tag, format, ...) fprintf(stderr, "[warn] %s: "format"\n", tag, ##__VA_ARGS__) +//#define ESP_LOGD(tag, format, ...) fprintf(stderr, "[debug] %s: "format"\n", tag, ##__VA_ARGS__) +//#define ESP_LOGI(tag, format, ...) fprintf(stderr, "[info] %s: "format"\n", tag, ##__VA_ARGS__) +#define ESP_LOGD(tag, format, ...) do {} while(0) +#define ESP_LOGI(tag, format, ...) do {} while(0) + +#define portMAX_DELAY UINT32_MAX +#endif + +static void close_client(TcpdClient_t client, bool mutex_locked); + +struct sockd_client { + Tcpd_t serv; // backreference for easier API usage + int fd; + uint32_t usetime; + void *cctx; + uint32_t tag; + struct sockaddr_in addr; + // injected FDs are custom streams that can be used for reads, but can't be kicked due to LRU + // this allows including e.g. stdin in the select + bool injected; +}; + +struct sockd_server { + struct tcpd_config config; + int server_fd; + fd_set active_fd_set; + struct sockd_client *clients; // this is malloc'd + uint32_t rxcnt; // activity counter (for tracking LRU sockets) + ThreadHandle task; +#if TCPD_USE_RXLOCK + Mutex rxMutex; +#endif +}; + +/** + * Get client by FD. Returns NULL if not found. + * + * @param serv - server struct + * @param sockfd + * @return + */ +TcpdClient_t tcpd_client_by_fd(Tcpd_t serv, int sockfd) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "client_by_fd with no serv!"); + return NULL; + } + + if (!serv->clients) { + ESP_LOGE(TAG, "client_by_fd with no clients table!"); + return NULL; + } +#endif + + for (int i = 0; i < serv->config.max_clients; i++) { + if (serv->clients[i].fd == sockfd) { + return &serv->clients[i]; + } + } + + ESP_LOGE(TAG, "Client for fd %d not found!", sockfd); + return NULL; +} + +/** + * Get client by tag. Returns NULL if not found. + * + * @param serv - server struct + * @param sockfd + * @return + */ +TcpdClient_t tcpd_client_by_tag(Tcpd_t serv, uint32_t tag) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_client_by_tag with no serv!"); + return NULL; + } + + if (!serv->clients) { + ESP_LOGE(TAG, "tcpd_client_by_tag with no clients table!"); + return NULL; + } +#endif + + for (int i = 0; i < serv->config.max_clients; i++) { + if (serv->clients[i].tag == tag && serv->clients[i].fd != FD_NONE) { + return &serv->clients[i]; + } + } + + ESP_LOGE(TAG, "Client for tag %x not found!", tag); + return NULL; +} + +/** + * Update client's LRU time to indicate its activity + * + * @param client - client pointer + */ +static inline void record_client_activity(TcpdClient_t client) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "record_client_activity with no client!"); + return; + } + + if (client->fd == FD_NONE) { + ESP_LOGE(TAG, "record_client_activity with no FD!"); + return; + } +#endif + + client->usetime = ++client->serv->rxcnt; + ESP_LOGD(TAG, "fd %d used, time %d", client->fd, client->serv->rxcnt); +} + +/** + * Mark client slot as free + * + * @param client - client pointer + */ +static inline void mark_client_free(TcpdClient_t client) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "mark_client_free with no client!"); + return; + } +#endif + + client->fd = FD_NONE; + client->usetime = 0; + client->tag = 0; + client->cctx = NULL; +} + +/** + * Create the main server socket + */ +static tcpd_err_t make_server_socket(int *pSock, uint16_t port) +{ +#if TCPD_PARANOID_CHECKS + if (!pSock) { + ESP_LOGE(TAG, "make_server_socket with no pSock!"); + return ESP_ERR_INVALID_ARG; + } +#endif + + int sock; + struct sockaddr_in name; + + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + ESP_LOGE(TAG, "err in socket()"); + return ESP_FAIL; + } + +#ifndef ESP_PLATFORM + // this should prevent failing to bind on POSIX when the program was recently hard-closed + int one = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&one, sizeof(one)); + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&one, sizeof(one)); +#endif + + name.sin_family = AF_INET; + name.sin_port = htons(port); + name.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(sock, (struct sockaddr *) &name, sizeof(name)) < 0) { + ESP_LOGE(TAG, "err in bind()"); + return ESP_FAIL; + } + + *pSock = sock; + return ESP_OK; +} + +/** + * Get a client slot for a new incoming connection. + * Frees the LRU slot (closing its connection) when all slots are full (and LRU closing is enabled) + * + * The client is cleared + * + * @param serv - server struct + * @return client slot + */ +static TcpdClient_t get_new_client_slot(Tcpd_t serv) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "get_new_client_slot with no serv!"); + return NULL; + } + + if (!serv->clients) { + ESP_LOGE(TAG, "get_new_client_slot with no clients table!"); + return NULL; + } +#endif + + ESP_LOGD(TAG, "find free slot"); + + // find free or LRU slot + uint32_t min = UINT32_MAX; + TcpdClient_t client = NULL; + int min_slotnum = -1; + + for (int i = 0; i < serv->config.max_clients; i++) { + TcpdClient_t aClient = &serv->clients[i]; + if (aClient->fd == FD_NONE) { + client = aClient; + ESP_LOGD(TAG, "-> Found empty slot #%d", i); + break; + } + + // find LRU slot + if (aClient->usetime < min && !aClient->injected) { + client = aClient; + min = aClient->usetime; + min_slotnum = i; + } + } + +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "get_new_client_slot no slot found!"); + return NULL; + } +#endif + + // close old connection + if (client->fd != FD_NONE) { // injected slot can never get here + assert(!client->injected); + + if (serv->config.close_lru) { + ESP_LOGD(TAG, "Out of client sockets, closing oldest (fd %d, #%d)", client->fd, min_slotnum); + close(client->fd); + FD_CLR (client->fd, &serv->active_fd_set); + } else { + ESP_LOGW(TAG, "No client slots left."); + return NULL; + } + } + + client->serv = serv; // ensure the server reference is set + mark_client_free(client); + + return client; +} + +/* kick one client */ +void tcpd_kick(TcpdClient_t client) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "tcpd_kick with no client!"); + return; + } + + if (client->fd == FD_NONE) { + ESP_LOGE(TAG, "tcpd_kick with no FD!"); + return; + } +#endif + + ESP_LOGW(TAG, "kicking client fd %d", client->fd); + close_client(client, false); +} + +/* kick all clients */ +void tcpd_kick_all(Tcpd_t serv, bool with_injected) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_kick_all with no server!"); + return; + } + if (serv->server_fd == FD_NONE) { + ESP_LOGE(TAG, "tcpd_kick_all with no server FD!"); + return; + } +#endif + + for (int i = 0; i < serv->config.max_clients; i++) { + TcpdClient_t client = &serv->clients[i]; + if (client->fd != FD_NONE && (!client->injected || with_injected)) { + tcpd_kick(client); + } + } +} + +/* kick all clients */ +int tcpd_kick_by_tag(Tcpd_t serv, uint32_t tag) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_kick_by_tag with no server!"); + return -1; + } + if (serv->server_fd == FD_NONE) { + ESP_LOGE(TAG, "tcpd_kick_by_tag with no server FD!"); + return -1; + } +#endif + + int count = 0; + for (int i = 0; i < serv->config.max_clients; i++) { + TcpdClient_t client = &serv->clients[i]; + if ((client->fd != FD_NONE) && (client->tag == tag)) { + tcpd_kick(client); + count++; + } + } + return count; +} + + +/* kick clients with an IP */ +int tcpd_kick_by_ip(Tcpd_t serv, const struct in_addr *addr) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_kick_by_ip with no server!"); + return -1; + } + if (serv->server_fd == FD_NONE) { + ESP_LOGE(TAG, "tcpd_kick_by_ip with no server FD!"); + return -1; + } +#endif + + ESP_LOGD(TAG, "Kick IP=%"PRIx32, addr->s_addr); + + int count = 0; + for (int i = 0; i < serv->config.max_clients; i++) { + TcpdClient_t client = &serv->clients[i]; + if ((client->fd != FD_NONE)) { + ESP_LOGD(TAG, "client %"PRIu32, client->addr.sin_addr.s_addr); + if (client->addr.sin_addr.s_addr == addr->s_addr) { + tcpd_kick(client); + count++; + } + } + } + return count; +} + +static bool fd_is_valid(int fd) +{ + if (fd == FD_NONE) { + return false; + } + + errno = 0; + + // always returns 0, but can set errno to EBADF or other + fcntl(fd, F_GETFD); + + return errno == 0; +} + +static tcpd_err_t read_from_client(TcpdClient_t client) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "read_from_client with no client!"); + return ESP_ERR_INVALID_ARG; + } + + if (!client->serv) { + ESP_LOGE(TAG, "read_from_client with no client->serv!"); + return ESP_ERR_INVALID_ARG; + } + + if (client->fd == FD_NONE) { + ESP_LOGE(TAG, "read_from_client with no FD!"); + return ESP_ERR_INVALID_ARG; + } +#endif + + if (client->serv->config.read_fn) { + return client->serv->config.read_fn(client->serv, client, client->fd); + } else { + static uint8_t scratch[16]; + return read(client->fd, scratch, sizeof(scratch)); + } +} + +static void open_client(TcpdClient_t client, int fd, struct sockaddr_in *sockaddr) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "open_client with no client!"); + return; + } + + if (!sockaddr) { + ESP_LOGE(TAG, "open_client with no sockaddr!"); + return; + } +#endif + + client->fd = fd; + memcpy(&client->addr, sockaddr, sizeof(struct sockaddr_in)); + + ESP_LOGD(TAG, "+ Client %s:%d joined (fd %d)", + inet_ntoa(sockaddr->sin_addr), + ntohs(sockaddr->sin_port), + client->fd); + + record_client_activity(client); + FD_SET(client->fd, &client->serv->active_fd_set); + + if (client->serv->config.open_fn) { + client->serv->config.open_fn(client->serv, client); + } +} + +TcpdClient_t tcpd_inject_client(Tcpd_t server, int fd) +{ +#if TCPD_PARANOID_CHECKS + if (!server) { + ESP_LOGE(TAG, "tcpd_inject_client with no server!"); + return NULL; + } +#endif + + TcpdClient_t client = get_new_client_slot(server); + if (!client) { return NULL; } + + client->injected = true; + client->fd = fd; + + ESP_LOGI(TAG, "+ Client injected (fd %d)", client->fd); + FD_SET(fd, &server->active_fd_set); + record_client_activity(client); + + if (server->config.open_fn) { + server->config.open_fn(server, client); + } + + return client; +} + +static void close_client(TcpdClient_t client, bool mutex_locked) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "close_client with no client!"); + return; + } +#endif + + if (client->fd == FD_NONE) { + ESP_LOGW(TAG, "- Client became invalid while preparing to close!"); + } else { + if (!client->serv) { + ESP_LOGE(TAG, "close_client with no serv!"); + return; + } + +#if TCPD_USE_RXLOCK + // We must guard this to prevent a lwip crash - https://github.com/espressif/esp-lwip/issues/13 + if (!mutex_locked) { + mutex_lock(client->serv->rxMutex, portMAX_DELAY); + } +#endif + + FD_CLR(client->fd, &client->serv->active_fd_set); + + if (!client->injected) { + close(client->fd); + } + +#if TCPD_USE_RXLOCK + if (!mutex_locked) { + mutex_unlock(client->serv->rxMutex); + } +#endif + + ESP_LOGD(TAG, "- Closing client (fd %d)", client->fd); + } + + if (client->serv->config.close_fn) { + client->serv->config.close_fn(client->serv, client); + } + mark_client_free(client); +} + +/** +* TCP server's main thread +* +* @param arg +*/ +DEFINE_TASK(socksrv_thread) +{ + Tcpd_t serv = param; + +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "socksrv_thread with no serv!"); + thread_exit(); + } + + if (!serv->clients) { + ESP_LOGE(TAG, "socksrv_thread with no clients table!"); + thread_exit(); + } + + if (0 == serv->config.port) { + ESP_LOGE(TAG, "socksrv_thread with zero port!"); + thread_exit(); + } +#endif + + fd_set read_fd_set; + struct sockaddr_in clientname; + + /* Create the socket and set it up to accept connections. */ + tcpd_err_t rv = make_server_socket(&serv->server_fd, serv->config.port); + if (rv != ESP_OK) { + ESP_LOGE(TAG, "failed to start TCP server"); + thread_exit(); + } + + if (listen(serv->server_fd, 4) < 0) { + ESP_LOGE(TAG, "err listen"); + thread_exit(); + } + + ESP_LOGD(TAG, "Started TCP server on port %d", serv->config.port); + + /* Initialize the set of active sockets. */ + FD_ZERO (&serv->active_fd_set); + FD_SET (serv->server_fd, &serv->active_fd_set); + + while (1) { +#if TCPD_USE_RXLOCK + mutex_lock(serv->rxMutex, portMAX_DELAY); +#endif + + /* Block until input arrives on one or more active sockets. */ + struct timeval selectto = {.tv_usec = 100000}; + read_fd_set = serv->active_fd_set; + if (select(FD_SETSIZE, &read_fd_set, NULL, NULL, &selectto) < 0) { + ESP_LOGE(TAG, "err in select()"); + + // try to clean up + for (int i = 0; i < serv->config.max_clients; i++) { + TcpdClient_t client = &serv->clients[i]; + if (client->fd != FD_NONE) { + if (!fd_is_valid(client->fd)) { + close_client(client, true); + FD_CLR(client->fd, &read_fd_set); + } + } + } + + if (!fd_is_valid(serv->server_fd)) { +#if TCPD_USE_RXLOCK + mutex_unlock(serv->rxMutex); +#endif + } + break; + } + + /* Service all the sockets with input pending. */ + for (int fd = 0; fd < FD_SETSIZE; ++fd) { + if (FD_ISSET (fd, &read_fd_set)) { + if (fd == serv->server_fd) { + /* Connection request on original socket. */ + socklen_t size = sizeof(clientname); + int newfd = accept(serv->server_fd, (struct sockaddr *) &clientname, &size); + if (newfd < 0) { + ESP_LOGE(TAG, "err in accept()"); +#if TCPD_USE_RXLOCK + mutex_unlock(serv->rxMutex); +#endif + continue; + } + + struct timeval sockto1 = {.tv_sec = 5}; + setsockopt(newfd, SOL_SOCKET, SO_RCVTIMEO, (char *) &sockto1, sizeof(sockto1)); + struct timeval sockto2 = {.tv_sec = 5}; + setsockopt(newfd, SOL_SOCKET, SO_SNDTIMEO, (char *) &sockto2, sizeof(sockto2)); + + TcpdClient_t slot = get_new_client_slot(serv); + if (slot) { + open_client(slot, newfd, &clientname); + } else { + // reject the connection + close(newfd); + } + } else if (FD_ISSET(fd, &serv->active_fd_set)) { + /* Data arriving for an already connected client */ + TcpdClient_t client = tcpd_client_by_fd(serv, fd); + if (client) { + if (ESP_OK != read_from_client(client)) { + close_client(client, true); + } + } + } + } /* end isset */ + } /* end fd iteration */ + +#if TCPD_USE_RXLOCK + mutex_unlock(serv->rxMutex); +#endif + } /* end server loop */ + + thread_exit(); +} + +tcpd_err_t tcpd_broadcast(Tcpd_t serv, const uint8_t *data, ssize_t len) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_broadcast with no serv!"); + return ESP_ERR_INVALID_ARG; + } + + if (serv->server_fd == FD_NONE) { + ESP_LOGE(TAG, "tcpd_broadcast with no server FD!"); + return ESP_ERR_INVALID_ARG; + } + + if (!data) { + ESP_LOGE(TAG, "tcpd_send with no data!"); + return ESP_ERR_INVALID_ARG; + } +#endif + + if (len < 0) { len = strlen((const char *) data); } + + // send it to all clients + for (int i = 0; i < serv->config.max_clients; i++) { + TcpdClient_t client = &serv->clients[i]; + if (client->fd != FD_NONE) { + tcpd_send(client, data, len); + } + } + + return ESP_OK; +} + +tcpd_err_t tcpd_send(TcpdClient_t client, const uint8_t *data, ssize_t len) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "tcpd_send with no client!"); + return ESP_ERR_INVALID_ARG; + } + + if (!client->serv) { + ESP_LOGE(TAG, "tcpd_send with no client->serv!"); + return ESP_ERR_INVALID_ARG; + } + + if (client->fd == FD_NONE) { + ESP_LOGE(TAG, "tcpd_send with no FD!"); + return ESP_ERR_INVALID_ARG; + } + + if (!data) { + ESP_LOGE(TAG, "tcpd_send with no data!"); + return ESP_ERR_INVALID_ARG; + } +#endif + + int fd = client->fd; + + if (client->injected) { + if (fd == STDIN_FILENO) { + fd = STDOUT_FILENO; + } else if (O_RDONLY == fcntl(fd, F_GETFL)) { + // read-only file, do not try to write (maybe injected stdin) + return ESP_OK; + } + } + + if (len < 0) { len = strlen((const char *) data); } + + int rv = write(fd, data, (size_t) len); + if (rv <= 0) { + ESP_LOGE(TAG, "Client sock write failed"); + close_client(client, false); + return ESP_FAIL; + } else { + record_client_activity(client); + } + + return ESP_OK; +} + +void *tcpd_get_server_ctx(Tcpd_t serv) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_get_server_ctx on NULL!"); + return NULL; + } +#endif + return serv->config.sctx; +} + +void *tcpd_get_client_ctx(TcpdClient_t client) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "tcpd_get_client_ctx on NULL!"); + return NULL; + } +#endif + return client->cctx; +} + +void tcpd_set_client_ctx(TcpdClient_t client, void *cctx) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "tcpd_set_client_ctx on NULL!"); + return; + } +#endif + client->cctx = cctx; +} + +uint32_t tcpd_get_client_tag(TcpdClient_t client) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "tcpd_get_client_tag on NULL!"); + return 0; + } +#endif + return client->tag; +} + +const struct sockaddr_in * tcpd_get_client_addr(TcpdClient_t client) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "tcpd_get_client_addr on NULL!"); + return NULL; + } +#endif + return &client->addr; +} + +void tcpd_set_client_tag(TcpdClient_t client, uint32_t tag) +{ +#if TCPD_PARANOID_CHECKS + if (!client) { + ESP_LOGE(TAG, "tcpd_set_client_tag on NULL!"); + return; + } +#endif + client->tag = tag; +} + +int tcpd_get_client_fd(TcpdClient_t client) +{ + return (client == NULL ? FD_NONE : client->fd); +} + +tcpd_err_t tcpd_init(const tcpd_config_t *config, Tcpd_t *handle) +{ +#if TCPD_PARANOID_CHECKS + if (!config) { + ESP_LOGE(TAG, "tcpd_init with no config!"); + return ESP_ERR_INVALID_ARG; + } + + if (!handle) { + ESP_LOGE(TAG, "tcpd_init with no handle ptr!"); + return ESP_ERR_INVALID_ARG; + } +#endif + + Tcpd_t serv = port_calloc(sizeof(struct sockd_server), 1); + if (!serv) { + ESP_LOGE(TAG, "server alloc failed"); + return ESP_ERR_NO_MEM; + } + + memcpy(&serv->config, config, sizeof(struct tcpd_config)); + + serv->clients = port_calloc(sizeof(struct sockd_client), config->max_clients); + if (!serv->clients) { + port_free(serv); + ESP_LOGE(TAG, "clients alloc failed"); + return ESP_ERR_NO_MEM; + } + + // gracefully shutdown all clients (this calls the close function to free user ctx) + for (int i = 0; i < serv->config.max_clients; i++) { + mark_client_free(&serv->clients[i]); + } + +#if TCPD_USE_RXLOCK + if (!mutex_create_ok(serv->rxMutex)) { + ESP_LOGE(TAG, "error creating mutex"); + port_free(serv->clients); + port_free(serv); + return ESP_FAIL; + } +#endif + + if (!config->start_immediately) { + tcpd_suspend(serv); + } + if (PORT_THREAD_CREATE_OK != port_thread_create(socksrv_thread, + config->task_name, + config->task_stack, + serv, + config->task_prio, + &serv->task) + ) { + ESP_LOGE(TAG, "error creating server thread"); +#if TCPD_USE_RXLOCK + mutex_remove(serv->rxMutex); +#endif + port_free(serv->clients); + port_free(serv); + return ESP_FAIL; + } + + *handle = serv; + return ESP_OK; +} + +void tcpd_shutdown(Tcpd_t serv) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_shutdown with no handle!"); + return; + } +#endif + close(serv->server_fd); + + // this can fail if the server is already stopped, that's OK + tcpd_suspend(serv); + + // gracefully shutdown all clients (this calls the close function to free user ctx) + for (int i = 0; i < serv->config.max_clients; i++) { + TcpdClient_t client = &serv->clients[i]; + if (client->fd != FD_NONE && !client->injected) { + close_client(client, true); + } + } + + if (serv->config.sctx_free_fn && serv->config.sctx) { + serv->config.sctx_free_fn(serv->config.sctx); + } + + // XXX task kill is not implemented in CSP +#ifdef ESP_PLATFORM + vTaskDelete(serv->task); +#endif + +#if TCPD_USE_RXLOCK + mutex_remove(serv->rxMutex); +#endif + port_free(serv->clients); + port_free(serv); +} + +void tcpd_suspend(Tcpd_t serv) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_stop with no handle!"); + return; + } +#endif + + // Since these were implemented using the lock, they now don't work. +#if TCPD_USE_RXLOCK + mutex_lock(serv->rxMutex, portMAX_DELAY); +#endif +} + +void tcpd_resume(Tcpd_t serv) +{ +#if TCPD_PARANOID_CHECKS + if (!serv) { + ESP_LOGE(TAG, "tcpd_start with no handle!"); + return; + } +#endif + +#if TCPD_USE_RXLOCK + mutex_unlock(&serv->rxMutex); +#endif +} + +tcpd_err_t tcpd_iter_init(struct tcpd_client_iter *iter, Tcpd_t serv) { + if (!serv) { + ESP_LOGE(TAG, "tcpd_iter_init NULL server!"); + return -1; + } + + iter->server = serv; + iter->next = 0; + return 0; +} + +TcpdClient_t tcpd_client_iter_next(struct tcpd_client_iter *iterator) +{ +#if TCPD_PARANOID_CHECKS + if (!iterator) { + ESP_LOGE(TAG, "tcpd_client_iter_next NULL iterator!"); + return NULL; + } + if (!iterator->server) { + ESP_LOGE(TAG, "tcpd_client_iter_next NULL server!"); + return NULL; + } +#endif + + const uint16_t maxnum = iterator->server->config.max_clients; + struct sockd_client * const clients = iterator->server->clients; + + for(; iterator->next < maxnum; iterator->next++) { + TcpdClient_t client = &clients[iterator->next]; + if (client->fd != FD_NONE) { + iterator->next++; + return client; + } + } + + return NULL; +} diff --git a/components/vconsole/CMakeLists.txt b/components/vconsole/CMakeLists.txt new file mode 100644 index 0000000..cbe2aa3 --- /dev/null +++ b/components/vconsole/CMakeLists.txt @@ -0,0 +1,18 @@ +idf_component_register( + INCLUDE_DIRS "libconsole/include" +) + +set(CONSOLE_FILE_SUPPORT OFF) +set(CONSOLE_USE_FILE_IO_STREAMS OFF) +set(CONSOLE_USE_TERMIOS OFF) +set(CONSOLE_USE_MEMSTREAM OFF) +set(CONSOLE_MAX_NUM_ARGS 16) +set(CONSOLE_LINE_BUF_LEN 128) +set(CONSOLE_PROMPT_MAX_LEN 24) +set(CONSOLE_HISTORY_LEN 8) +set(CONSOLE_HAVE_CSP OFF) +set(CONSOLE_USE_CSP_COMMANDS OFF) + +add_subdirectory(libconsole) + +target_link_libraries(${COMPONENT_LIB} INTERFACE console argtable3) diff --git a/components/vconsole/libconsole/.gitignore b/components/vconsole/libconsole/.gitignore new file mode 100644 index 0000000..40451f1 --- /dev/null +++ b/components/vconsole/libconsole/.gitignore @@ -0,0 +1,54 @@ +build +*.swp +.lock* +.waf* +.waf3* +waf-*/ +*.o +*.d +*.pyc +.project +.cproject +~* +pdebug* +*.tar* +tags +.DS_store + +# ninja files +build.ninja +rules.ninja +.ninja_deps +.ninja_log + +# generated by cmake +CMakeCache.txt +*.cmake +CMakeFiles +vcom +Makefile +*.cbp +*.a + +.idea/ +.DS_Store + +# Visual Studio clutter +_ReSharper* +*.sdf +*.suo +*.dir +*.vcxproj* +*.sln +.vs +CMakeSettings.json +Win32 +x64 +Debug +Release +MinSizeRel +RelWithDebInfo +*.opensdf + +# this is generated when using in-tree make build +include/console/config.h diff --git a/components/vconsole/libconsole/CMakeLists.txt b/components/vconsole/libconsole/CMakeLists.txt new file mode 100644 index 0000000..723a11f --- /dev/null +++ b/components/vconsole/libconsole/CMakeLists.txt @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 3.13) +project(lib-console) + +add_subdirectory("lib/argtable3") + +file(GLOB CONSOLE_SOURCES "src/*.c") + +# Console config options + +# Line buffer length +set(CONSOLE_LINE_BUF_LEN "255" CACHE STRING "Line buffer length (max command size)") + +# Max number of CLI args +set(CONSOLE_MAX_NUM_ARGS "64" CACHE STRING "Max number of arguments for a command") + +# Prompt buffer length +set(CONSOLE_PROMPT_MAX_LEN "32" CACHE STRING "Max prompt string length") + +# CLI history length +set(CONSOLE_HISTORY_LEN "32" CACHE STRING "Console history length") + +option(CONSOLE_FILE_SUPPORT "Support filesystem operations (history save/load)" ON) + +option(CONSOLE_USE_FILE_IO_STREAMS "Use FILE* based console I/O" ON) +option(CONSOLE_USE_TERMIOS "Use unistd/termios to set nonblocking mode & implement bytes available check" ON) +option(CONSOLE_USE_MEMSTREAM "Allow using open_memstream() to generate command hints and report argtable errors" ON) + +if(CONSOLE_USE_TERMIOS AND NOT CONSOLE_USE_FILE_IO_STREAMS) + message( FATAL_ERROR "Can't use TERMIOS without FILE_IO_STREAMS" ) +endif() + +option(CONSOLE_USE_FREERTOS "Use FreeRTOS" ON) +option(CONSOLE_USE_PTHREADS "Use pthreads" OFF) + +# Default timeout for CSP commands +set(CONSOLE_CSP_DEF_TIMEOUT_MS "3000" CACHE STRING "Default timeout for CSP commands (milliseconds)") + +option(CONSOLE_TESTING_ALLOC_FUNCS "Test the internal console_malloc etc. functions on startup (with asserts)" OFF) + +configure_file( + "include/console/config.h.in" + "include/console/config.h" +) + +add_library(console ${CONSOLE_SOURCES}) + +# Enable extra warnings +#set_target_properties(console PROPERTIES COMPILE_FLAGS "-Wall -Wextra" ) + +target_include_directories(console + PUBLIC "include" "${CMAKE_CURRENT_BINARY_DIR}/include" # this is where the generated config header is placed + PRIVATE "src" +) + +# Link libraries +set(LIBRARIES argtable3) + +if(ESP_PLATFORM) + set(CONSOLE_USE_FREERTOS ON) + set(CONSOLE_USE_PTHREADS OFF) + # special hack for ESP-IDF is needed to allow implementing extern prototypes in main + set(LIBRARIES ${LIBRARIES} idf::main) +endif() + +if(NOT CONSOLE_USE_FREERTOS AND NOT CONSOLE_USE_PTHREADS) + message( FATAL_ERROR "Required either FreeRTOS or PTHREADS!" ) +endif() + +if(CONSOLE_USE_PTHREADS) + set(LIBRARIES ${LIBRARIES} pthread) +endif() + +target_link_libraries(console ${LIBRARIES}) diff --git a/components/vconsole/libconsole/LICENSE.txt b/components/vconsole/libconsole/LICENSE.txt new file mode 100644 index 0000000..2fbc8be --- /dev/null +++ b/components/vconsole/libconsole/LICENSE.txt @@ -0,0 +1 @@ +Proprietary code (c) VZLU 2019-2020 diff --git a/components/vconsole/libconsole/include/console/cmddef.h b/components/vconsole/libconsole/include/console/cmddef.h new file mode 100644 index 0000000..a60b3ec --- /dev/null +++ b/components/vconsole/libconsole/include/console/cmddef.h @@ -0,0 +1,24 @@ +// +// Header with useful defines and common includes +// to use when defining console commands. +// +// This file aims to concentrate the most common includes +// and utility macros to make command definitions easier to write. +// +// Created by MightyPork on 2020/03/11. +// + +#ifndef LIBCONSOLE_CMDDEF_H +#define LIBCONSOLE_CMDDEF_H + +#include +#include +#include "console/console.h" + +#if CONSOLE_HAVE_CSP +#include +#endif + +#include "console/utils.h" + +#endif //LIBCONSOLE_CMDDEF_H diff --git a/components/vconsole/libconsole/include/console/config.h.in b/components/vconsole/libconsole/include/console/config.h.in new file mode 100644 index 0000000..a01b139 --- /dev/null +++ b/components/vconsole/libconsole/include/console/config.h.in @@ -0,0 +1,22 @@ +/** + * Console configuration file, filled by CMake + * + * Created on 2020/03/16. + */ + +#ifndef LIBCONSOLE_CONFIG_H +#define LIBCONSOLE_CONFIG_H + +#cmakedefine CONSOLE_LINE_BUF_LEN @CONSOLE_LINE_BUF_LEN@ +#cmakedefine CONSOLE_MAX_NUM_ARGS @CONSOLE_MAX_NUM_ARGS@ +#cmakedefine CONSOLE_PROMPT_MAX_LEN @CONSOLE_PROMPT_MAX_LEN@ +#cmakedefine CONSOLE_HISTORY_LEN @CONSOLE_HISTORY_LEN@ +#cmakedefine01 CONSOLE_FILE_SUPPORT +#cmakedefine01 CONSOLE_USE_FILE_IO_STREAMS +#cmakedefine01 CONSOLE_USE_TERMIOS +#cmakedefine01 CONSOLE_USE_MEMSTREAM +#cmakedefine01 CONSOLE_USE_FREERTOS +#cmakedefine01 CONSOLE_USE_PTHREADS +#cmakedefine01 CONSOLE_TESTING_ALLOC_FUNCS + +#endif //LIBCONSOLE_CONFIG_H diff --git a/components/vconsole/libconsole/include/console/console.h b/components/vconsole/libconsole/include/console/console.h new file mode 100644 index 0000000..0f2b966 --- /dev/null +++ b/components/vconsole/libconsole/include/console/console.h @@ -0,0 +1,362 @@ +/** + * Console - VCOM command engine + * + * Created on 2020/02/28 by Ondrej Hruska + * + * Parts are based on the console component from esp-idf + * licensed under the Apache 2 license. + */ + +#ifndef LIBCONSOLE_H +#define LIBCONSOLE_H + +#include + +#include +#include +#include + +typedef enum { + /* Colors */ + COLOR_RESET = 0xF0, + COLOR_BLACK = 0x01, + COLOR_RED = 0x02, + COLOR_GREEN = 0x03, + COLOR_YELLOW = 0x04, + COLOR_BLUE = 0x05, + COLOR_MAGENTA = 0x06, + COLOR_CYAN = 0x07, + COLOR_WHITE = 0x08, + /* Modifiers */ + COLOR_NORMAL = 0x0F, + COLOR_BOLD = 0x10, + COLOR_UNDERLINE = 0x20, + COLOR_BLINK = 0x30, + COLOR_HIDE = 0x40, +} console_color_t; + +#if CONSOLE_USE_FREERTOS + #include "freertos/FreeRTOS.h" + #include "freertos/task.h" + #include "freertos/semphr.h" + + typedef SemaphoreHandle_t console_mutex_t; +#endif + +#if CONSOLE_USE_PTHREADS + #include + typedef pthread_mutex_t console_mutex_t; +#endif + +#if CONSOLE_USE_TERMIOS +#include +#endif + +/** + * Console config struct + */ +struct console_config { + /** + * Timeout waiting for execution lock when handling a command. + * This should be longer than the slowest command in the system. + */ + uint32_t execution_lock_timeout_ms; +}; + +/** + * Macro to init the console config struct + */ +#define CONSOLE_CONFIG_DEFAULTS() { \ + .execution_lock_timeout_ms = 10000, \ +} + +typedef struct console_config console_config_t; + +struct console_ctx; // early declaration + +/** + * Console context + */ +typedef struct console_ctx console_ctx_t; + +/** + * Console errors - return codes + */ +enum console_err { + CONSOLE_OK = 0, + /** unspecified error */ + CONSOLE_ERROR = 1, + /** Allocation failed */ + CONSOLE_ERR_NO_MEM, + /** Function call not allowed (e.g. console not inited) */ + CONSOLE_ERR_BAD_CALL, + /** Argument validation failed */ + CONSOLE_ERR_INVALID_ARG, + /** Command not recognized */ + CONSOLE_ERR_UNKNOWN_CMD, + /** Timeout */ + CONSOLE_ERR_TIMEOUT, + /** IO error (file open fail, etc.) */ + CONSOLE_ERR_IO, + /** Operation denied (not allowed / insufficient rights) */ + CONSOLE_ERR_NOT_POSSIBLE, + /** End marker */ + _CONSOLE_ERR_MAX, +}; + +/** + * Print string describing console error. + * In case of unknown error, the number is shown. + * + * The argument should be `enum console_err`, but any number is valid. + */ +void console_err_print_ctx(struct console_ctx *ctx, int e); + +// TODO error-to-string function + +typedef enum console_err console_err_t; + +// early decl's +struct cmd_signature; +typedef struct cmd_signature cmd_signature_t; + +/** + * Command signature, passed as the last argument to the command handler + * to perform registration. + * + * \note Fill only fields that differ from default (zeros/NULLs) + */ +struct cmd_signature { + const char* command; //!< Command name, used in invocations (filled internally, do not set) + const char* help; //!< Command help text, shown when called with -h + const char* hint; //!< Hint text, generated from argtable if hint==NULL & argtable!=NULL + bool no_history; //!< Command skips history + bool custom_args; //!< Disable argtable parsing in the handler function, will be parsed manually from argv/argc + + /** + * Argtable, struct or array that must end with arg_end(). + * Used by the register function and for disambiguation. + */ + void* argtable; +}; + +/** + * Active console context pointer, valid only when handling a console command (otherwise NULL). + * + * Used by the console printf & other IO methods. + */ +extern struct console_ctx * console_active_ctx; + +/** + * Function handling a callback from the console loop. + */ +typedef void(*console_callback_t)(console_ctx_t *ctx); + +/** + * Console context struct + */ +struct console_ctx { +#if CONSOLE_USE_FILE_IO_STREAMS + // Streams + FILE* in; //!< stdin fd, can be -1 if not available (running commands in non-interactive mode) + FILE* out; //!< stdout fd + +#if CONSOLE_USE_TERMIOS + // original termios is stored here before entering raw mode + struct termios orig_termios; +#endif + +#else + void *ioctx; +#endif //CONSOLE_USE_FILE_IO_STREAMS + +#if CONSOLE_FILE_SUPPORT + char *history_file; +#endif //CONSOLE_FILE_SUPPORT + + bool __internal_heap_allocated; + bool exit_allowed; + + char prompt[CONSOLE_PROMPT_MAX_LEN]; //!< Prompt, can be modified by a command or `before_readline_fn` + char line_buffer[CONSOLE_LINE_BUF_LEN]; + + /** + * Callback fired in the command evaluation loop, each time before the prompt is shown and new line read. + * This command can print to the output streams, change prompt, shutdown console, etc. + */ + console_callback_t loop_handler; + + /** + * Callback fired before the console task shuts down + */ + console_callback_t shutdown_handler; + + /** + * Shutdown requested. Console will exit as soon as possible. + */ + bool exit_requested; + + /** + * Interactive mode. Enables additional outputs for user convenience. + */ + bool interactive; + + /** + * Enable ANSI colors + */ + bool use_colors; + + /* These fields are valid only during command execution */ + const char **argv; //!< The current argv + size_t argc; //!< The current argc + const cmd_signature_t *cmd; //!< Pointer to the currently executed command signature + + /** Used for argument validation */ + uint32_t __internal_magic; +}; + +#define CONSOLE_CTX_MAGIC 0x6f587468 + +/** + * Command handler type. + * + * @param ctx - console context, including input/output files + * @param reg - signature struct; if not NULL, init the static argtable, fill this struct, and return OK (0). + * @return status code, 0 = OK (use cons_err_t constants if possible) + */ +typedef int (*console_command_t)(console_ctx_t *ctx, struct cmd_signature *reg); + +/** + * Intialize the console + * + * @param config - config pointer, NULL to use defaults + * @return status code + */ +console_err_t console_init(const console_config_t *config); + +/** + * @brief Register console command + * + * If the command function is already registered, this creates an alias. + * A multi-word command automatically create a command group. The group can + * be described using a description string by calling `console_group_add()` + * - at convenience before or after the commands are registered. + * + * @param name - command name (may contain spaces for "multi-part commands") + * @param handler pointer to the command handler. + * @return status code + */ +console_err_t console_cmd_register(console_command_t handler, const char *name); + +/** + * @brief Register a command group. + * + * Command groups are created automatically when used. + * This method can create a group with description, or attach a custom description + * to an existing group. + * + * @param name - group name (first word of multi-part commands) + * @param descr - description to attach, can be NULL + * @return staus code + */ +console_err_t console_group_add(const char *name, const char *descr); + +/** + * Add alias to an existing command by name. + * + * @param original - original command name + * @param alias - command's alias + * @return status code + */ +console_err_t console_cmd_add_alias(const char *original, const char *alias); + +/** + * Add alias by handler function + * + * @param handler - command handler + * @param alias - new name + * @return status code + */ +console_err_t console_cmd_add_alias_fn(console_command_t handler, const char *alias); + +/** + * Internal error print function. Has WEAK linkage, can be overridden. + * + * This function is used to report detected bugs and should not be called + * in well-written "production code". + * + * @param msg - error message + */ +void console_internal_error_print(const char *msg); + +/** + * This function is guarded by a mutex and will wait for the execution lock as + * configured in console_config. + * + * @brief Run command line + * @param[in] outf + * @param[in] inf + * @param cmdline command line (command name followed by a number of arguments) + * @param[out] pRetval return code from the command (set if command was run) + * @param[out] pCommandSig - is set to a pointer to the matched command signature, or NULL on error + * @return status code + */ +console_err_t console_handle_cmd( + console_ctx_t *ctx, + const char *cmdline, + int *pRetval, + const struct cmd_signature **pCommandSig +); + +/** + * Count all registered commands + * + * @return + */ +size_t console_count_commands(void); + +/** + * Create a console IO context and init it to defaults. + * + * takes stdin and stdout file descriptors, or IO context (based on config flags) + * + * In the FD variant, pass NULL as STDIN if not available. + * + * @param ctx - context, if using static alloc, NULL to allocate internally. + * @return the context, NULL if alloc or init fails + */ +console_ctx_t *console_ctx_init( + console_ctx_t *ctx, +#if CONSOLE_USE_FILE_IO_STREAMS + FILE* inf, FILE* outf +#else + void * ioctx +#endif +); + +/** + * Destroy a console IO context. + * + * Make sure to release any user fields (ioctx, for example) beforehand. + * + * @attention ONLY CALL THIS IF THE CONTEXT WAS DYNAMICALLY ALLOCATED! + * + * @param[in,out] ctx - pointer to context, will be set to NULL. + */ +void console_ctx_destroy(console_ctx_t *ctx); + +/** + * Console task + * + * @param[in] param - must be a valid console context (see `console_ctx_init()`) + */ +void console_task(void *param); + +/** + * Variant of 'console_task' for pthreads (returns NULL) + */ +void* console_task_posix(void *param); + +#include "console_io.h" + +#endif //LIBCONSOLE_H diff --git a/components/vconsole/libconsole/include/console/console_io.h b/components/vconsole/libconsole/include/console/console_io.h new file mode 100644 index 0000000..eedc9db --- /dev/null +++ b/components/vconsole/libconsole/include/console/console_io.h @@ -0,0 +1,196 @@ +/** + * Console IO functions. + * + * This header is included internally by console.h + * + * Created on 2020/04/09. + */ + +#ifndef LIBCONSOLE_IO_H +#define LIBCONSOLE_IO_H + +#ifndef LIBCONSOLE_H +#error Include console.h! +#endif + +#include + +// ------ If the FILE based IO streams option is OFF, these are extern ------- + +/** + * Write to console context. + * + * In command context, the more convenient "console_write", "console_print", "console_println" + * and "console_printf" functions can be used instead. + * + * This function is a Linenoise write callback. + * + * Return number of characters written, -1 on error. + */ +extern int console_write_ctx(console_ctx_t *ctx, const char *text, size_t len); + +/** + * Read from console context's input stream. + * + * In command context, the more convenient "console_read" function and the + * "console_can_read" and "console_have_stdin" helper functions can be used instead. + * + * This is also a Linenoise read callback. + * + * Return number of characters read, -1 on error + */ +extern int console_read_ctx(console_ctx_t *ctx, char *dest, size_t count); + +/** + * Check if console input stream has bytes ready. + * + * @return number of queued bytes, 0 if none, -1 on error. + */ +extern int console_can_read_ctx(console_ctx_t *ctx); + +/** + * Test if console context is not NULL and has stdin stream available + * + * @return have stdin + */ +extern bool console_have_stdin_ctx(console_ctx_t *ctx); + + +// ----- end extern interface ----- + +/** + * Print zero-terminated string to to console output + * + * @param text - characters to write + * @return number of characters written, or -1 on error + */ +int console_print_ctx(console_ctx_t *ctx, const char *text); + +/** + * Print zero-terminated string to console output, followed by a newline + * + * @param text - characters to write + * @return number of characters written, or -1 on error + */ +ssize_t console_println_ctx(console_ctx_t *ctx, const char *text); + +/** + * Console printf + * + * @param ctx - console context + * @param color - color to use, COLOR_RESET = default + * @param format + * @param ... + * @return bytes written, or -1 on error + */ +ssize_t console_printf_ctx(console_ctx_t *ctx, console_color_t color, const char *format, ...) __attribute__((format(printf,3,4))); + +/** + * Console vprintf + * + * @param ctx - console context + * @param color - color to use, COLOR_RESET = default + * @param format - format string + * @param args - varargs passed as a va_list + * @return bytes written, or -1 on error + */ +ssize_t console_vprintf_ctx(console_ctx_t *ctx, console_color_t color, const char *format, va_list args); + +/** + * Write to console output + * + * @attention Can only be used within a console command context + * + * @param text - characters to write + * @param len - text length + * @return number of characters written, or -1 on error + */ +ssize_t console_write(const char *text, size_t len); + + +// -------------------- Convenience functions ------------------------- + +/** + * Test if we are in the console command context. + * + * @return in command context + */ +static inline bool console_context_available(void) { + return console_active_ctx != NULL; +} + +/** + * Test if we are in the console command context AND the console context has an input stream + * (input stream may be used in interactive commands) + * + * @return have stdin + */ +bool console_have_stdin(void); + +/** + * Console printf. Defined as a macro to pass variadic arguments + * + * @attention Can only be used within a console command context + * + * @param format + * @param ... + * @return bytes written, or -1 on error + */ +#define console_printf(format, ...) console_printf_ctx(console_active_ctx, COLOR_RESET, format, ##__VA_ARGS__) + +/** + * Console printf with colors. Defined as a macro to pass variadic arguments + * + * @attention Can only be used within a console command context + * + * @param color - from console_colors_t enum + * @param format + * @param ... + * @return bytes written, or -1 on error + */ +#define console_color_printf(color, format, ...) console_printf_ctx(console_active_ctx, color, format, ##__VA_ARGS__) + +/** + * Read from console input + * + * @attention Can only be used within a console command context + * + * @param dest - destination buffer + * @param count - how many characters to read + * @return number of characters read, or -1 on error + */ +ssize_t console_read(char *dest, size_t count); + +/** + * Check if console input stream has bytes ready. + * + * @attention Can only be used within a console command context + * + * @return number of queued bytes, 0 if none, -1 on error. + */ +int console_can_read(void); + +/** + * Print zero-terminated string to to console output + * + * @attention Can only be used within a console command context + * + * @param text - characters to write + * @return number of characters written, or -1 on error + */ +static inline int console_print(const char *text) { + return console_print_ctx(console_active_ctx, text); +} + +/** + * Print zero-terminated string to console output, followed by a newline + * + * @attention Can only be used within a console command context + * + * @param text - characters to write + * @return number of characters written, or -1 on error + */ +ssize_t console_println(const char *text); + + +#endif //LIBCONSOLE_IO_H diff --git a/components/vconsole/libconsole/include/console/prefix_match.h b/components/vconsole/libconsole/include/console/prefix_match.h new file mode 100644 index 0000000..a4d121f --- /dev/null +++ b/components/vconsole/libconsole/include/console/prefix_match.h @@ -0,0 +1,94 @@ +/** + * Prefix Match + * + * Match input value to a list of options, allowing non-ambiguous abbreviation and partial matching. + * This library was designed for command recognition in interactive consoles and command interfaces. + * + * Created on 2020/06/09 by Ondřej Hruška + */ + +#ifndef _PREFIX_MATCH_H +#define _PREFIX_MATCH_H + +#include +#include + +/** Use case-sensitive matching */ +#define PREFIXMATCH_CASE_SENSITIVE 1 +/** Forbid abbreviations */ +#define PREFIXMATCH_NOABBREV 2 +/** Allow matching fewer words, if unambiguous */ +#define PREFIXMATCH_MULTI_PARTIAL 4 + +enum pm_test_result { + PM_TEST_NO_MATCH = 0, + PM_TEST_MATCH = 1, + PM_TEST_MATCH_MULTI_PARTIAL = 2, +}; + +/** + * Recognize (optionally abbreviated) input + * + * @param[in] value - tested value + * @param[in] options - options to match against + * @param[in] flags - matching options (bitmask) - accepts PREFIXMATCH_CASE_SENSITIVE and PREFIXMATCH_NOABBREV + * @return index of the matched option, -1 on mismatch or ambiguous match + */ +int prefix_match(const char *value, const char **options, int flags); + +/** + * Recognize input consisting of one or more (optionally abbreviated) words + * + * @param[in] value - tested value + * @param[in] options - options to match against, multi-word options separated by the listed delimiters + * @param[in] delims - string with a list of possible delimiters (like for strtok) + * @param[in] flags - matching options (bitmask) - accepts all options + * @return index of the matched option, -1 on mismatch or ambiguous match + */ +int prefix_multipart_match(const char *restrict value, const char **options, const char* restrict delims, int flags); + +// useful internal functions exported for possible re-use + +/** + * Test if two word sentences match, with individual words optionally allowed to be abbreviated. + * + * @internal + * @param[in] tested - tested (optionally abbreviated) sentence + * @param[in] full - full sentence + * @param[in] delims - list of possible delimiters, same may be used for both sentences + * @param[in] flags - matching options (bitmask) - accepts all options + * @return 1-match; 0-no match; 2-partial (some words) match, if the PREFIXMATCH_MULTI_PARTIAL flag is set + */ +enum pm_test_result prefix_multipart_test(const char *restrict tested, const char* restrict full, const char *restrict delims, int flags); + +/** + * Count words in a "sentence", delimited by any of the given set of delimiters. + * + * @internal + * @param[in] sentence - one or multi-word string + * @param[in] delims - delimiters accepted + * @return number of words + */ +size_t pm_count_words(const char * restrict sentence, const char * restrict delims); + +/** + * Measure word length + * + * @internal + * @param[in] word - start of a word that ends with either one of the delimiters, or a null byte. + * @param[in] delims - delimiters accepted + * @return word length + */ +size_t pm_word_len(const char * restrict word, const char * restrict delims); + +/** + * Skip N words in a sentence. + * + * @param[in] sentence - one or multi-word string + * @param[in] delims - delimiters accepted + * @param[in] skip - how many words to skip + * @return pointer to the first byte after the last skipped word + */ +const char *pm_skip_words(const char * restrict sentence, const char * restrict delims, size_t skip); + +#endif //_PREFIX_MATCH_H diff --git a/components/vconsole/libconsole/include/console/utils.h b/components/vconsole/libconsole/include/console/utils.h new file mode 100644 index 0000000..97bcc3d --- /dev/null +++ b/components/vconsole/libconsole/include/console/utils.h @@ -0,0 +1,213 @@ +/** + * Utilities for console commands + * + * Created on 2020/03/11. + */ + +#ifndef LIBCONSOLE_UTILS_H +#define LIBCONSOLE_UTILS_H + +#include +#include + +#ifndef STR +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) +#endif + +#ifndef MIN +#define MIN(a,b) (((a)<(b))?(a):(b)) +#endif + +#ifndef MAX +#define MAX(a,b) (((a)>(b))?(a):(b)) +#endif + +#ifndef OBC_FIRMWARE +#define EXPENDABLE_STRING(x) x +#define EXPENDABLE_CODE(x) x +#else +#define EXPENDABLE_STRING(x) "" +#define EXPENDABLE_CODE(x) do {} while(0); +#endif + +/** + * Read an argument, or return default if it is empty. + * + * This works for commands arg_int0 + * + * Usage: + * + * \code + * static struct { + * struct arg_int *foo; + * } args; + * + * args.foo = arg_int0(...); + * + * int foo = GET_ARG_INT0(args.foo, 1234); + * \endcode + */ +#define GET_ARG_INT0(_arg, _def) ((_arg)->count ? (_arg)->ival[0] : (_def)) + +/** + * Get CSP node ID from an argument table, using own address as default. + * + * Usage: + * + * \code + * static struct { + * struct arg_int *node; + * } args; + * + * args.node = arg_int0(...); + * + * int node = GET_ARG_CSPADDR0(args.node); + * \endcode + */ +#define GET_ARG_CSPADDR0(_arg) GET_ARG_INT0((_arg), csp_get_address()) + +/** + * Shortcut to get a timeout argument's value, using CSP_DEF_TIMEOUT_MS as default. + */ +#define GET_ARG_TIMEOUT0(_arg) GET_ARG_INT0((_arg), CONSOLE_CSP_DEF_TIMEOUT_MS) + +/** + * Define an optional CSP node argument + */ +#define arg_cspaddr0() arg_int0(NULL, NULL, "", EXPENDABLE_STRING("node ID")) + +/** + * Define a mandatory CSP node argument + */ +#define arg_cspaddr1() arg_int1(NULL, NULL, "", EXPENDABLE_STRING("node ID")) + +/** + * Define an optional timeout argument, with `CSP_DEF_TIMEOUT_MS` + * shown as default. Use `GET_ARG_TIMEOUT0()` to retrieve its value. + */ +#define arg_timeout0() arg_int0("t", "timeout", "", EXPENDABLE_STRING("timeout in ms (default "STR(CONSOLE_CSP_DEF_TIMEOUT_MS)")")) + +/** + * Define a timeout argument with a custom value shown as default. + * Use `GET_ARG_INT0()` with the matching default to retrieve its value. + */ +#define arg_timeout0_def(_def) arg_int0("t", "timeout", "", EXPENDABLE_STRING("timeout in ms (default "STR(_def)")")) + + +#define EMPTY_CMD_SETUP(_helptext) \ + (void)ctx; \ + static struct { \ + struct arg_end *end; \ + } args; \ + \ + if (reg) { \ + args.end = arg_end(1); \ + \ + reg->argtable = &args; \ + reg->help = EXPENDABLE_STRING(_helptext); \ + return 0; \ + } + +/** + * Hexdump a buffer + * + * @param outf - output file + * @param data - data to dump + * @param len - data size + */ +void console_hexdump(const void *data, size_t len); + +/** + * Decode hexa string to binary + * + * @param hex - hexa string, upper or lower case, must have even length + * @param dest - destination buffer + * @param capacity - buffer size + * @return destination length, or: -1 (bad args), -2 (bad format), -3 (too long) + */ +int console_base16_decode(const char *hex, void *dest, size_t capacity); + +#if CONSOLE_USE_MEMSTREAM + +/** + * Data struct for the filecap utilities + */ +struct console_filecap { + char *buf; + size_t buf_size; + FILE *file; +}; + +typedef struct console_filecap console_filecap_t; + +/** + * Open a temporary in-memory file that can be used to capture the output + * of functions taking a FILE * argument. + * + * @param cap - pointer to a filecap struct (can be on stack) + * @return success + */ +console_err_t console_filecap_init(console_filecap_t *cap); + +/** + * Clean up the capture struct. + * + * If the buffer is to be used elsewhere, place NULL in the struct to avoid freeing it. + * + * If the struct itself was allocated on heap, it is the caller's responsibility to free it manually. + * + * @param cap - pointer to a filecap struct + */ +void console_filecap_end(console_filecap_t *cap); + +/** + * Print the captured output to console output stream and clean up the struct (calls `console_filecap_end()`) + * + * @param cap - pointer to a filecap struct + */ +void console_filecap_print_end(console_filecap_t *cap); + +#endif // CONSOLE_USE_MEMSTREAM + +/** + * Cross-platform malloc that can be used from console commands. + * If CSP is available and the platform is not POSIX, the implementation from there is used (i.e. FreeRTOS alloc) + */ +void * __attribute__((malloc)) console_malloc(size_t size); + +/** + * Cross-platform realloc that can be used from console commands. + * + * It is not possible to determine the size of an allocated memory region in a portable way, + * that's why this function takes the old size as an argument. On POSIX, the argument is simply ignored. + * + * If CSP is available and the platform is not POSIX, the implementation from there is used (i.e. FreeRTOS alloc). + * NOTE: CSP does not provide realloc, therefore this function allocates a new buffer, copies data, and frees the old buffer. + * + * Returns the original buffer if the new size is <= old size. + */ +void * console_realloc(void *ptr, size_t oldsize, size_t newsize); + +/** + * Cross-platform calloc that can be used from console commands. + * If CSP is available and the platform is not POSIX, the implementation from there is used (i.e. FreeRTOS alloc) + */ +void * __attribute__((malloc,alloc_size(1,2))) console_calloc(size_t nmemb, size_t size); + +/** + * `free()` for memory allocated by `console_malloc()` or `console_calloc()` + */ +void console_free(void *ptr); + +/** + * `strdup()` using `console_malloc()`. Free with `console_free()` + */ +char * console_strdup(const char *ptr); + +/** + * `strndup()` using `console_malloc()`. Free with `console_free()` + */ +char * console_strndup(const char *ptr, size_t maxlen); + +#endif //LIBCONSOLE_UTILS_H diff --git a/components/vconsole/libconsole/lib/argtable3/CMakeLists.txt b/components/vconsole/libconsole/lib/argtable3/CMakeLists.txt new file mode 100644 index 0000000..e5f28bd --- /dev/null +++ b/components/vconsole/libconsole/lib/argtable3/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) + +project(console-argtable3) + +add_library(argtable3 argtable3.c) +target_include_directories(argtable3 + PUBLIC "." +) +target_link_libraries(argtable3 PRIVATE console) diff --git a/components/vconsole/libconsole/lib/argtable3/README.txt b/components/vconsole/libconsole/lib/argtable3/README.txt new file mode 100644 index 0000000..3612411 --- /dev/null +++ b/components/vconsole/libconsole/lib/argtable3/README.txt @@ -0,0 +1,3 @@ +Copy of upstream argtable3 from github + +It is released under the 3-clause BSD license. diff --git a/components/vconsole/libconsole/lib/argtable3/argtable3.c b/components/vconsole/libconsole/lib/argtable3/argtable3.c new file mode 100644 index 0000000..e8c3b59 --- /dev/null +++ b/components/vconsole/libconsole/lib/argtable3/argtable3.c @@ -0,0 +1,4969 @@ +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include "argtable3.h" + +#pragma GCC diagnostic ignored "-Wclobbered" + +#include + +#define at3_malloc(n) console_malloc(n) +#define at3_calloc(a,b) console_calloc(a,b) +#define at3_free(p) console_free(p) +#define at3_realloc(p, old, new) console_realloc(p, old, new) + +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 2013 Tom G. Huang + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#ifndef ARG_UTILS_H +#define ARG_UTILS_H + +#define ARG_ENABLE_TRACE 0 +#define ARG_ENABLE_LOG 1 + +#ifdef __cplusplus +extern "C" { +#endif + +enum +{ + EMINCOUNT = 1, + EMAXCOUNT, + EBADINT, + EOVERFLOW, + EBADDOUBLE, + EBADDATE, + EREGNOMATCH +}; + + +#if defined(_MSC_VER) +#define ARG_TRACE(x) \ + __pragma(warning(push)) \ + __pragma(warning(disable:4127)) \ + do { if (ARG_ENABLE_TRACE) dbg_printf x; } while (0) \ + __pragma(warning(pop)) + +#define ARG_LOG(x) \ + __pragma(warning(push)) \ + __pragma(warning(disable:4127)) \ + do { if (ARG_ENABLE_LOG) dbg_printf x; } while (0) \ + __pragma(warning(pop)) +#else +#define ARG_TRACE(x) \ + do { if (ARG_ENABLE_TRACE) dbg_printf x; } while (0) + +#define ARG_LOG(x) \ + do { if (ARG_ENABLE_LOG) dbg_printf x; } while (0) +#endif + +extern void dbg_printf(const char *fmt, ...); + +#ifdef __cplusplus +} +#endif + +#endif + +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include +#include + + +void dbg_printf(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); +} + +/* $Id: getopt.h,v 1.1 2009/10/16 19:50:28 rodney Exp rodney $ */ +/* $OpenBSD: getopt.h,v 1.1 2002/12/03 20:24:29 millert Exp $ */ +/* $NetBSD: getopt.h,v 1.4 2000/07/07 10:43:54 ad Exp $ */ + +/*- + * Copyright (c) 2000 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Dieter Baron and Thomas Klausner. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by the NetBSD + * Foundation, Inc. and its contributors. + * 4. Neither the name of The NetBSD Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef _GETOPT_H_ +#define _GETOPT_H_ + +#if 0 +#include +#endif + +/* + * GNU-like getopt_long() and 4.4BSD getsubopt()/optreset extensions + */ +#define no_argument 0 +#define required_argument 1 +#define optional_argument 2 + +struct option { + /* name of long option */ + const char *name; + /* + * one of no_argument, required_argument, and optional_argument: + * whether option takes an argument + */ + int has_arg; + /* if not NULL, set *flag to val when option found */ + int *flag; + /* if flag not NULL, value to set *flag to; else return value */ + int val; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +int getopt_long(int, char * const *, const char *, + const struct option *, int *); +int getopt_long_only(int, char * const *, const char *, + const struct option *, int *); +#ifndef _GETOPT_DEFINED +#define _GETOPT_DEFINED +int getopt(int, char * const *, const char *); +int getsubopt(char **, char * const *, char **); + +extern char *optarg; /* getopt(3) external variables */ +extern int opterr; +extern int optind; +extern int optopt; +extern int optreset; +extern char *suboptarg; /* getsubopt(3) external variable */ +#endif /* _GETOPT_DEFINED */ + +#ifdef __cplusplus +} +#endif +#endif /* !_GETOPT_H_ */ +/* $Id: getopt_long.c,v 1.1 2009/10/16 19:50:28 rodney Exp rodney $ */ +/* $OpenBSD: getopt_long.c,v 1.23 2007/10/31 12:34:57 chl Exp $ */ +/* $NetBSD: getopt_long.c,v 1.15 2002/01/31 22:43:40 tv Exp $ */ + +/* + * Copyright (c) 2002 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Sponsored in part by the Defense Advanced Research Projects + * Agency (DARPA) and Air Force Research Laboratory, Air Force + * Materiel Command, USAF, under agreement number F39502-99-1-0512. + */ + +#ifndef lint +//static const char rcsid[]="$Id: getopt_long.c,v 1.1 2009/10/16 19:50:28 rodney Exp rodney $"; +#endif /* lint */ +/*- + * Copyright (c) 2000 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Dieter Baron and Thomas Klausner. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +// Define this to replace system getopt +// +//#define REPLACE_GETOPT /* use this getopt as the system getopt(3) */ + +#ifdef REPLACE_GETOPT +int opterr = 1; /* if error message should be printed */ +int optind = 1; /* index into parent argv vector */ +int optopt = '?'; /* character checked for validity */ +int optreset; /* reset getopt */ +char *optarg; /* argument associated with option */ + + +#define PRINT_ERROR ((opterr) && (*options != ':')) + +#define FLAG_PERMUTE 0x01 /* permute non-options to the end of argv */ +#define FLAG_ALLARGS 0x02 /* treat non-options as args to option "-1" */ +#define FLAG_LONGONLY 0x04 /* operate as getopt_long_only */ + +/* return values */ +#define BADCH (int)'?' +#define BADARG ((*options == ':') ? (int)':' : (int)'?') +#define INORDER (int)1 + +#define EMSG "" + +static int getopt_internal(int, char * const *, const char *, + const struct option *, int *, int); +static int parse_long_options(char * const *, const char *, + const struct option *, int *, int); +static int gcd(int, int); +static void permute_args(int, int, int, char * const *); + +static char *place = EMSG; /* option letter processing */ + +/* XXX: set optreset to 1 rather than these two */ +static int nonopt_start = -1; /* first non option argument (for permute) */ +static int nonopt_end = -1; /* first option after non options (for permute) */ + +/* Error messages */ +static const char recargchar[] = "option requires an argument -- %c"; +static const char recargstring[] = "option requires an argument -- %s"; +static const char ambig[] = "ambiguous option -- %.*s"; +static const char noarg[] = "option doesn't take an argument -- %.*s"; +static const char illoptchar[] = "unknown option -- %c"; +static const char illoptstring[] = "unknown option -- %s"; + + +#if defined(_WIN32) || defined(ESP_PLATFORM) + +/* Windows needs warnx(). We change the definition though: + * 1. (another) global is defined, opterrmsg, which holds the error message + * 2. errors are always printed out on stderr w/o the program name + * Note that opterrmsg always gets set no matter what opterr is set to. The + * error message will not be printed if opterr is 0 as usual. + */ + +#include +#include + +extern char opterrmsg[128]; +char opterrmsg[128]; /* buffer for the last error message */ + +static void warnx(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + /* + Make sure opterrmsg is always zero-terminated despite the _vsnprintf() + implementation specifics and manually suppress the warning. + */ + memset(opterrmsg, 0, sizeof opterrmsg); + if (fmt != NULL) + vsnprintf(opterrmsg, sizeof(opterrmsg) - 1, fmt, ap); + va_end(ap); +#if defined(_WIN32) +#pragma warning(suppress: 6053) +#endif + fprintf(stderr, "%s\n", opterrmsg); +} + +#else +#include +#endif /*_WIN32*/ + +/* + * Compute the greatest common divisor of a and b. + */ +static int +gcd(int a, int b) +{ + int c; + + c = a % b; + while (c != 0) { + a = b; + b = c; + c = a % b; + } + + return (b); +} + +/* + * Exchange the block from nonopt_start to nonopt_end with the block + * from nonopt_end to opt_end (keeping the same order of arguments + * in each block). + */ +static void +permute_args(int panonopt_start, int panonopt_end, int opt_end, + char * const *nargv) +{ + int cstart, cyclelen, i, j, ncycle, nnonopts, nopts, pos; + char *swap; + + /* + * compute lengths of blocks and number and size of cycles + */ + nnonopts = panonopt_end - panonopt_start; + nopts = opt_end - panonopt_end; + ncycle = gcd(nnonopts, nopts); + cyclelen = (opt_end - panonopt_start) / ncycle; + + for (i = 0; i < ncycle; i++) { + cstart = panonopt_end+i; + pos = cstart; + for (j = 0; j < cyclelen; j++) { + if (pos >= panonopt_end) + pos -= nnonopts; + else + pos += nopts; + swap = nargv[pos]; + /* LINTED const cast */ + ((char **) nargv)[pos] = nargv[cstart]; + /* LINTED const cast */ + ((char **)nargv)[cstart] = swap; + } + } +} + +/* + * parse_long_options -- + * Parse long options in argc/argv argument vector. + * Returns -1 if short_too is set and the option does not match long_options. + */ +static int +parse_long_options(char * const *nargv, const char *options, + const struct option *long_options, int *idx, int short_too) +{ + char *current_argv, *has_equal; + size_t current_argv_len; + int i, match; + + current_argv = place; + match = -1; + + optind++; + + if ((has_equal = strchr(current_argv, '=')) != NULL) { + /* argument found (--option=arg) */ + current_argv_len = has_equal - current_argv; + has_equal++; + } else + current_argv_len = strlen(current_argv); + + for (i = 0; long_options[i].name; i++) { + /* find matching long option */ + if (strncmp(current_argv, long_options[i].name, + current_argv_len)) + continue; + + if (strlen(long_options[i].name) == current_argv_len) { + /* exact match */ + match = i; + break; + } + /* + * If this is a known short option, don't allow + * a partial match of a single character. + */ + if (short_too && current_argv_len == 1) + continue; + + if (match == -1) /* partial match */ + match = i; + else { + /* ambiguous abbreviation */ + if (PRINT_ERROR) + warnx(ambig, (int)current_argv_len, + current_argv); + optopt = 0; + return (BADCH); + } + } + if (match != -1) { /* option found */ + if (long_options[match].has_arg == no_argument + && has_equal) { + if (PRINT_ERROR) + warnx(noarg, (int)current_argv_len, + current_argv); + /* + * XXX: GNU sets optopt to val regardless of flag + */ + if (long_options[match].flag == NULL) + optopt = long_options[match].val; + else + optopt = 0; + return (BADARG); + } + if (long_options[match].has_arg == required_argument || + long_options[match].has_arg == optional_argument) { + if (has_equal) + optarg = has_equal; + else if (long_options[match].has_arg == + required_argument) { + /* + * optional argument doesn't use next nargv + */ + optarg = nargv[optind++]; + } + } + if ((long_options[match].has_arg == required_argument) + && (optarg == NULL)) { + /* + * Missing argument; leading ':' indicates no error + * should be generated. + */ + if (PRINT_ERROR) + warnx(recargstring, + current_argv); + /* + * XXX: GNU sets optopt to val regardless of flag + */ + if (long_options[match].flag == NULL) + optopt = long_options[match].val; + else + optopt = 0; + --optind; + return (BADARG); + } + } else { /* unknown option */ + if (short_too) { + --optind; + return (-1); + } + if (PRINT_ERROR) + warnx(illoptstring, current_argv); + optopt = 0; + return (BADCH); + } + if (idx) + *idx = match; + if (long_options[match].flag) { + *long_options[match].flag = long_options[match].val; + return (0); + } else + return (long_options[match].val); +} + +/* + * getopt_internal -- + * Parse argc/argv argument vector. Called by user level routines. + */ +static int +getopt_internal(int nargc, char * const *nargv, const char *options, + const struct option *long_options, int *idx, int flags) +{ + char *oli; /* option letter list index */ + int optchar, short_too; + static int posixly_correct = -1; + + if (options == NULL) + return (-1); + + /* + * Disable GNU extensions if POSIXLY_CORRECT is set or options + * string begins with a '+'. + */ + if (posixly_correct == -1) + posixly_correct = (getenv("POSIXLY_CORRECT") != NULL); + if (posixly_correct || *options == '+') + flags &= ~FLAG_PERMUTE; + else if (*options == '-') + flags |= FLAG_ALLARGS; + if (*options == '+' || *options == '-') + options++; + + /* + * XXX Some GNU programs (like cvs) set optind to 0 instead of + * XXX using optreset. Work around this braindamage. + */ + if (optind == 0) + optind = optreset = 1; + + optarg = NULL; + if (optreset) + nonopt_start = nonopt_end = -1; +start: + if (optreset || !*place) { /* update scanning pointer */ + optreset = 0; + if (optind >= nargc) { /* end of argument vector */ + place = EMSG; + if (nonopt_end != -1) { + /* do permutation, if we have to */ + permute_args(nonopt_start, nonopt_end, + optind, nargv); + optind -= nonopt_end - nonopt_start; + } + else if (nonopt_start != -1) { + /* + * If we skipped non-options, set optind + * to the first of them. + */ + optind = nonopt_start; + } + nonopt_start = nonopt_end = -1; + return (-1); + } + if (*(place = nargv[optind]) != '-' || + (place[1] == '\0' && strchr(options, '-') == NULL)) { + place = EMSG; /* found non-option */ + if (flags & FLAG_ALLARGS) { + /* + * GNU extension: + * return non-option as argument to option 1 + */ + optarg = nargv[optind++]; + return (INORDER); + } + if (!(flags & FLAG_PERMUTE)) { + /* + * If no permutation wanted, stop parsing + * at first non-option. + */ + return (-1); + } + /* do permutation */ + if (nonopt_start == -1) + nonopt_start = optind; + else if (nonopt_end != -1) { + permute_args(nonopt_start, nonopt_end, + optind, nargv); + nonopt_start = optind - + (nonopt_end - nonopt_start); + nonopt_end = -1; + } + optind++; + /* process next argument */ + goto start; + } + if (nonopt_start != -1 && nonopt_end == -1) + nonopt_end = optind; + + /* + * If we have "-" do nothing, if "--" we are done. + */ + if (place[1] != '\0' && *++place == '-' && place[1] == '\0') { + optind++; + place = EMSG; + /* + * We found an option (--), so if we skipped + * non-options, we have to permute. + */ + if (nonopt_end != -1) { + permute_args(nonopt_start, nonopt_end, + optind, nargv); + optind -= nonopt_end - nonopt_start; + } + nonopt_start = nonopt_end = -1; + return (-1); + } + } + + /* + * Check long options if: + * 1) we were passed some + * 2) the arg is not just "-" + * 3) either the arg starts with -- we are getopt_long_only() + */ + if (long_options != NULL && place != nargv[optind] && + (*place == '-' || (flags & FLAG_LONGONLY))) { + short_too = 0; + if (*place == '-') + place++; /* --foo long option */ + else if (*place != ':' && strchr(options, *place) != NULL) + short_too = 1; /* could be short option too */ + + optchar = parse_long_options(nargv, options, long_options, + idx, short_too); + if (optchar != -1) { + place = EMSG; + return (optchar); + } + } + + if ((optchar = (int)*place++) == (int)':' || + (optchar == (int)'-' && *place != '\0') || + (oli = strchr(options, optchar)) == NULL) { + /* + * If the user specified "-" and '-' isn't listed in + * options, return -1 (non-option) as per POSIX. + * Otherwise, it is an unknown option character (or ':'). + */ + if (optchar == (int)'-' && *place == '\0') + return (-1); + if (!*place) + ++optind; + if (PRINT_ERROR) + warnx(illoptchar, optchar); + optopt = optchar; + return (BADCH); + } + if (long_options != NULL && optchar == 'W' && oli[1] == ';') { + /* -W long-option */ + if (*place) /* no space */ + /* NOTHING */; + else if (++optind >= nargc) { /* no arg */ + place = EMSG; + if (PRINT_ERROR) + warnx(recargchar, optchar); + optopt = optchar; + return (BADARG); + } else /* white space */ + place = nargv[optind]; + optchar = parse_long_options(nargv, options, long_options, + idx, 0); + place = EMSG; + return (optchar); + } + if (*++oli != ':') { /* doesn't take argument */ + if (!*place) + ++optind; + } else { /* takes (optional) argument */ + optarg = NULL; + if (*place) /* no white space */ + optarg = place; + else if (oli[1] != ':') { /* arg not optional */ + if (++optind >= nargc) { /* no arg */ + place = EMSG; + if (PRINT_ERROR) + warnx(recargchar, optchar); + optopt = optchar; + return (BADARG); + } else + optarg = nargv[optind]; + } + place = EMSG; + ++optind; + } + /* dump back option letter */ + return (optchar); +} + + +/* + * getopt -- + * Parse argc/argv argument vector. + * + * [eventually this will replace the BSD getopt] + */ +int +getopt(int nargc, char * const *nargv, const char *options) +{ + + /* + * We don't pass FLAG_PERMUTE to getopt_internal() since + * the BSD getopt(3) (unlike GNU) has never done this. + * + * Furthermore, since many privileged programs call getopt() + * before dropping privileges it makes sense to keep things + * as simple (and bug-free) as possible. + */ + return (getopt_internal(nargc, nargv, options, NULL, NULL, 0)); +} + + +/* + * getopt_long -- + * Parse argc/argv argument vector. + */ +int +getopt_long(int nargc, char * const *nargv, const char *options, + const struct option *long_options, int *idx) +{ + + return (getopt_internal(nargc, nargv, options, long_options, idx, + FLAG_PERMUTE)); +} + +/* + * getopt_long_only -- + * Parse argc/argv argument vector. + */ +int +getopt_long_only(int nargc, char * const *nargv, const char *options, + const struct option *long_options, int *idx) +{ + + return (getopt_internal(nargc, nargv, options, long_options, idx, + FLAG_PERMUTE|FLAG_LONGONLY)); +} +#endif /* REPLACE_GETOPT */ +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include +#include + +#include "argtable3.h" + + +char * arg_strptime(const char *buf, const char *fmt, struct tm *tm); + + +static void arg_date_resetfn(struct arg_date *parent) +{ + ARG_TRACE(("%s:resetfn(%p)\n", __FILE__, parent)); + parent->count = 0; +} + + +static int arg_date_scanfn(struct arg_date *parent, const char *argval) +{ + int errorcode = 0; + + if (parent->count == parent->hdr.maxcount) + { + errorcode = EMAXCOUNT; + } + else if (!argval) + { + /* no argument value was given, leave parent->tmval[] unaltered but still count it */ + parent->count++; + } + else + { + const char *pend; + struct tm tm = parent->tmval[parent->count]; + + /* parse the given argument value, store result in parent->tmval[] */ + pend = arg_strptime(argval, parent->format, &tm); + if (pend && pend[0] == '\0') + parent->tmval[parent->count++] = tm; + else + errorcode = EBADDATE; + } + + ARG_TRACE(("%s:scanfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static int arg_date_checkfn(struct arg_date *parent) +{ + int errorcode = (parent->count < parent->hdr.mincount) ? EMINCOUNT : 0; + + ARG_TRACE(("%s:checkfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static void arg_date_errorfn( + struct arg_date *parent, + FILE *fp, + int errorcode, + const char *argval, + const char *progname) +{ + const char *shortopts = parent->hdr.shortopts; + const char *longopts = parent->hdr.longopts; + const char *datatype = parent->hdr.datatype; + + /* make argval NULL safe */ + argval = argval ? argval : ""; + + fprintf(fp, "%s: ", progname); + switch(errorcode) + { + case EMINCOUNT: + fputs("missing option ", fp); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + + case EMAXCOUNT: + fputs("excess option ", fp); + arg_print_option(fp, shortopts, longopts, argval, "\r\n"); + break; + + case EBADDATE: + { + struct tm tm; + char buff[200]; + + fprintf(fp, "illegal timestamp format \"%s\"\r\n", argval); + memset(&tm, 0, sizeof(tm)); + arg_strptime("1999-12-31 23:59:59", "%F %H:%M:%S", &tm); + strftime(buff, sizeof(buff), parent->format, &tm); + printf("correct format is \"%s\"\r\n", buff); + break; + } + } +} + + +struct arg_date * arg_date0( + const char * shortopts, + const char * longopts, + const char * format, + const char *datatype, + const char *glossary) +{ + return arg_daten(shortopts, longopts, format, datatype, 0, 1, glossary); +} + + +struct arg_date * arg_date1( + const char * shortopts, + const char * longopts, + const char * format, + const char *datatype, + const char *glossary) +{ + return arg_daten(shortopts, longopts, format, datatype, 1, 1, glossary); +} + + +struct arg_date * arg_daten( + const char * shortopts, + const char * longopts, + const char * format, + const char *datatype, + int mincount, + int maxcount, + const char *glossary) +{ + size_t nbytes; + struct arg_date *result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + /* default time format is the national date format for the locale */ + if (!format) + format = "%x"; + + nbytes = sizeof(struct arg_date) /* storage for struct arg_date */ + + maxcount * sizeof(struct tm); /* storage for tmval[maxcount] array */ + + /* allocate storage for the arg_date struct + tmval[] array. */ + /* we use calloc because we want the tmval[] array zero filled. */ + result = (struct arg_date *)at3_calloc(1, nbytes); + if (result) + { + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : format; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn *)arg_date_resetfn; + result->hdr.scanfn = (arg_scanfn *)arg_date_scanfn; + result->hdr.checkfn = (arg_checkfn *)arg_date_checkfn; + result->hdr.errorfn = (arg_errorfn *)arg_date_errorfn; + + /* store the tmval[maxcount] array immediately after the arg_date struct */ + result->tmval = (struct tm *)(result + 1); + + /* init the remaining arg_date member variables */ + result->count = 0; + result->format = format; + } + + ARG_TRACE(("arg_daten() returns %p\n", result)); + return result; +} + + +/*- + * Copyright (c) 1997, 1998, 2005, 2008 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code was contributed to The NetBSD Foundation by Klaus Klein. + * Heavily optimised by David Laight + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +/* + * We do not implement alternate representations. However, we always + * check whether a given modifier is allowed for a certain conversion. + */ +#define ALT_E 0x01 +#define ALT_O 0x02 +#define LEGAL_ALT(x) { if (alt_format & ~(x)) return (0); } +#define TM_YEAR_BASE (1900) + +static int conv_num(const char * *, int *, int, int); + +static const char *day[7] = { + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", + "Friday", "Saturday" +}; + +static const char *abday[7] = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" +}; + +static const char *mon[12] = { + "January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December" +}; + +static const char *abmon[12] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" +}; + +static const char *am_pm[2] = { + "AM", "PM" +}; + + +static int arg_strcasecmp(const char *s1, const char *s2) +{ + const unsigned char *us1 = (const unsigned char *)s1; + const unsigned char *us2 = (const unsigned char *)s2; + while (tolower(*us1) == tolower(*us2++)) + if (*us1++ == '\0') + return 0; + + return tolower(*us1) - tolower(*--us2); +} + + +static int arg_strncasecmp(const char *s1, const char *s2, size_t n) +{ + if (n != 0) + { + const unsigned char *us1 = (const unsigned char *)s1; + const unsigned char *us2 = (const unsigned char *)s2; + do + { + if (tolower(*us1) != tolower(*us2++)) + return tolower(*us1) - tolower(*--us2); + + if (*us1++ == '\0') + break; + } while (--n != 0); + } + + return 0; +} + + +char * arg_strptime(const char *buf, const char *fmt, struct tm *tm) +{ + char c; + const char *bp; + size_t len = 0; + int alt_format, i, split_year = 0; + + bp = buf; + + while ((c = *fmt) != '\0') { + /* Clear `alternate' modifier prior to new conversion. */ + alt_format = 0; + + /* Eat up white-space. */ + if (isspace((int) c)) { + while (isspace((int) *bp)) + bp++; + + fmt++; + continue; + } + + if ((c = *fmt++) != '%') + goto literal; + + +again: + switch (c = *fmt++) + { + case '%': /* "%%" is converted to "%". */ +literal: + if (c != *bp++) + return (0); + break; + + /* + * "Alternative" modifiers. Just set the appropriate flag + * and start over again. + */ + case 'E': /* "%E?" alternative conversion modifier. */ + LEGAL_ALT(0); + alt_format |= ALT_E; + goto again; + + case 'O': /* "%O?" alternative conversion modifier. */ + LEGAL_ALT(0); + alt_format |= ALT_O; + goto again; + + /* + * "Complex" conversion rules, implemented through recursion. + */ + case 'c': /* Date and time, using the locale's format. */ + LEGAL_ALT(ALT_E); + bp = arg_strptime(bp, "%x %X", tm); + if (!bp) + return (0); + break; + + case 'D': /* The date as "%m/%d/%y". */ + LEGAL_ALT(0); + bp = arg_strptime(bp, "%m/%d/%y", tm); + if (!bp) + return (0); + break; + + case 'R': /* The time as "%H:%M". */ + LEGAL_ALT(0); + bp = arg_strptime(bp, "%H:%M", tm); + if (!bp) + return (0); + break; + + case 'r': /* The time in 12-hour clock representation. */ + LEGAL_ALT(0); + bp = arg_strptime(bp, "%I:%M:%S %p", tm); + if (!bp) + return (0); + break; + + case 'T': /* The time as "%H:%M:%S". */ + LEGAL_ALT(0); + bp = arg_strptime(bp, "%H:%M:%S", tm); + if (!bp) + return (0); + break; + + case 'X': /* The time, using the locale's format. */ + LEGAL_ALT(ALT_E); + bp = arg_strptime(bp, "%H:%M:%S", tm); + if (!bp) + return (0); + break; + + case 'x': /* The date, using the locale's format. */ + LEGAL_ALT(ALT_E); + bp = arg_strptime(bp, "%m/%d/%y", tm); + if (!bp) + return (0); + break; + + /* + * "Elementary" conversion rules. + */ + case 'A': /* The day of week, using the locale's form. */ + case 'a': + LEGAL_ALT(0); + for (i = 0; i < 7; i++) { + /* Full name. */ + len = strlen(day[i]); + if (arg_strncasecmp(day[i], bp, len) == 0) + break; + + /* Abbreviated name. */ + len = strlen(abday[i]); + if (arg_strncasecmp(abday[i], bp, len) == 0) + break; + } + + /* Nothing matched. */ + if (i == 7) + return (0); + + tm->tm_wday = i; + bp += len; + break; + + case 'B': /* The month, using the locale's form. */ + case 'b': + case 'h': + LEGAL_ALT(0); + for (i = 0; i < 12; i++) { + /* Full name. */ + len = strlen(mon[i]); + if (arg_strncasecmp(mon[i], bp, len) == 0) + break; + + /* Abbreviated name. */ + len = strlen(abmon[i]); + if (arg_strncasecmp(abmon[i], bp, len) == 0) + break; + } + + /* Nothing matched. */ + if (i == 12) + return (0); + + tm->tm_mon = i; + bp += len; + break; + + case 'C': /* The century number. */ + LEGAL_ALT(ALT_E); + if (!(conv_num(&bp, &i, 0, 99))) + return (0); + + if (split_year) { + tm->tm_year = (tm->tm_year % 100) + (i * 100); + } else { + tm->tm_year = i * 100; + split_year = 1; + } + break; + + case 'd': /* The day of month. */ + case 'e': + LEGAL_ALT(ALT_O); + if (!(conv_num(&bp, &tm->tm_mday, 1, 31))) + return (0); + break; + + case 'k': /* The hour (24-hour clock representation). */ + LEGAL_ALT(0); + /* FALLTHROUGH */ + case 'H': + LEGAL_ALT(ALT_O); + if (!(conv_num(&bp, &tm->tm_hour, 0, 23))) + return (0); + break; + + case 'l': /* The hour (12-hour clock representation). */ + LEGAL_ALT(0); + /* FALLTHROUGH */ + case 'I': + LEGAL_ALT(ALT_O); + if (!(conv_num(&bp, &tm->tm_hour, 1, 12))) + return (0); + if (tm->tm_hour == 12) + tm->tm_hour = 0; + break; + + case 'j': /* The day of year. */ + LEGAL_ALT(0); + if (!(conv_num(&bp, &i, 1, 366))) + return (0); + tm->tm_yday = i - 1; + break; + + case 'M': /* The minute. */ + LEGAL_ALT(ALT_O); + if (!(conv_num(&bp, &tm->tm_min, 0, 59))) + return (0); + break; + + case 'm': /* The month. */ + LEGAL_ALT(ALT_O); + if (!(conv_num(&bp, &i, 1, 12))) + return (0); + tm->tm_mon = i - 1; + break; + + case 'p': /* The locale's equivalent of AM/PM. */ + LEGAL_ALT(0); + /* AM? */ + if (arg_strcasecmp(am_pm[0], bp) == 0) { + if (tm->tm_hour > 11) + return (0); + + bp += strlen(am_pm[0]); + break; + } + /* PM? */ + else if (arg_strcasecmp(am_pm[1], bp) == 0) { + if (tm->tm_hour > 11) + return (0); + + tm->tm_hour += 12; + bp += strlen(am_pm[1]); + break; + } + + /* Nothing matched. */ + return (0); + + case 'S': /* The seconds. */ + LEGAL_ALT(ALT_O); + if (!(conv_num(&bp, &tm->tm_sec, 0, 61))) + return (0); + break; + + case 'U': /* The week of year, beginning on sunday. */ + case 'W': /* The week of year, beginning on monday. */ + LEGAL_ALT(ALT_O); + /* + * XXX This is bogus, as we can not assume any valid + * information present in the tm structure at this + * point to calculate a real value, so just check the + * range for now. + */ + if (!(conv_num(&bp, &i, 0, 53))) + return (0); + break; + + case 'w': /* The day of week, beginning on sunday. */ + LEGAL_ALT(ALT_O); + if (!(conv_num(&bp, &tm->tm_wday, 0, 6))) + return (0); + break; + + case 'Y': /* The year. */ + LEGAL_ALT(ALT_E); + if (!(conv_num(&bp, &i, 0, 9999))) + return (0); + + tm->tm_year = i - TM_YEAR_BASE; + break; + + case 'y': /* The year within 100 years of the epoch. */ + LEGAL_ALT(ALT_E | ALT_O); + if (!(conv_num(&bp, &i, 0, 99))) + return (0); + + if (split_year) { + tm->tm_year = ((tm->tm_year / 100) * 100) + i; + break; + } + split_year = 1; + if (i <= 68) + tm->tm_year = i + 2000 - TM_YEAR_BASE; + else + tm->tm_year = i + 1900 - TM_YEAR_BASE; + break; + + /* + * Miscellaneous conversions. + */ + case 'n': /* Any kind of white-space. */ + case 't': + LEGAL_ALT(0); + while (isspace((int) *bp)) + bp++; + break; + + + default: /* Unknown/unsupported conversion. */ + return (0); + } + + + } + + /* LINTED functional specification */ + return ((char *)bp); +} + + +static int conv_num(const char * *buf, int *dest, int llim, int ulim) +{ + int result = 0; + + /* The limit also determines the number of valid digits. */ + int rulim = ulim; + + if (**buf < '0' || **buf > '9') + return (0); + + do { + result *= 10; + result += *(*buf)++ - '0'; + rulim /= 10; + } while ((result * 10 <= ulim) && rulim && **buf >= '0' && **buf <= '9'); + + if (result < llim || result > ulim) + return (0); + + *dest = result; + return (1); +} +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include + +#include "argtable3.h" + + +static void arg_dbl_resetfn(struct arg_dbl *parent) +{ + ARG_TRACE(("%s:resetfn(%p)\n", __FILE__, parent)); + parent->count = 0; +} + + +static int arg_dbl_scanfn(struct arg_dbl *parent, const char *argval) +{ + int errorcode = 0; + + if (parent->count == parent->hdr.maxcount) + { + /* maximum number of arguments exceeded */ + errorcode = EMAXCOUNT; + } + else if (!argval) + { + /* a valid argument with no argument value was given. */ + /* This happens when an optional argument value was invoked. */ + /* leave parent argument value unaltered but still count the argument. */ + parent->count++; + } + else + { + double val; + char *end; + + /* extract double from argval into val */ + val = strtod(argval, &end); + + /* if success then store result in parent->dval[] array otherwise return error*/ + if (*end == 0) + parent->dval[parent->count++] = val; + else + errorcode = EBADDOUBLE; + } + + ARG_TRACE(("%s:scanfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static int arg_dbl_checkfn(struct arg_dbl *parent) +{ + int errorcode = (parent->count < parent->hdr.mincount) ? EMINCOUNT : 0; + + ARG_TRACE(("%s:checkfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static void arg_dbl_errorfn( + struct arg_dbl *parent, + FILE *fp, + int errorcode, + const char *argval, + const char *progname) +{ + const char *shortopts = parent->hdr.shortopts; + const char *longopts = parent->hdr.longopts; + const char *datatype = parent->hdr.datatype; + + /* make argval NULL safe */ + argval = argval ? argval : ""; + + fprintf(fp, "%s: ", progname); + switch(errorcode) + { + case EMINCOUNT: + fputs("missing option ", fp); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + + case EMAXCOUNT: + fputs("excess option ", fp); + arg_print_option(fp, shortopts, longopts, argval, "\r\n"); + break; + + case EBADDOUBLE: + fprintf(fp, "invalid argument \"%s\" to option ", argval); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + } +} + + +struct arg_dbl * arg_dbl0( + const char * shortopts, + const char * longopts, + const char *datatype, + const char *glossary) +{ + return arg_dbln(shortopts, longopts, datatype, 0, 1, glossary); +} + + +struct arg_dbl * arg_dbl1( + const char * shortopts, + const char * longopts, + const char *datatype, + const char *glossary) +{ + return arg_dbln(shortopts, longopts, datatype, 1, 1, glossary); +} + + +struct arg_dbl * arg_dbln( + const char * shortopts, + const char * longopts, + const char *datatype, + int mincount, + int maxcount, + const char *glossary) +{ + size_t nbytes; + struct arg_dbl *result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(struct arg_dbl) /* storage for struct arg_dbl */ + + (maxcount + 1) * sizeof(double); /* storage for dval[maxcount] array plus one extra for padding to memory boundary */ + + result = (struct arg_dbl *)at3_malloc(nbytes); + if (result) + { + size_t addr; + size_t rem; + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn *)arg_dbl_resetfn; + result->hdr.scanfn = (arg_scanfn *)arg_dbl_scanfn; + result->hdr.checkfn = (arg_checkfn *)arg_dbl_checkfn; + result->hdr.errorfn = (arg_errorfn *)arg_dbl_errorfn; + + /* Store the dval[maxcount] array on the first double boundary that + * immediately follows the arg_dbl struct. We do the memory alignment + * purely for SPARC and Motorola systems. They require floats and + * doubles to be aligned on natural boundaries. + */ + addr = (size_t)(result + 1); + rem = addr % sizeof(double); + result->dval = (double *)(addr + sizeof(double) - rem); + ARG_TRACE(("addr=%p, dval=%p, sizeof(double)=%d rem=%d\n", addr, result->dval, (int)sizeof(double), (int)rem)); + + result->count = 0; + } + + ARG_TRACE(("arg_dbln() returns %p\n", result)); + return result; +} +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include + +#include "argtable3.h" + + +static void arg_end_resetfn(struct arg_end *parent) +{ + ARG_TRACE(("%s:resetfn(%p)\n", __FILE__, parent)); + parent->count = 0; +} + +static void arg_end_errorfn( + void *parent, + FILE *fp, + int error, + const char *argval, + const char *progname) +{ + /* suppress unreferenced formal parameter warning */ + (void)parent; + + progname = progname ? progname : ""; + argval = argval ? argval : ""; + + fprintf(fp, "%s: ", progname); + switch(error) + { + case ARG_ELIMIT: + fputs("too many errors to display", fp); + break; + case ARG_EMALLOC: + fputs("insufficent memory", fp); + break; + case ARG_ENOMATCH: + fprintf(fp, "unexpected argument \"%s\"", argval); + break; + case ARG_EMISSARG: + fprintf(fp, "option \"%s\" requires an argument", argval); + break; + case ARG_ELONGOPT: + fprintf(fp, "invalid option \"%s\"", argval); + break; + default: + fprintf(fp, "invalid option \"-%c\"", error); + break; + } + + fputs("\r\n", fp); +} + + +struct arg_end * arg_end(int maxcount) +{ + size_t nbytes; + struct arg_end *result; + + nbytes = sizeof(struct arg_end) + + maxcount * sizeof(int) /* storage for int error[maxcount] array*/ + + maxcount * sizeof(void *) /* storage for void* parent[maxcount] array */ + + maxcount * sizeof(char *); /* storage for char* argval[maxcount] array */ + + result = (struct arg_end *)at3_malloc(nbytes); + if (result) + { + /* init the arg_hdr struct */ + result->hdr.flag = ARG_TERMINATOR; + result->hdr.shortopts = NULL; + result->hdr.longopts = NULL; + result->hdr.datatype = NULL; + result->hdr.glossary = NULL; + result->hdr.mincount = 1; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn *)arg_end_resetfn; + result->hdr.scanfn = NULL; + result->hdr.checkfn = NULL; + result->hdr.errorfn = (arg_errorfn *)arg_end_errorfn; + + /* store error[maxcount] array immediately after struct arg_end */ + result->error = (int *)(result + 1); + + /* store parent[maxcount] array immediately after error[] array */ + result->parent = (void * *)(result->error + maxcount ); + + /* store argval[maxcount] array immediately after parent[] array */ + result->argval = (const char * *)(result->parent + maxcount ); + } + + ARG_TRACE(("arg_end(%d) returns %p\n", maxcount, result)); + return result; +} + + +void arg_print_errors(FILE * fp, struct arg_end * end, const char * progname) +{ + int i; + ARG_TRACE(("arg_errors()\n")); + for (i = 0; i < end->count; i++) + { + struct arg_hdr *errorparent = (struct arg_hdr *)(end->parent[i]); + if (errorparent->errorfn) + errorparent->errorfn(end->parent[i], + fp, + end->error[i], + end->argval[i], + progname); + } +} +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include +#include + +#include "argtable3.h" + +#ifdef WIN32 +# define FILESEPARATOR1 '\\' +# define FILESEPARATOR2 '/' +#else +# define FILESEPARATOR1 '/' +# define FILESEPARATOR2 '/' +#endif + + +static void arg_file_resetfn(struct arg_file *parent) +{ + ARG_TRACE(("%s:resetfn(%p)\n", __FILE__, parent)); + parent->count = 0; +} + + +/* Returns ptr to the base filename within *filename */ +static const char * arg_basename(const char *filename) +{ + const char *result = NULL, *result1, *result2; + + /* Find the last occurrence of eother file separator character. */ + /* Two alternative file separator chars are supported as legal */ + /* file separators but not both together in the same filename. */ + result1 = (filename ? strrchr(filename, FILESEPARATOR1) : NULL); + result2 = (filename ? strrchr(filename, FILESEPARATOR2) : NULL); + + if (result2) + result = result2 + 1; /* using FILESEPARATOR2 (the alternative file separator) */ + + if (result1) + result = result1 + 1; /* using FILESEPARATOR1 (the preferred file separator) */ + + if (!result) + result = filename; /* neither file separator was found so basename is the whole filename */ + + /* special cases of "." and ".." are not considered basenames */ + if (result && ( strcmp(".", result) == 0 || strcmp("..", result) == 0 )) + result = filename + strlen(filename); + + return result; +} + + +/* Returns ptr to the file extension within *basename */ +static const char * arg_extension(const char *basename) +{ + /* find the last occurrence of '.' in basename */ + const char *result = (basename ? strrchr(basename, '.') : NULL); + + /* if no '.' was found then return pointer to end of basename */ + if (basename && !result) + result = basename + strlen(basename); + + /* special case: basenames with a single leading dot (eg ".foo") are not considered as true extensions */ + if (basename && result == basename) + result = basename + strlen(basename); + + /* special case: empty extensions (eg "foo.","foo..") are not considered as true extensions */ + if (basename && result && result[1] == '\0') + result = basename + strlen(basename); + + return result; +} + + +static int arg_file_scanfn(struct arg_file *parent, const char *argval) +{ + int errorcode = 0; + + if (parent->count == parent->hdr.maxcount) + { + /* maximum number of arguments exceeded */ + errorcode = EMAXCOUNT; + } + else if (!argval) + { + /* a valid argument with no argument value was given. */ + /* This happens when an optional argument value was invoked. */ + /* leave parent arguiment value unaltered but still count the argument. */ + parent->count++; + } + else + { + parent->filename[parent->count] = argval; + parent->basename[parent->count] = arg_basename(argval); + parent->extension[parent->count] = + arg_extension(parent->basename[parent->count]); /* only seek extensions within the basename (not the file path)*/ + parent->count++; + } + + ARG_TRACE(("%s4:scanfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static int arg_file_checkfn(struct arg_file *parent) +{ + int errorcode = (parent->count < parent->hdr.mincount) ? EMINCOUNT : 0; + + ARG_TRACE(("%s:checkfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static void arg_file_errorfn( + struct arg_file *parent, + FILE *fp, + int errorcode, + const char *argval, + const char *progname) +{ + const char *shortopts = parent->hdr.shortopts; + const char *longopts = parent->hdr.longopts; + const char *datatype = parent->hdr.datatype; + + /* make argval NULL safe */ + argval = argval ? argval : ""; + + fprintf(fp, "%s: ", progname); + switch(errorcode) + { + case EMINCOUNT: + fputs("missing option ", fp); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + + case EMAXCOUNT: + fputs("excess option ", fp); + arg_print_option(fp, shortopts, longopts, argval, "\r\n"); + break; + + default: + fprintf(fp, "unknown error at \"%s\"\r\n", argval); + } +} + + +struct arg_file * arg_file0( + const char * shortopts, + const char * longopts, + const char *datatype, + const char *glossary) +{ + return arg_filen(shortopts, longopts, datatype, 0, 1, glossary); +} + + +struct arg_file * arg_file1( + const char * shortopts, + const char * longopts, + const char *datatype, + const char *glossary) +{ + return arg_filen(shortopts, longopts, datatype, 1, 1, glossary); +} + + +struct arg_file * arg_filen( + const char * shortopts, + const char * longopts, + const char *datatype, + int mincount, + int maxcount, + const char *glossary) +{ + size_t nbytes; + struct arg_file *result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(struct arg_file) /* storage for struct arg_file */ + + sizeof(char *) * maxcount /* storage for filename[maxcount] array */ + + sizeof(char *) * maxcount /* storage for basename[maxcount] array */ + + sizeof(char *) * maxcount; /* storage for extension[maxcount] array */ + + result = (struct arg_file *)at3_malloc(nbytes); + if (result) + { + int i; + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.glossary = glossary; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn *)arg_file_resetfn; + result->hdr.scanfn = (arg_scanfn *)arg_file_scanfn; + result->hdr.checkfn = (arg_checkfn *)arg_file_checkfn; + result->hdr.errorfn = (arg_errorfn *)arg_file_errorfn; + + /* store the filename,basename,extension arrays immediately after the arg_file struct */ + result->filename = (const char * *)(result + 1); + result->basename = result->filename + maxcount; + result->extension = result->basename + maxcount; + result->count = 0; + + /* foolproof the string pointers by initialising them with empty strings */ + for (i = 0; i < maxcount; i++) + { + result->filename[i] = ""; + result->basename[i] = ""; + result->extension[i] = ""; + } + } + + ARG_TRACE(("arg_filen() returns %p\n", result)); + return result; +} +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include +#include +#include + +#include "argtable3.h" + + +static void arg_int_resetfn(struct arg_int *parent) +{ + ARG_TRACE(("%s:resetfn(%p)\n", __FILE__, parent)); + parent->count = 0; +} + + +/* strtol0x() is like strtol() except that the numeric string is */ +/* expected to be prefixed by "0X" where X is a user supplied char. */ +/* The string may optionally be prefixed by white space and + or - */ +/* as in +0X123 or -0X123. */ +/* Once the prefix has been scanned, the remainder of the numeric */ +/* string is converted using strtol() with the given base. */ +/* eg: to parse hex str="-0X12324", specify X='X' and base=16. */ +/* eg: to parse oct str="+0o12324", specify X='O' and base=8. */ +/* eg: to parse bin str="-0B01010", specify X='B' and base=2. */ +/* Failure of conversion is indicated by result where *endptr==str. */ +static long int strtol0X(const char * str, + const char * *endptr, + char X, + int base) +{ + long int val; /* stores result */ + int s = 1; /* sign is +1 or -1 */ + const char *ptr = str; /* ptr to current position in str */ + + /* skip leading whitespace */ + while (isspace((int) *ptr)) + ptr++; + /* printf("1) %s\n",ptr); */ + + /* scan optional sign character */ + switch (*ptr) + { + case '+': + ptr++; + s = 1; + break; + case '-': + ptr++; + s = -1; + break; + default: + s = 1; + break; + } + /* printf("2) %s\n",ptr); */ + + /* '0X' prefix */ + if ((*ptr++) != '0') + { + /* printf("failed to detect '0'\n"); */ + *endptr = str; + return 0; + } + /* printf("3) %s\n",ptr); */ + if (toupper((int) *ptr++) != toupper((int) X)) + { + /* printf("failed to detect '%c'\n",X); */ + *endptr = str; + return 0; + } + /* printf("4) %s\n",ptr); */ + + /* attempt conversion on remainder of string using strtol() */ + val = strtol(ptr, (char * *)endptr, base); + if (*endptr == ptr) + { + /* conversion failed */ + *endptr = str; + return 0; + } + + /* success */ + return s * val; +} + + +/* Returns 1 if str matches suffix (case insensitive). */ +/* Str may contain trailing whitespace, but nothing else. */ +static int detectsuffix(const char *str, const char *suffix) +{ + /* scan pairwise through strings until mismatch detected */ + while( toupper((int) *str) == toupper((int) *suffix) ) + { + /* printf("'%c' '%c'\n", *str, *suffix); */ + + /* return 1 (success) if match persists until the string terminator */ + if (*str == '\0') + return 1; + + /* next chars */ + str++; + suffix++; + } + /* printf("'%c' '%c' mismatch\n", *str, *suffix); */ + + /* return 0 (fail) if the matching did not consume the entire suffix */ + if (*suffix != 0) + return 0; /* failed to consume entire suffix */ + + /* skip any remaining whitespace in str */ + while (isspace((int) *str)) + str++; + + /* return 1 (success) if we have reached end of str else return 0 (fail) */ + return (*str == '\0') ? 1 : 0; +} + + +static int arg_int_scanfn(struct arg_int *parent, const char *argval) +{ + int errorcode = 0; + + if (parent->count == parent->hdr.maxcount) + { + /* maximum number of arguments exceeded */ + errorcode = EMAXCOUNT; + } + else if (!argval) + { + /* a valid argument with no argument value was given. */ + /* This happens when an optional argument value was invoked. */ + /* leave parent arguiment value unaltered but still count the argument. */ + parent->count++; + } + else + { + long int val; + const char *end; + + /* attempt to extract hex integer (eg: +0x123) from argval into val conversion */ + val = strtol0X(argval, &end, 'X', 16); + if (end == argval) + { + /* hex failed, attempt octal conversion (eg +0o123) */ + val = strtol0X(argval, &end, 'O', 8); + if (end == argval) + { + /* octal failed, attempt binary conversion (eg +0B101) */ + val = strtol0X(argval, &end, 'B', 2); + if (end == argval) + { + /* binary failed, attempt decimal conversion with no prefix (eg 1234) */ + val = strtol(argval, (char * *)&end, 10); + if (end == argval) + { + /* all supported number formats failed */ + return EBADINT; + } + } + } + } + + /* Safety check for integer overflow. WARNING: this check */ + /* achieves nothing on machines where size(int)==size(long). */ + if ( val > INT_MAX || val < INT_MIN ) + errorcode = EOVERFLOW; + + /* Detect any suffixes (KB,MB,GB) and multiply argument value appropriately. */ + /* We need to be mindful of integer overflows when using such big numbers. */ + if (detectsuffix(end, "KB")) /* kilobytes */ + { + if ( val > (INT_MAX / 1024) || val < (INT_MIN / 1024) ) + errorcode = EOVERFLOW; /* Overflow would occur if we proceed */ + else + val *= 1024; /* 1KB = 1024 */ + } + else if (detectsuffix(end, "MB")) /* megabytes */ + { + if ( val > (INT_MAX / 1048576) || val < (INT_MIN / 1048576) ) + errorcode = EOVERFLOW; /* Overflow would occur if we proceed */ + else + val *= 1048576; /* 1MB = 1024*1024 */ + } + else if (detectsuffix(end, "GB")) /* gigabytes */ + { + if ( val > (INT_MAX / 1073741824) || val < (INT_MIN / 1073741824) ) + errorcode = EOVERFLOW; /* Overflow would occur if we proceed */ + else + val *= 1073741824; /* 1GB = 1024*1024*1024 */ + } + else if (!detectsuffix(end, "")) + errorcode = EBADINT; /* invalid suffix detected */ + + /* if success then store result in parent->ival[] array */ + if (errorcode == 0) + parent->ival[parent->count++] = val; + } + + /* printf("%s:scanfn(%p,%p) returns %d\n",__FILE__,parent,argval,errorcode); */ + return errorcode; +} + + +static int arg_int_checkfn(struct arg_int *parent) +{ + int errorcode = (parent->count < parent->hdr.mincount) ? EMINCOUNT : 0; + /*printf("%s:checkfn(%p) returns %d\n",__FILE__,parent,errorcode);*/ + return errorcode; +} + + +static void arg_int_errorfn( + struct arg_int *parent, + FILE *fp, + int errorcode, + const char *argval, + const char *progname) +{ + const char *shortopts = parent->hdr.shortopts; + const char *longopts = parent->hdr.longopts; + const char *datatype = parent->hdr.datatype; + + /* make argval NULL safe */ + argval = argval ? argval : ""; + + fprintf(fp, "%s: ", progname); + switch(errorcode) + { + case EMINCOUNT: + fputs("missing option ", fp); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + + case EMAXCOUNT: + fputs("excess option ", fp); + arg_print_option(fp, shortopts, longopts, argval, "\r\n"); + break; + + case EBADINT: + fprintf(fp, "invalid argument \"%s\" to option ", argval); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + + case EOVERFLOW: + fputs("integer overflow at option ", fp); + arg_print_option(fp, shortopts, longopts, datatype, " "); + fprintf(fp, "(%s is too large)\r\n", argval); + break; + } +} + + +struct arg_int * arg_int0( + const char *shortopts, + const char *longopts, + const char *datatype, + const char *glossary) +{ + return arg_intn(shortopts, longopts, datatype, 0, 1, glossary); +} + + +struct arg_int * arg_int1( + const char *shortopts, + const char *longopts, + const char *datatype, + const char *glossary) +{ + return arg_intn(shortopts, longopts, datatype, 1, 1, glossary); +} + + +struct arg_int * arg_intn( + const char *shortopts, + const char *longopts, + const char *datatype, + int mincount, + int maxcount, + const char *glossary) +{ + size_t nbytes; + struct arg_int *result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(struct arg_int) /* storage for struct arg_int */ + + maxcount * sizeof(int); /* storage for ival[maxcount] array */ + + result = (struct arg_int *)at3_malloc(nbytes); + if (result) + { + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn *)arg_int_resetfn; + result->hdr.scanfn = (arg_scanfn *)arg_int_scanfn; + result->hdr.checkfn = (arg_checkfn *)arg_int_checkfn; + result->hdr.errorfn = (arg_errorfn *)arg_int_errorfn; + + /* store the ival[maxcount] array immediately after the arg_int struct */ + result->ival = (int *)(result + 1); + result->count = 0; + } + + ARG_TRACE(("arg_intn() returns %p\n", result)); + return result; +} +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include + +#include "argtable3.h" + + +static void arg_lit_resetfn(struct arg_lit *parent) +{ + ARG_TRACE(("%s:resetfn(%p)\n", __FILE__, parent)); + parent->count = 0; +} + + +static int arg_lit_scanfn(struct arg_lit *parent, const char *argval) +{ + int errorcode = 0; + if (parent->count < parent->hdr.maxcount ) + parent->count++; + else + errorcode = EMAXCOUNT; + + ARG_TRACE(("%s:scanfn(%p,%s) returns %d\n", __FILE__, parent, argval, + errorcode)); + return errorcode; +} + + +static int arg_lit_checkfn(struct arg_lit *parent) +{ + int errorcode = (parent->count < parent->hdr.mincount) ? EMINCOUNT : 0; + ARG_TRACE(("%s:checkfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static void arg_lit_errorfn( + struct arg_lit *parent, + FILE *fp, + int errorcode, + const char *argval, + const char *progname) +{ + const char *shortopts = parent->hdr.shortopts; + const char *longopts = parent->hdr.longopts; + const char *datatype = parent->hdr.datatype; + + switch(errorcode) + { + case EMINCOUNT: + fprintf(fp, "%s: missing option ", progname); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + fprintf(fp, "\r\n"); + break; + + case EMAXCOUNT: + fprintf(fp, "%s: extraneous option ", progname); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + } + + ARG_TRACE(("%s:errorfn(%p, %p, %d, %s, %s)\n", __FILE__, parent, fp, + errorcode, argval, progname)); +} + + +struct arg_lit * arg_lit0( + const char * shortopts, + const char * longopts, + const char * glossary) +{ + return arg_litn(shortopts, longopts, 0, 1, glossary); +} + + +struct arg_lit * arg_lit1( + const char *shortopts, + const char *longopts, + const char *glossary) +{ + return arg_litn(shortopts, longopts, 1, 1, glossary); +} + + +struct arg_lit * arg_litn( + const char *shortopts, + const char *longopts, + int mincount, + int maxcount, + const char *glossary) +{ + struct arg_lit *result; + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + result = (struct arg_lit *)at3_malloc(sizeof(struct arg_lit)); + if (result) + { + /* init the arg_hdr struct */ + result->hdr.flag = 0; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = NULL; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn *)arg_lit_resetfn; + result->hdr.scanfn = (arg_scanfn *)arg_lit_scanfn; + result->hdr.checkfn = (arg_checkfn *)arg_lit_checkfn; + result->hdr.errorfn = (arg_errorfn *)arg_lit_errorfn; + + /* init local variables */ + result->count = 0; + } + + ARG_TRACE(("arg_litn() returns %p\n", result)); + return result; +} +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include + +#include "argtable3.h" + +struct arg_rem *arg_rem(const char *datatype, const char *glossary) +{ + struct arg_rem *result = (struct arg_rem *)at3_malloc(sizeof(struct arg_rem)); + if (result) + { + result->hdr.flag = 0; + result->hdr.shortopts = NULL; + result->hdr.longopts = NULL; + result->hdr.datatype = datatype; + result->hdr.glossary = glossary; + result->hdr.mincount = 1; + result->hdr.maxcount = 1; + result->hdr.parent = result; + result->hdr.resetfn = NULL; + result->hdr.scanfn = NULL; + result->hdr.checkfn = NULL; + result->hdr.errorfn = NULL; + } + + ARG_TRACE(("arg_rem() returns %p\n", result)); + return result; +} + +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include +#include + +#include "argtable3.h" + + +#ifndef _TREX_H_ +#define _TREX_H_ +/*************************************************************** + T-Rex a tiny regular expression library + + Copyright (C) 2003-2006 Alberto Demichelis + + This software is provided 'as-is', without any express + or implied warranty. In no event will the authors be held + liable for any damages arising from the use of this software. + + Permission is granted to anyone to use this software for + any purpose, including commercial applications, and to alter + it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; + you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but + is not required. + + 2. Altered source versions must be plainly marked as such, + and must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any + source distribution. + +****************************************************************/ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef _UNICODE +#define TRexChar unsigned short +#define MAX_CHAR 0xFFFF +#define _TREXC(c) L##c +#define trex_strlen wcslen +#define trex_printf wprintf +#else +#define TRexChar char +#define MAX_CHAR 0xFF +#define _TREXC(c) (c) +#define trex_strlen strlen +#define trex_printf printf +#endif + +#ifndef TREX_API +#define TREX_API extern +#endif + +#define TRex_True 1 +#define TRex_False 0 + +#define TREX_ICASE ARG_REX_ICASE + +typedef unsigned int TRexBool; +typedef struct TRex TRex; + +typedef struct { + const TRexChar *begin; + int len; +} TRexMatch; + +TREX_API TRex *trex_compile(const TRexChar *pattern, const TRexChar **error, int flags); +TREX_API void trex_free(TRex *exp); +TREX_API TRexBool trex_match(TRex* exp, const TRexChar* text); +TREX_API TRexBool trex_search(TRex* exp, const TRexChar* text, const TRexChar** out_begin, const TRexChar** out_end); +TREX_API TRexBool trex_searchrange(TRex* exp, const TRexChar* text_begin, const TRexChar* text_end, const TRexChar** out_begin, const TRexChar** out_end); +TREX_API int trex_getsubexpcount(TRex* exp); +TREX_API TRexBool trex_getsubexp(TRex* exp, int n, TRexMatch *subexp); + +#ifdef __cplusplus +} +#endif + +#endif + + + +struct privhdr +{ + const char *pattern; + int flags; +}; + + +static void arg_rex_resetfn(struct arg_rex *parent) +{ + ARG_TRACE(("%s:resetfn(%p)\n", __FILE__, parent)); + parent->count = 0; +} + +static int arg_rex_scanfn(struct arg_rex *parent, const char *argval) +{ + int errorcode = 0; + const TRexChar *error = NULL; + TRex *rex = NULL; + TRexBool is_match = TRex_False; + + if (parent->count == parent->hdr.maxcount ) + { + /* maximum number of arguments exceeded */ + errorcode = EMAXCOUNT; + } + else if (!argval) + { + /* a valid argument with no argument value was given. */ + /* This happens when an optional argument value was invoked. */ + /* leave parent argument value unaltered but still count the argument. */ + parent->count++; + } + else + { + struct privhdr *priv = (struct privhdr *)parent->hdr.priv; + + /* test the current argument value for a match with the regular expression */ + /* if a match is detected, record the argument value in the arg_rex struct */ + + rex = trex_compile(priv->pattern, &error, priv->flags); + is_match = trex_match(rex, argval); + if (!is_match) + errorcode = EREGNOMATCH; + else + parent->sval[parent->count++] = argval; + + trex_free(rex); + } + + ARG_TRACE(("%s:scanfn(%p) returns %d\n",__FILE__,parent,errorcode)); + return errorcode; +} + +static int arg_rex_checkfn(struct arg_rex *parent) +{ + int errorcode = (parent->count < parent->hdr.mincount) ? EMINCOUNT : 0; + //struct privhdr *priv = (struct privhdr*)parent->hdr.priv; + + /* free the regex "program" we constructed in resetfn */ + //regfree(&(priv->regex)); + + /*printf("%s:checkfn(%p) returns %d\n",__FILE__,parent,errorcode);*/ + return errorcode; +} + +static void arg_rex_errorfn(struct arg_rex *parent, + FILE *fp, + int errorcode, + const char *argval, + const char *progname) +{ + const char *shortopts = parent->hdr.shortopts; + const char *longopts = parent->hdr.longopts; + const char *datatype = parent->hdr.datatype; + + /* make argval NULL safe */ + argval = argval ? argval : ""; + + fprintf(fp, "%s: ", progname); + switch(errorcode) + { + case EMINCOUNT: + fputs("missing option ", fp); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + + case EMAXCOUNT: + fputs("excess option ", fp); + arg_print_option(fp, shortopts, longopts, argval, "\r\n"); + break; + + case EREGNOMATCH: + fputs("illegal value ", fp); + arg_print_option(fp, shortopts, longopts, argval, "\r\n"); + break; + + default: + { + //char errbuff[256]; + //regerror(errorcode, NULL, errbuff, sizeof(errbuff)); + //printf("%s\n", errbuff); + } + break; + } +} + + +struct arg_rex * arg_rex0(const char * shortopts, + const char * longopts, + const char * pattern, + const char *datatype, + int flags, + const char *glossary) +{ + return arg_rexn(shortopts, + longopts, + pattern, + datatype, + 0, + 1, + flags, + glossary); +} + +struct arg_rex * arg_rex1(const char * shortopts, + const char * longopts, + const char * pattern, + const char *datatype, + int flags, + const char *glossary) +{ + return arg_rexn(shortopts, + longopts, + pattern, + datatype, + 1, + 1, + flags, + glossary); +} + + +struct arg_rex * arg_rexn(const char * shortopts, + const char * longopts, + const char * pattern, + const char *datatype, + int mincount, + int maxcount, + int flags, + const char *glossary) +{ + size_t nbytes; + struct arg_rex *result; + struct privhdr *priv; + int i; + const TRexChar *error = NULL; + TRex *rex = NULL; + + if (!pattern) + { + printf( + "argtable: ERROR - illegal regular expression pattern \"(NULL)\"\r\n"); + printf("argtable: Bad argument table.\r\n"); + return NULL; + } + + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(struct arg_rex) /* storage for struct arg_rex */ + + sizeof(struct privhdr) /* storage for private arg_rex data */ + + maxcount * sizeof(char *); /* storage for sval[maxcount] array */ + + result = (struct arg_rex *)at3_malloc(nbytes); + if (result == NULL) + return result; + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : pattern; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn *)arg_rex_resetfn; + result->hdr.scanfn = (arg_scanfn *)arg_rex_scanfn; + result->hdr.checkfn = (arg_checkfn *)arg_rex_checkfn; + result->hdr.errorfn = (arg_errorfn *)arg_rex_errorfn; + + /* store the arg_rex_priv struct immediately after the arg_rex struct */ + result->hdr.priv = result + 1; + priv = (struct privhdr *)(result->hdr.priv); + priv->pattern = pattern; + priv->flags = flags; + + /* store the sval[maxcount] array immediately after the arg_rex_priv struct */ + result->sval = (const char * *)(priv + 1); + result->count = 0; + + /* foolproof the string pointers by initializing them to reference empty strings */ + for (i = 0; i < maxcount; i++) + result->sval[i] = ""; + + /* here we construct and destroy a regex representation of the regular + * expression for no other reason than to force any regex errors to be + * trapped now rather than later. If we don't, then errors may go undetected + * until an argument is actually parsed. + */ + + rex = trex_compile(priv->pattern, &error, priv->flags); + if (rex == NULL) + { + ARG_LOG(("argtable: %s \"%s\"\n", error ? error : _TREXC("undefined"), priv->pattern)); + ARG_LOG(("argtable: Bad argument table.\n")); + } + + trex_free(rex); + + ARG_TRACE(("arg_rexn() returns %p\n", result)); + return result; +} + + + +/* see copyright notice in trex.h */ +#include +#include +#include +#include + +#ifdef _UINCODE +#define scisprint iswprint +#define scstrlen wcslen +#define scprintf wprintf +#define _SC(x) L(x) +#else +#define scisprint isprint +#define scstrlen strlen +#define scprintf printf +#define _SC(x) (x) +#endif + +#ifdef _DEBUG +#include + +static const TRexChar *g_nnames[] = +{ + _SC("NONE"),_SC("OP_GREEDY"), _SC("OP_OR"), + _SC("OP_EXPR"),_SC("OP_NOCAPEXPR"),_SC("OP_DOT"), _SC("OP_CLASS"), + _SC("OP_CCLASS"),_SC("OP_NCLASS"),_SC("OP_RANGE"),_SC("OP_CHAR"), + _SC("OP_EOL"),_SC("OP_BOL"),_SC("OP_WB") +}; + +#endif +#define OP_GREEDY (MAX_CHAR+1) // * + ? {n} +#define OP_OR (MAX_CHAR+2) +#define OP_EXPR (MAX_CHAR+3) //parentesis () +#define OP_NOCAPEXPR (MAX_CHAR+4) //parentesis (?:) +#define OP_DOT (MAX_CHAR+5) +#define OP_CLASS (MAX_CHAR+6) +#define OP_CCLASS (MAX_CHAR+7) +#define OP_NCLASS (MAX_CHAR+8) //negates class the [^ +#define OP_RANGE (MAX_CHAR+9) +#define OP_CHAR (MAX_CHAR+10) +#define OP_EOL (MAX_CHAR+11) +#define OP_BOL (MAX_CHAR+12) +#define OP_WB (MAX_CHAR+13) + +#define TREX_SYMBOL_ANY_CHAR ('.') +#define TREX_SYMBOL_GREEDY_ONE_OR_MORE ('+') +#define TREX_SYMBOL_GREEDY_ZERO_OR_MORE ('*') +#define TREX_SYMBOL_GREEDY_ZERO_OR_ONE ('?') +#define TREX_SYMBOL_BRANCH ('|') +#define TREX_SYMBOL_END_OF_STRING ('$') +#define TREX_SYMBOL_BEGINNING_OF_STRING ('^') +#define TREX_SYMBOL_ESCAPE_CHAR ('\\') + + +typedef int TRexNodeType; + +typedef struct tagTRexNode{ + TRexNodeType type; + int left; + int right; + int next; +}TRexNode; + +struct TRex{ + const TRexChar *_eol; + const TRexChar *_bol; + const TRexChar *_p; + int _first; + int _op; + TRexNode *_nodes; + int _nallocated; + int _nsize; + int _nsubexpr; + TRexMatch *_matches; + int _currsubexp; + void *_jmpbuf; + const TRexChar **_error; + int _flags; +}; + +static int trex_list(TRex *exp); + +static int trex_newnode(TRex *exp, TRexNodeType type) +{ + TRexNode n; + int newid; + n.type = type; + n.next = n.right = n.left = -1; + if(type == OP_EXPR) + n.right = exp->_nsubexpr++; + if(exp->_nallocated < (exp->_nsize + 1)) { + size_t old_nallocated = exp->_nallocated; + exp->_nallocated *= 2; + exp->_nodes = (TRexNode *)at3_realloc(exp->_nodes, + old_nallocated * sizeof(TRexNode), + exp->_nallocated * sizeof(TRexNode)); + } + exp->_nodes[exp->_nsize++] = n; + newid = exp->_nsize - 1; + return (int)newid; +} + +static void trex_error(TRex *exp,const TRexChar *error) +{ + if(exp->_error) *exp->_error = error; + longjmp(*((jmp_buf*)exp->_jmpbuf),-1); +} + +static void trex_expect(TRex *exp, int n){ + if((*exp->_p) != n) + trex_error(exp, _SC("expected paren")); + exp->_p++; +} + +static TRexChar trex_escapechar(TRex *exp) +{ + if(*exp->_p == TREX_SYMBOL_ESCAPE_CHAR){ + exp->_p++; + switch(*exp->_p) { + case 'v': exp->_p++; return '\v'; + case 'n': exp->_p++; return '\n'; + case 't': exp->_p++; return '\t'; + case 'r': exp->_p++; return '\r'; + case 'f': exp->_p++; return '\f'; + default: return (*exp->_p++); + } + } else if(!scisprint((int) *exp->_p)) trex_error(exp,_SC("letter expected")); + return (*exp->_p++); +} + +static int trex_charclass(TRex *exp,int classid) +{ + int n = trex_newnode(exp,OP_CCLASS); + exp->_nodes[n].left = classid; + return n; +} + +static int trex_charnode(TRex *exp,TRexBool isclass) +{ + TRexChar t; + if(*exp->_p == TREX_SYMBOL_ESCAPE_CHAR) { + exp->_p++; + switch(*exp->_p) { + case 'n': exp->_p++; return trex_newnode(exp,'\n'); + case 't': exp->_p++; return trex_newnode(exp,'\t'); + case 'r': exp->_p++; return trex_newnode(exp,'\r'); + case 'f': exp->_p++; return trex_newnode(exp,'\f'); + case 'v': exp->_p++; return trex_newnode(exp,'\v'); + case 'a': case 'A': case 'w': case 'W': case 's': case 'S': + case 'd': case 'D': case 'x': case 'X': case 'c': case 'C': + case 'p': case 'P': case 'l': case 'u': + { + t = *exp->_p; exp->_p++; + return trex_charclass(exp,t); + } + case 'b': + case 'B': + if(!isclass) { + int node = trex_newnode(exp,OP_WB); + exp->_nodes[node].left = *exp->_p; + exp->_p++; + return node; + } //else default + /* falls through */ + default: + t = *exp->_p; exp->_p++; + return trex_newnode(exp,t); + } + } + else if(!scisprint((int) *exp->_p)) { + + trex_error(exp,_SC("letter expected")); + } + t = *exp->_p; exp->_p++; + return trex_newnode(exp,t); +} +static int trex_class(TRex *exp) +{ + int ret = -1; + int first = -1,chain; + if(*exp->_p == TREX_SYMBOL_BEGINNING_OF_STRING){ + ret = trex_newnode(exp,OP_NCLASS); + exp->_p++; + }else ret = trex_newnode(exp,OP_CLASS); + + if(*exp->_p == ']') trex_error(exp,_SC("empty class")); + chain = ret; + while(*exp->_p != ']' && exp->_p != exp->_eol) { + if(*exp->_p == '-' && first != -1){ + int r,t; + if(*exp->_p++ == ']') trex_error(exp,_SC("unfinished range")); + r = trex_newnode(exp,OP_RANGE); + if(first>*exp->_p) trex_error(exp,_SC("invalid range")); + if(exp->_nodes[first].type == OP_CCLASS) trex_error(exp,_SC("cannot use character classes in ranges")); + exp->_nodes[r].left = exp->_nodes[first].type; + t = trex_escapechar(exp); + exp->_nodes[r].right = t; + exp->_nodes[chain].next = r; + chain = r; + first = -1; + } + else{ + if(first!=-1){ + int c = first; + exp->_nodes[chain].next = c; + chain = c; + first = trex_charnode(exp,TRex_True); + } + else{ + first = trex_charnode(exp,TRex_True); + } + } + } + if(first!=-1){ + int c = first; + exp->_nodes[chain].next = c; + chain = c; + first = -1; + } + /* hack? */ + exp->_nodes[ret].left = exp->_nodes[ret].next; + exp->_nodes[ret].next = -1; + return ret; +} + +static int trex_parsenumber(TRex *exp) +{ + int ret = *exp->_p-'0'; + int positions = 10; + exp->_p++; + while(isdigit((int) *exp->_p)) { + ret = ret*10+(*exp->_p++-'0'); + if(positions==1000000000) trex_error(exp,_SC("overflow in numeric constant")); + positions *= 10; + }; + return ret; +} + +static int trex_element(TRex *exp) +{ + int ret = -1; + switch(*exp->_p) + { + case '(': { + int expr,newn; + exp->_p++; + + + if(*exp->_p =='?') { + exp->_p++; + trex_expect(exp,':'); + expr = trex_newnode(exp,OP_NOCAPEXPR); + } + else + expr = trex_newnode(exp,OP_EXPR); + newn = trex_list(exp); + exp->_nodes[expr].left = newn; + ret = expr; + trex_expect(exp,')'); + } + break; + case '[': + exp->_p++; + ret = trex_class(exp); + trex_expect(exp,']'); + break; + case TREX_SYMBOL_END_OF_STRING: exp->_p++; ret = trex_newnode(exp,OP_EOL);break; + case TREX_SYMBOL_ANY_CHAR: exp->_p++; ret = trex_newnode(exp,OP_DOT);break; + default: + ret = trex_charnode(exp,TRex_False); + break; + } + + { + TRexBool isgreedy = TRex_False; + unsigned short p0 = 0, p1 = 0; + switch(*exp->_p){ + case TREX_SYMBOL_GREEDY_ZERO_OR_MORE: p0 = 0; p1 = 0xFFFF; exp->_p++; isgreedy = TRex_True; break; + case TREX_SYMBOL_GREEDY_ONE_OR_MORE: p0 = 1; p1 = 0xFFFF; exp->_p++; isgreedy = TRex_True; break; + case TREX_SYMBOL_GREEDY_ZERO_OR_ONE: p0 = 0; p1 = 1; exp->_p++; isgreedy = TRex_True; break; + case '{': + exp->_p++; + if(!isdigit((int) *exp->_p)) trex_error(exp,_SC("number expected")); + p0 = (unsigned short)trex_parsenumber(exp); + /*******************************/ + switch(*exp->_p) { + case '}': + p1 = p0; exp->_p++; + break; + case ',': + exp->_p++; + p1 = 0xFFFF; + if(isdigit((int) *exp->_p)){ + p1 = (unsigned short)trex_parsenumber(exp); + } + trex_expect(exp,'}'); + break; + default: + trex_error(exp,_SC(", or } expected")); + } + /*******************************/ + isgreedy = TRex_True; + break; + + } + if(isgreedy) { + int nnode = trex_newnode(exp,OP_GREEDY); + exp->_nodes[nnode].left = ret; + exp->_nodes[nnode].right = ((p0)<<16)|p1; + ret = nnode; + } + } + if((*exp->_p != TREX_SYMBOL_BRANCH) && (*exp->_p != ')') && (*exp->_p != TREX_SYMBOL_GREEDY_ZERO_OR_MORE) && (*exp->_p != TREX_SYMBOL_GREEDY_ONE_OR_MORE) && (*exp->_p != '\0')) { + int nnode = trex_element(exp); + exp->_nodes[ret].next = nnode; + } + + return ret; +} + +static int trex_list(TRex *exp) +{ + int ret=-1,e; + if(*exp->_p == TREX_SYMBOL_BEGINNING_OF_STRING) { + exp->_p++; + ret = trex_newnode(exp,OP_BOL); + } + e = trex_element(exp); + if(ret != -1) { + exp->_nodes[ret].next = e; + } + else ret = e; + + if(*exp->_p == TREX_SYMBOL_BRANCH) { + int temp,tright; + exp->_p++; + temp = trex_newnode(exp,OP_OR); + exp->_nodes[temp].left = ret; + tright = trex_list(exp); + exp->_nodes[temp].right = tright; + ret = temp; + } + return ret; +} + +static TRexBool trex_matchcclass(int cclass,TRexChar ch) +{ + int c = ch; + switch(cclass) { + case 'a': return isalpha(c)?TRex_True:TRex_False; + case 'A': return !isalpha(c)?TRex_True:TRex_False; + case 'w': return (isalnum(c) || c == '_')?TRex_True:TRex_False; + case 'W': return (!isalnum(c) && c != '_')?TRex_True:TRex_False; + case 's': return isspace(c)?TRex_True:TRex_False; + case 'S': return !isspace(c)?TRex_True:TRex_False; + case 'd': return isdigit(c)?TRex_True:TRex_False; + case 'D': return !isdigit(c)?TRex_True:TRex_False; + case 'x': return isxdigit(c)?TRex_True:TRex_False; + case 'X': return !isxdigit(c)?TRex_True:TRex_False; + case 'c': return iscntrl(c)?TRex_True:TRex_False; + case 'C': return !iscntrl(c)?TRex_True:TRex_False; + case 'p': return ispunct(c)?TRex_True:TRex_False; + case 'P': return !ispunct(c)?TRex_True:TRex_False; + case 'l': return islower(c)?TRex_True:TRex_False; + case 'u': return isupper(c)?TRex_True:TRex_False; + } + return TRex_False; /*cannot happen*/ +} + +static TRexBool trex_matchclass(TRex* exp,TRexNode *node,TRexChar c) +{ + do { + switch(node->type) { + case OP_RANGE: + if (exp->_flags & TREX_ICASE) + { + if(c >= toupper(node->left) && c <= toupper(node->right)) return TRex_True; + if(c >= tolower(node->left) && c <= tolower(node->right)) return TRex_True; + } + else + { + if(c >= node->left && c <= node->right) return TRex_True; + } + break; + case OP_CCLASS: + if(trex_matchcclass(node->left,c)) return TRex_True; + break; + default: + if (exp->_flags & TREX_ICASE) + { + if (c == tolower(node->type) || c == toupper(node->type)) return TRex_True; + } + else + { + if(c == node->type)return TRex_True; + } + + } + } while((node->next != -1) && (node = &exp->_nodes[node->next])); + return TRex_False; +} + +static const TRexChar *trex_matchnode(TRex* exp,TRexNode *node,const TRexChar *str,TRexNode *next) +{ + + TRexNodeType type = node->type; + switch(type) { + case OP_GREEDY: { + //TRexNode *greedystop = (node->next != -1) ? &exp->_nodes[node->next] : NULL; + TRexNode *greedystop = NULL; + int p0 = (node->right >> 16)&0x0000FFFF, p1 = node->right&0x0000FFFF, nmaches = 0; + const TRexChar *s=str, *good = str; + + if(node->next != -1) { + greedystop = &exp->_nodes[node->next]; + } + else { + greedystop = next; + } + + while((nmaches == 0xFFFF || nmaches < p1)) { + + const TRexChar *stop; + if(!(s = trex_matchnode(exp,&exp->_nodes[node->left],s,greedystop))) + break; + nmaches++; + good=s; + if(greedystop) { + //checks that 0 matches satisfy the expression(if so skips) + //if not would always stop(for instance if is a '?') + if(greedystop->type != OP_GREEDY || + (greedystop->type == OP_GREEDY && ((greedystop->right >> 16)&0x0000FFFF) != 0)) + { + TRexNode *gnext = NULL; + if(greedystop->next != -1) { + gnext = &exp->_nodes[greedystop->next]; + }else if(next && next->next != -1){ + gnext = &exp->_nodes[next->next]; + } + stop = trex_matchnode(exp,greedystop,s,gnext); + if(stop) { + //if satisfied stop it + if(p0 == p1 && p0 == nmaches) break; + else if(nmaches >= p0 && p1 == 0xFFFF) break; + else if(nmaches >= p0 && nmaches <= p1) break; + } + } + } + + if(s >= exp->_eol) + break; + } + if(p0 == p1 && p0 == nmaches) return good; + else if(nmaches >= p0 && p1 == 0xFFFF) return good; + else if(nmaches >= p0 && nmaches <= p1) return good; + return NULL; + } + case OP_OR: { + const TRexChar *asd = str; + TRexNode *temp=&exp->_nodes[node->left]; + while( (asd = trex_matchnode(exp,temp,asd,NULL)) ) { + if(temp->next != -1) + temp = &exp->_nodes[temp->next]; + else + return asd; + } + asd = str; + temp = &exp->_nodes[node->right]; + while( (asd = trex_matchnode(exp,temp,asd,NULL)) ) { + if(temp->next != -1) + temp = &exp->_nodes[temp->next]; + else + return asd; + } + return NULL; + break; + } + case OP_EXPR: + case OP_NOCAPEXPR:{ + TRexNode *n = &exp->_nodes[node->left]; + const TRexChar *cur = str; + int capture = -1; + if(node->type != OP_NOCAPEXPR && node->right == exp->_currsubexp) { + capture = exp->_currsubexp; + exp->_matches[capture].begin = cur; + exp->_currsubexp++; + } + + do { + TRexNode *subnext = NULL; + if(n->next != -1) { + subnext = &exp->_nodes[n->next]; + }else { + subnext = next; + } + if(!(cur = trex_matchnode(exp,n,cur,subnext))) { + if(capture != -1){ + exp->_matches[capture].begin = 0; + exp->_matches[capture].len = 0; + } + return NULL; + } + } while((n->next != -1) && (n = &exp->_nodes[n->next])); + + if(capture != -1) + exp->_matches[capture].len = cur - exp->_matches[capture].begin; + return cur; + } + case OP_WB: + if((str == exp->_bol && !isspace((int) *str)) + || ((str == exp->_eol && !isspace((int) *(str-1)))) + || ((!isspace((int) *str) && isspace((int) *(str+1)))) + || ((isspace((int) *str) && !isspace((int) *(str+1)))) ) { + return (node->left == 'b')?str:NULL; + } + return (node->left == 'b')?NULL:str; + case OP_BOL: + if(str == exp->_bol) return str; + return NULL; + case OP_EOL: + if(str == exp->_eol) return str; + return NULL; + case OP_DOT: + str++; + return str; + case OP_NCLASS: + case OP_CLASS: + if(trex_matchclass(exp,&exp->_nodes[node->left],*str)?(type == OP_CLASS?TRex_True:TRex_False):(type == OP_NCLASS?TRex_True:TRex_False)) { + str++; + return str; + } + return NULL; + case OP_CCLASS: + if(trex_matchcclass(node->left,*str)) { + str++; + return str; + } + return NULL; + default: /* char */ + if (exp->_flags & TREX_ICASE) + { + if(*str != tolower(node->type) && *str != toupper(node->type)) return NULL; + } + else + { + if (*str != node->type) return NULL; + } + str++; + return str; + } + return NULL; +} + +/* public api */ +TRex *trex_compile(const TRexChar *pattern,const TRexChar **error,int flags) +{ + TRex *exp = (TRex *)at3_malloc(sizeof(TRex)); + exp->_eol = exp->_bol = NULL; + exp->_p = pattern; + exp->_nallocated = (int)scstrlen(pattern) * sizeof(TRexChar); + exp->_nodes = (TRexNode *)at3_malloc(exp->_nallocated * sizeof(TRexNode)); + exp->_nsize = 0; + exp->_matches = 0; + exp->_nsubexpr = 0; + exp->_first = trex_newnode(exp,OP_EXPR); + exp->_error = error; + exp->_jmpbuf = at3_malloc(sizeof(jmp_buf)); + exp->_flags = flags; + if(setjmp(*((jmp_buf*)exp->_jmpbuf)) == 0) { + int res = trex_list(exp); + exp->_nodes[exp->_first].left = res; + if(*exp->_p!='\0') + trex_error(exp,_SC("unexpected character")); +#ifdef _DEBUG + { + int nsize,i; + TRexNode *t; + nsize = exp->_nsize; + t = &exp->_nodes[0]; + scprintf(_SC("\n")); + for(i = 0;i < nsize; i++) { + if(exp->_nodes[i].type>MAX_CHAR) + scprintf(_SC("[%02d] %10s "),i,g_nnames[exp->_nodes[i].type-MAX_CHAR]); + else + scprintf(_SC("[%02d] %10c "),i,exp->_nodes[i].type); + scprintf(_SC("left %02d right %02d next %02d\n"),exp->_nodes[i].left,exp->_nodes[i].right,exp->_nodes[i].next); + } + scprintf(_SC("\n")); + } +#endif + exp->_matches = (TRexMatch *) at3_malloc(exp->_nsubexpr * sizeof(TRexMatch)); + memset(exp->_matches,0,exp->_nsubexpr * sizeof(TRexMatch)); + } + else{ + trex_free(exp); + return NULL; + } + return exp; +} + +void trex_free(TRex *exp) +{ + if(exp) { + if(exp->_nodes) at3_free(exp->_nodes); + if(exp->_jmpbuf) at3_free(exp->_jmpbuf); + if(exp->_matches) at3_free(exp->_matches); + at3_free(exp); + } +} + +TRexBool trex_match(TRex* exp,const TRexChar* text) +{ + const TRexChar* res = NULL; + exp->_bol = text; + exp->_eol = text + scstrlen(text); + exp->_currsubexp = 0; + res = trex_matchnode(exp,exp->_nodes,text,NULL); + if(res == NULL || res != exp->_eol) + return TRex_False; + return TRex_True; +} + +TRexBool trex_searchrange(TRex* exp,const TRexChar* text_begin,const TRexChar* text_end,const TRexChar** out_begin, const TRexChar** out_end) +{ + const TRexChar *cur = NULL; + int node = exp->_first; + if(text_begin >= text_end) return TRex_False; + exp->_bol = text_begin; + exp->_eol = text_end; + do { + cur = text_begin; + while(node != -1) { + exp->_currsubexp = 0; + cur = trex_matchnode(exp,&exp->_nodes[node],cur,NULL); + if(!cur) + break; + node = exp->_nodes[node].next; + } + text_begin++; + } while(cur == NULL && text_begin != text_end); + + if(cur == NULL) + return TRex_False; + + --text_begin; + + if(out_begin) *out_begin = text_begin; + if(out_end) *out_end = cur; + return TRex_True; +} + +TRexBool trex_search(TRex* exp,const TRexChar* text, const TRexChar** out_begin, const TRexChar** out_end) +{ + return trex_searchrange(exp,text,text + scstrlen(text),out_begin,out_end); +} + +int trex_getsubexpcount(TRex* exp) +{ + return exp->_nsubexpr; +} + +TRexBool trex_getsubexp(TRex* exp, int n, TRexMatch *subexp) +{ + if( n<0 || n >= exp->_nsubexpr) return TRex_False; + *subexp = exp->_matches[n]; + return TRex_True; +} +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include + +#include "argtable3.h" + + +static void arg_str_resetfn(struct arg_str *parent) +{ + ARG_TRACE(("%s:resetfn(%p)\n", __FILE__, parent)); + parent->count = 0; +} + + +static int arg_str_scanfn(struct arg_str *parent, const char *argval) +{ + int errorcode = 0; + + if (parent->count == parent->hdr.maxcount) + { + /* maximum number of arguments exceeded */ + errorcode = EMAXCOUNT; + } + else if (!argval) + { + /* a valid argument with no argument value was given. */ + /* This happens when an optional argument value was invoked. */ + /* leave parent arguiment value unaltered but still count the argument. */ + parent->count++; + } + else + { + parent->sval[parent->count++] = argval; + } + + ARG_TRACE(("%s:scanfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static int arg_str_checkfn(struct arg_str *parent) +{ + int errorcode = (parent->count < parent->hdr.mincount) ? EMINCOUNT : 0; + + ARG_TRACE(("%s:checkfn(%p) returns %d\n", __FILE__, parent, errorcode)); + return errorcode; +} + + +static void arg_str_errorfn( + struct arg_str *parent, + FILE *fp, + int errorcode, + const char *argval, + const char *progname) +{ + const char *shortopts = parent->hdr.shortopts; + const char *longopts = parent->hdr.longopts; + const char *datatype = parent->hdr.datatype; + + /* make argval NULL safe */ + argval = argval ? argval : ""; + + fprintf(fp, "%s: ", progname); + switch(errorcode) + { + case EMINCOUNT: + fputs("missing option ", fp); + arg_print_option(fp, shortopts, longopts, datatype, "\r\n"); + break; + + case EMAXCOUNT: + fputs("excess option ", fp); + arg_print_option(fp, shortopts, longopts, argval, "\r\n"); + break; + } +} + + +struct arg_str * arg_str0( + const char *shortopts, + const char *longopts, + const char *datatype, + const char *glossary) +{ + return arg_strn(shortopts, longopts, datatype, 0, 1, glossary); +} + + +struct arg_str * arg_str1( + const char *shortopts, + const char *longopts, + const char *datatype, + const char *glossary) +{ + return arg_strn(shortopts, longopts, datatype, 1, 1, glossary); +} + + +struct arg_str * arg_strn( + const char *shortopts, + const char *longopts, + const char *datatype, + int mincount, + int maxcount, + const char *glossary) +{ + size_t nbytes; + struct arg_str *result; + + /* should not allow this stupid error */ + /* we should return an error code warning this logic error */ + /* foolproof things by ensuring maxcount is not less than mincount */ + maxcount = (maxcount < mincount) ? mincount : maxcount; + + nbytes = sizeof(struct arg_str) /* storage for struct arg_str */ + + maxcount * sizeof(char *); /* storage for sval[maxcount] array */ + + result = (struct arg_str *)at3_malloc(nbytes); + if (result) + { + int i; + + /* init the arg_hdr struct */ + result->hdr.flag = ARG_HASVALUE; + result->hdr.shortopts = shortopts; + result->hdr.longopts = longopts; + result->hdr.datatype = datatype ? datatype : ""; + result->hdr.glossary = glossary; + result->hdr.mincount = mincount; + result->hdr.maxcount = maxcount; + result->hdr.parent = result; + result->hdr.resetfn = (arg_resetfn *)arg_str_resetfn; + result->hdr.scanfn = (arg_scanfn *)arg_str_scanfn; + result->hdr.checkfn = (arg_checkfn *)arg_str_checkfn; + result->hdr.errorfn = (arg_errorfn *)arg_str_errorfn; + + /* store the sval[maxcount] array immediately after the arg_str struct */ + result->sval = (const char * *)(result + 1); + result->count = 0; + + /* foolproof the string pointers by initialising them to reference empty strings */ + for (i = 0; i < maxcount; i++) + result->sval[i] = ""; + } + + ARG_TRACE(("arg_strn() returns %p\n", result)); + return result; +} +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#include +#include +#include +#include + +#include "argtable3.h" + +static +void arg_register_error(struct arg_end *end, + void *parent, + int error, + const char *argval) +{ + /* printf("arg_register_error(%p,%p,%d,%s)\n",end,parent,error,argval); */ + if (end->count < end->hdr.maxcount) + { + end->error[end->count] = error; + end->parent[end->count] = parent; + end->argval[end->count] = argval; + end->count++; + } + else + { + end->error[end->hdr.maxcount - 1] = ARG_ELIMIT; + end->parent[end->hdr.maxcount - 1] = end; + end->argval[end->hdr.maxcount - 1] = NULL; + } +} + + +/* + * Return index of first table entry with a matching short option + * or -1 if no match was found. + */ +static +int find_shortoption(struct arg_hdr * *table, char shortopt) +{ + int tabindex; + for(tabindex = 0; !(table[tabindex]->flag & ARG_TERMINATOR); tabindex++) + { + if (table[tabindex]->shortopts && + strchr(table[tabindex]->shortopts, shortopt)) + return tabindex; + } + return -1; +} + + +struct longoptions +{ + int getoptval; + int noptions; + struct option *options; +}; + +#if 0 +static +void dump_longoptions(struct longoptions * longoptions) +{ + int i; + printf("getoptval = %d\n", longoptions->getoptval); + printf("noptions = %d\n", longoptions->noptions); + for (i = 0; i < longoptions->noptions; i++) + { + printf("options[%d].name = \"%s\"\n", + i, + longoptions->options[i].name); + printf("options[%d].has_arg = %d\n", i, longoptions->options[i].has_arg); + printf("options[%d].flag = %p\n", i, longoptions->options[i].flag); + printf("options[%d].val = %d\n", i, longoptions->options[i].val); + } +} +#endif + +static +struct longoptions * alloc_longoptions(struct arg_hdr * *table) +{ + struct longoptions *result; + size_t nbytes; + int noptions = 1; + size_t longoptlen = 0; + int tabindex; + + /* + * Determine the total number of option structs required + * by counting the number of comma separated long options + * in all table entries and return the count in noptions. + * note: noptions starts at 1 not 0 because we getoptlong + * requires a NULL option entry to terminate the option array. + * While we are at it, count the number of chars required + * to store private copies of all the longoption strings + * and return that count in logoptlen. + */ + tabindex = 0; + do + { + const char *longopts = table[tabindex]->longopts; + longoptlen += (longopts ? strlen(longopts) : 0) + 1; + while (longopts) + { + noptions++; + longopts = strchr(longopts + 1, ','); + } + } while(!(table[tabindex++]->flag & ARG_TERMINATOR)); + /*printf("%d long options consuming %d chars in total\n",noptions,longoptlen);*/ + + + /* allocate storage for return data structure as: */ + /* (struct longoptions) + (struct options)[noptions] + char[longoptlen] */ + nbytes = sizeof(struct longoptions) + + sizeof(struct option) * noptions + + longoptlen; + result = (struct longoptions *)at3_malloc(nbytes); + if (result) + { + int option_index = 0; + char *store; + + result->getoptval = 0; + result->noptions = noptions; + result->options = (struct option *)(result + 1); + store = (char *)(result->options + noptions); + + for(tabindex = 0; !(table[tabindex]->flag & ARG_TERMINATOR); tabindex++) + { + const char *longopts = table[tabindex]->longopts; + + while(longopts && *longopts) + { + char *storestart = store; + + /* copy progressive longopt strings into the store */ + while (*longopts != 0 && *longopts != ',') + *store++ = *longopts++; + *store++ = 0; + if (*longopts == ',') + longopts++; + /*fprintf(stderr,"storestart=\"%s\"\n",storestart);*/ + + result->options[option_index].name = storestart; + result->options[option_index].flag = &(result->getoptval); + result->options[option_index].val = tabindex; + if (table[tabindex]->flag & ARG_HASOPTVALUE) + result->options[option_index].has_arg = 2; + else if (table[tabindex]->flag & ARG_HASVALUE) + result->options[option_index].has_arg = 1; + else + result->options[option_index].has_arg = 0; + + option_index++; + } + } + /* terminate the options array with a zero-filled entry */ + result->options[option_index].name = 0; + result->options[option_index].has_arg = 0; + result->options[option_index].flag = 0; + result->options[option_index].val = 0; + } + + /*dump_longoptions(result);*/ + return result; +} + +static +char * alloc_shortoptions(struct arg_hdr * *table) +{ + char *result; + size_t len = 2; + int tabindex; + + /* determine the total number of option chars required */ + for(tabindex = 0; !(table[tabindex]->flag & ARG_TERMINATOR); tabindex++) + { + struct arg_hdr *hdr = table[tabindex]; + len += 3 * (hdr->shortopts ? strlen(hdr->shortopts) : 0); + } + + result = at3_malloc(len); + if (result) + { + char *res = result; + + /* add a leading ':' so getopt return codes distinguish */ + /* unrecognised option and options missing argument values */ + *res++ = ':'; + + for(tabindex = 0; !(table[tabindex]->flag & ARG_TERMINATOR); tabindex++) + { + struct arg_hdr *hdr = table[tabindex]; + const char *shortopts = hdr->shortopts; + while(shortopts && *shortopts) + { + *res++ = *shortopts++; + if (hdr->flag & ARG_HASVALUE) + *res++ = ':'; + if (hdr->flag & ARG_HASOPTVALUE) + *res++ = ':'; + } + } + /* null terminate the string */ + *res = 0; + } + + /*printf("alloc_shortoptions() returns \"%s\"\n",(result?result:"NULL"));*/ + return result; +} + + +/* return index of the table terminator entry */ +static +int arg_endindex(struct arg_hdr * *table) +{ + int tabindex = 0; + while (!(table[tabindex]->flag & ARG_TERMINATOR)) + tabindex++; + return tabindex; +} + + +static +void arg_parse_tagged(int argc, + char * *argv, + struct arg_hdr * *table, + struct arg_end *endtable) +{ + struct longoptions *longoptions; + char *shortoptions; + int copt; + + /*printf("arg_parse_tagged(%d,%p,%p,%p)\n",argc,argv,table,endtable);*/ + + /* allocate short and long option arrays for the given opttable[]. */ + /* if the allocs fail then put an error msg in the last table entry. */ + longoptions = alloc_longoptions(table); + shortoptions = alloc_shortoptions(table); + if (!longoptions || !shortoptions) + { + /* one or both memory allocs failed */ + arg_register_error(endtable, endtable, ARG_EMALLOC, NULL); + /* free anything that was allocated (this is null safe) */ + at3_free(shortoptions); + at3_free(longoptions); + return; + } + + /*dump_longoptions(longoptions);*/ + + /* reset getopts internal option-index to zero, and disable error reporting */ + optind = 0; + opterr = 0; + + /* fetch and process args using getopt_long */ + while( (copt = + getopt_long(argc, argv, shortoptions, longoptions->options, + NULL)) != -1) + { + /* + printf("optarg='%s'\n",optarg); + printf("optind=%d\n",optind); + printf("copt=%c\n",(char)copt); + printf("optopt=%c (%d)\n",optopt, (int)(optopt)); + */ + switch(copt) + { + case 0: + { + int tabindex = longoptions->getoptval; + void *parent = table[tabindex]->parent; + /*printf("long option detected from argtable[%d]\n", tabindex);*/ + if (optarg && optarg[0] == 0 && + (table[tabindex]->flag & ARG_HASVALUE)) + { + /* printf(": long option %s requires an argument\n",argv[optind-1]); */ + arg_register_error(endtable, endtable, ARG_EMISSARG, + argv[optind - 1]); + /* continue to scan the (empty) argument value to enforce argument count checking */ + } + if (table[tabindex]->scanfn) + { + int errorcode = table[tabindex]->scanfn(parent, optarg); + if (errorcode != 0) + arg_register_error(endtable, parent, errorcode, optarg); + } + } + break; + + case '?': + /* + * getopt_long() found an unrecognised short option. + * if it was a short option its value is in optopt + * if it was a long option then optopt=0 + */ + switch (optopt) + { + case 0: + /*printf("?0 unrecognised long option %s\n",argv[optind-1]);*/ + arg_register_error(endtable, endtable, ARG_ELONGOPT, + argv[optind - 1]); + break; + default: + /*printf("?* unrecognised short option '%c'\n",optopt);*/ + arg_register_error(endtable, endtable, optopt, NULL); + break; + } + break; + + case ':': + /* + * getopt_long() found an option with its argument missing. + */ + /*printf(": option %s requires an argument\n",argv[optind-1]); */ + arg_register_error(endtable, endtable, ARG_EMISSARG, + argv[optind - 1]); + break; + + default: + { + /* getopt_long() found a valid short option */ + int tabindex = find_shortoption(table, (char)copt); + /*printf("short option detected from argtable[%d]\n", tabindex);*/ + if (tabindex == -1) + { + /* should never get here - but handle it just in case */ + /*printf("unrecognised short option %d\n",copt);*/ + arg_register_error(endtable, endtable, copt, NULL); + } + else + { + if (table[tabindex]->scanfn) + { + void *parent = table[tabindex]->parent; + int errorcode = table[tabindex]->scanfn(parent, optarg); + if (errorcode != 0) + arg_register_error(endtable, parent, errorcode, optarg); + } + } + break; + } + } + } + + at3_free(shortoptions); + at3_free(longoptions); +} + + +static +void arg_parse_untagged(int argc, + char * *argv, + struct arg_hdr * *table, + struct arg_end *endtable) +{ + int tabindex = 0; + int errorlast = 0; + const char *optarglast = NULL; + void *parentlast = NULL; + + /*printf("arg_parse_untagged(%d,%p,%p,%p)\n",argc,argv,table,endtable);*/ + while (!(table[tabindex]->flag & ARG_TERMINATOR)) + { + void *parent; + int errorcode; + + /* if we have exhausted our argv[optind] entries then we have finished */ + if (optind >= argc) + { + /*printf("arg_parse_untagged(): argv[] exhausted\n");*/ + return; + } + + /* skip table entries with non-null long or short options (they are not untagged entries) */ + if (table[tabindex]->longopts || table[tabindex]->shortopts) + { + /*printf("arg_parse_untagged(): skipping argtable[%d] (tagged argument)\n",tabindex);*/ + tabindex++; + continue; + } + + /* skip table entries with NULL scanfn */ + if (!(table[tabindex]->scanfn)) + { + /*printf("arg_parse_untagged(): skipping argtable[%d] (NULL scanfn)\n",tabindex);*/ + tabindex++; + continue; + } + + /* attempt to scan the current argv[optind] with the current */ + /* table[tabindex] entry. If it succeeds then keep it, otherwise */ + /* try again with the next table[] entry. */ + parent = table[tabindex]->parent; + errorcode = table[tabindex]->scanfn(parent, argv[optind]); + if (errorcode == 0) + { + /* success, move onto next argv[optind] but stay with same table[tabindex] */ + /*printf("arg_parse_untagged(): argtable[%d] successfully matched\n",tabindex);*/ + optind++; + + /* clear the last tentative error */ + errorlast = 0; + } + else + { + /* failure, try same argv[optind] with next table[tabindex] entry */ + /*printf("arg_parse_untagged(): argtable[%d] failed match\n",tabindex);*/ + tabindex++; + + /* remember this as a tentative error we may wish to reinstate later */ + errorlast = errorcode; + optarglast = argv[optind]; + parentlast = parent; + } + + } + + /* if a tenative error still remains at this point then register it as a proper error */ + if (errorlast) + { + arg_register_error(endtable, parentlast, errorlast, optarglast); + optind++; + } + + /* only get here when not all argv[] entries were consumed */ + /* register an error for each unused argv[] entry */ + while (optind < argc) + { + /*printf("arg_parse_untagged(): argv[%d]=\"%s\" not consumed\n",optind,argv[optind]);*/ + arg_register_error(endtable, endtable, ARG_ENOMATCH, argv[optind++]); + } + + return; +} + + +static +void arg_parse_check(struct arg_hdr * *table, struct arg_end *endtable) +{ + int tabindex = 0; + /* printf("arg_parse_check()\n"); */ + do + { + if (table[tabindex]->checkfn) + { + void *parent = table[tabindex]->parent; + int errorcode = table[tabindex]->checkfn(parent); + if (errorcode != 0) + arg_register_error(endtable, parent, errorcode, NULL); + } + } while(!(table[tabindex++]->flag & ARG_TERMINATOR)); +} + + +static +void arg_reset(void * *argtable) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + int tabindex = 0; + /*printf("arg_reset(%p)\n",argtable);*/ + do + { + if (table[tabindex]->resetfn) + table[tabindex]->resetfn(table[tabindex]->parent); + } while(!(table[tabindex++]->flag & ARG_TERMINATOR)); +} + + +int arg_parse(int argc, char * *argv, void * *argtable) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + struct arg_end *endtable; + int endindex; + char * *argvcopy = NULL; + + /*printf("arg_parse(%d,%p,%p)\n",argc,argv,argtable);*/ + + /* reset any argtable data from previous invocations */ + arg_reset(argtable); + + /* locate the first end-of-table marker within the array */ + endindex = arg_endindex(table); + endtable = (struct arg_end *)table[endindex]; + + /* Special case of argc==0. This can occur on Texas Instruments DSP. */ + /* Failure to trap this case results in an unwanted NULL result from */ + /* the malloc for argvcopy (next code block). */ + if (argc == 0) + { + /* We must still perform post-parse checks despite the absence of command line arguments */ + arg_parse_check(table, endtable); + + /* Now we are finished */ + return endtable->count; + } + + argvcopy = (char **)at3_malloc(sizeof(char *) * (argc + 1)); + if (argvcopy) + { + int i; + + /* + Fill in the local copy of argv[]. We need a local copy + because getopt rearranges argv[] which adversely affects + susbsequent parsing attempts. + */ + for (i = 0; i < argc; i++) + argvcopy[i] = argv[i]; + + argvcopy[argc] = NULL; + + /* parse the command line (local copy) for tagged options */ + arg_parse_tagged(argc, argvcopy, table, endtable); + + /* parse the command line (local copy) for untagged options */ + arg_parse_untagged(argc, argvcopy, table, endtable); + + /* if no errors so far then perform post-parse checks otherwise dont bother */ + if (endtable->count == 0) + arg_parse_check(table, endtable); + + /* release the local copt of argv[] */ + at3_free(argvcopy); + } + else + { + /* memory alloc failed */ + arg_register_error(endtable, endtable, ARG_EMALLOC, NULL); + } + + return endtable->count; +} + + +/* + * Concatenate contents of src[] string onto *pdest[] string. + * The *pdest pointer is altered to point to the end of the + * target string and *pndest is decremented by the same number + * of chars. + * Does not append more than *pndest chars into *pdest[] + * so as to prevent buffer overruns. + * Its something like strncat() but more efficient for repeated + * calls on the same destination string. + * Example of use: + * char dest[30] = "good" + * size_t ndest = sizeof(dest); + * char *pdest = dest; + * arg_char(&pdest,"bye ",&ndest); + * arg_char(&pdest,"cruel ",&ndest); + * arg_char(&pdest,"world!",&ndest); + * Results in: + * dest[] == "goodbye cruel world!" + * ndest == 10 + */ +static +void arg_cat(char * *pdest, const char *src, size_t *pndest) +{ + char *dest = *pdest; + char *end = dest + *pndest; + + /*locate null terminator of dest string */ + while(dest < end && *dest != 0) + dest++; + + /* concat src string to dest string */ + while(dest < end && *src != 0) + *dest++ = *src++; + + /* null terminate dest string */ + *dest = 0; + + /* update *pdest and *pndest */ + *pndest = end - dest; + *pdest = dest; +} + + +static +void arg_cat_option(char *dest, + size_t ndest, + const char *shortopts, + const char *longopts, + const char *datatype, + int optvalue) +{ + if (shortopts) + { + char option[3]; + + /* note: option array[] is initialiazed dynamically here to satisfy */ + /* a deficiency in the watcom compiler wrt static array initializers. */ + option[0] = '-'; + option[1] = shortopts[0]; + option[2] = 0; + + arg_cat(&dest, option, &ndest); + if (datatype) + { + arg_cat(&dest, " ", &ndest); + if (optvalue) + { + arg_cat(&dest, "[", &ndest); + arg_cat(&dest, datatype, &ndest); + arg_cat(&dest, "]", &ndest); + } + else + arg_cat(&dest, datatype, &ndest); + } + } + else if (longopts) + { + size_t ncspn; + + /* add "--" tag prefix */ + arg_cat(&dest, "--", &ndest); + + /* add comma separated option tag */ + ncspn = strcspn(longopts, ","); + strncat(dest, longopts, (ncspn < ndest) ? ncspn : ndest); + + if (datatype) + { + arg_cat(&dest, "=", &ndest); + if (optvalue) + { + arg_cat(&dest, "[", &ndest); + arg_cat(&dest, datatype, &ndest); + arg_cat(&dest, "]", &ndest); + } + else + arg_cat(&dest, datatype, &ndest); + } + } + else if (datatype) + { + if (optvalue) + { + arg_cat(&dest, "[", &ndest); + arg_cat(&dest, datatype, &ndest); + arg_cat(&dest, "]", &ndest); + } + else + arg_cat(&dest, datatype, &ndest); + } +} + +static +void arg_cat_optionv(char *dest, + size_t ndest, + const char *shortopts, + const char *longopts, + const char *datatype, + int optvalue, + const char *separator) +{ + separator = separator ? separator : ""; + + if (shortopts) + { + const char *c = shortopts; + while(*c) + { + /* "-a|-b|-c" */ + char shortopt[3]; + + /* note: shortopt array[] is initialiazed dynamically here to satisfy */ + /* a deficiency in the watcom compiler wrt static array initializers. */ + shortopt[0] = '-'; + shortopt[1] = *c; + shortopt[2] = 0; + + arg_cat(&dest, shortopt, &ndest); + if (*++c) + arg_cat(&dest, separator, &ndest); + } + } + + /* put separator between long opts and short opts */ + if (shortopts && longopts) + arg_cat(&dest, separator, &ndest); + + if (longopts) + { + const char *c = longopts; + while(*c) + { + size_t ncspn; + + /* add "--" tag prefix */ + arg_cat(&dest, "--", &ndest); + + /* add comma separated option tag */ + ncspn = strcspn(c, ","); + strncat(dest, c, (ncspn < ndest) ? ncspn : ndest); + c += ncspn; + + /* add given separator in place of comma */ + if (*c == ',') + { + arg_cat(&dest, separator, &ndest); + c++; + } + } + } + + if (datatype) + { + if (longopts) + arg_cat(&dest, "=", &ndest); + else if (shortopts) + arg_cat(&dest, " ", &ndest); + + if (optvalue) + { + arg_cat(&dest, "[", &ndest); + arg_cat(&dest, datatype, &ndest); + arg_cat(&dest, "]", &ndest); + } + else + arg_cat(&dest, datatype, &ndest); + } +} + + +/* this function should be deprecated because it doesnt consider optional argument values (ARG_HASOPTVALUE) */ +void arg_print_option(FILE *fp, + const char *shortopts, + const char *longopts, + const char *datatype, + const char *suffix) +{ + char syntax[200] = ""; + suffix = suffix ? suffix : ""; + + /* there is no way of passing the proper optvalue for optional argument values here, so we must ignore it */ + arg_cat_optionv(syntax, + sizeof(syntax), + shortopts, + longopts, + datatype, + 0, + "|"); + + fputs(syntax, fp); + fputs(suffix, fp); +} + + +/* + * Print a GNU style [OPTION] string in which all short options that + * do not take argument values are presented in abbreviated form, as + * in: -xvfsd, or -xvf[sd], or [-xvsfd] + */ +static +void arg_print_gnuswitch(FILE *fp, struct arg_hdr * *table) +{ + int tabindex; + const char *format1 = " -%c"; + const char *format2 = " [-%c"; + const char *suffix = ""; + + /* print all mandatory switches that are without argument values */ + for(tabindex = 0; + table[tabindex] && !(table[tabindex]->flag & ARG_TERMINATOR); + tabindex++) + { + /* skip optional options */ + if (table[tabindex]->mincount < 1) + continue; + + /* skip non-short options */ + if (table[tabindex]->shortopts == NULL) + continue; + + /* skip options that take argument values */ + if (table[tabindex]->flag & ARG_HASVALUE) + continue; + + /* print the short option (only the first short option char, ignore multiple choices)*/ + fprintf(fp, format1, table[tabindex]->shortopts[0]); + format1 = "%c"; + format2 = "[%c"; + } + + /* print all optional switches that are without argument values */ + for(tabindex = 0; + table[tabindex] && !(table[tabindex]->flag & ARG_TERMINATOR); + tabindex++) + { + /* skip mandatory args */ + if (table[tabindex]->mincount > 0) + continue; + + /* skip args without short options */ + if (table[tabindex]->shortopts == NULL) + continue; + + /* skip args with values */ + if (table[tabindex]->flag & ARG_HASVALUE) + continue; + + /* print first short option */ + fprintf(fp, format2, table[tabindex]->shortopts[0]); + format2 = "%c"; + suffix = "]"; + } + + fprintf(fp, "%s", suffix); +} + + +void arg_print_syntax(FILE *fp, void * *argtable, const char *suffix) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + int i, tabindex; + + /* print GNU style [OPTION] string */ + arg_print_gnuswitch(fp, table); + + /* print remaining options in abbreviated style */ + for(tabindex = 0; + table[tabindex] && !(table[tabindex]->flag & ARG_TERMINATOR); + tabindex++) + { + char syntax[200] = ""; + const char *shortopts, *longopts, *datatype; + + /* skip short options without arg values (they were printed by arg_print_gnu_switch) */ + if (table[tabindex]->shortopts && + !(table[tabindex]->flag & ARG_HASVALUE)) + continue; + + shortopts = table[tabindex]->shortopts; + longopts = table[tabindex]->longopts; + datatype = table[tabindex]->datatype; + arg_cat_option(syntax, + sizeof(syntax), + shortopts, + longopts, + datatype, + table[tabindex]->flag & ARG_HASOPTVALUE); + + if (strlen(syntax) > 0) + { + /* print mandatory instances of this option */ + for (i = 0; i < table[tabindex]->mincount; i++) + fprintf(fp, " %s", syntax); + + /* print optional instances enclosed in "[..]" */ + switch ( table[tabindex]->maxcount - table[tabindex]->mincount ) + { + case 0: + break; + case 1: + fprintf(fp, " [%s]", syntax); + break; + case 2: + fprintf(fp, " [%s] [%s]", syntax, syntax); + break; + default: + fprintf(fp, " [%s]...", syntax); + break; + } + } + } + + if (suffix) + fprintf(fp, "%s", suffix); +} + + +void arg_print_syntaxv(FILE *fp, void * *argtable, const char *suffix) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + int i, tabindex; + + /* print remaining options in abbreviated style */ + for(tabindex = 0; + table[tabindex] && !(table[tabindex]->flag & ARG_TERMINATOR); + tabindex++) + { + char syntax[200] = ""; + const char *shortopts, *longopts, *datatype; + + shortopts = table[tabindex]->shortopts; + longopts = table[tabindex]->longopts; + datatype = table[tabindex]->datatype; + arg_cat_optionv(syntax, + sizeof(syntax), + shortopts, + longopts, + datatype, + table[tabindex]->flag & ARG_HASOPTVALUE, + "|"); + + /* print mandatory options */ + for (i = 0; i < table[tabindex]->mincount; i++) + fprintf(fp, " %s", syntax); + + /* print optional args enclosed in "[..]" */ + switch ( table[tabindex]->maxcount - table[tabindex]->mincount ) + { + case 0: + break; + case 1: + fprintf(fp, " [%s]", syntax); + break; + case 2: + fprintf(fp, " [%s] [%s]", syntax, syntax); + break; + default: + fprintf(fp, " [%s]...", syntax); + break; + } + } + + if (suffix) + fprintf(fp, "%s", suffix); +} + + +void arg_print_glossary(FILE *fp, void * *argtable, const char *format) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + int tabindex; + + format = format ? format : " %-20s %s\r\n"; + for (tabindex = 0; !(table[tabindex]->flag & ARG_TERMINATOR); tabindex++) + { + if (table[tabindex]->glossary) + { + char syntax[200] = ""; + const char *shortopts = table[tabindex]->shortopts; + const char *longopts = table[tabindex]->longopts; + const char *datatype = table[tabindex]->datatype; + const char *glossary = table[tabindex]->glossary; + arg_cat_optionv(syntax, + sizeof(syntax), + shortopts, + longopts, + datatype, + table[tabindex]->flag & ARG_HASOPTVALUE, + ", "); + fprintf(fp, format, syntax, glossary); + } + } +} + + +/** + * Print a piece of text formatted, which means in a column with a + * left and a right margin. The lines are wrapped at whitspaces next + * to right margin. The function does not indent the first line, but + * only the following ones. + * + * Example: + * arg_print_formatted( fp, 0, 5, "Some text that doesn't fit." ) + * will result in the following output: + * + * Some + * text + * that + * doesn' + * t fit. + * + * Too long lines will be wrapped in the middle of a word. + * + * arg_print_formatted( fp, 2, 7, "Some text that doesn't fit." ) + * will result in the following output: + * + * Some + * text + * that + * doesn' + * t fit. + * + * As you see, the first line is not indented. This enables output of + * lines, which start in a line where output already happened. + * + * Author: Uli Fouquet + */ +void arg_print_formatted( FILE *fp, + const unsigned lmargin, + const unsigned rmargin, + const char *text ) +{ + const unsigned textlen = strlen( text ); + unsigned line_start = 0; + unsigned line_end = textlen + 1; + const unsigned colwidth = (rmargin - lmargin) + 1; + + /* Someone doesn't like us... */ + if ( line_end < line_start ) + { fprintf( fp, "%s\r\n", text ); } + + while (line_end - 1 > line_start ) + { + /* Eat leading whitespaces. This is essential because while + wrapping lines, there will often be a whitespace at beginning + of line */ + while ( isspace((int) *(text + line_start)) ) + { line_start++; } + + if ((line_end - line_start) > colwidth ) + { + line_end = line_start + colwidth; + + /* Find last whitespace, that fits into line */ + while ((line_end > line_start) + //&& ( line_end - line_start > colwidth ) + && !isspace((int) *(text + line_end))) { + line_end--; + } + } else { + /* Do not print trailing whitespace. If this text + has got only one line, line_end now points to the + last char due to initialization. */ + line_end--; + } + + /* Output line of text */ + while ( line_start < line_end ) + { + fputc(*(text + line_start), fp ); + line_start++; + } + fputs("\r\n", fp); + + /* Initialize another line */ + if ( line_end + 1 < textlen ) + { + unsigned i; + + for (i = 0; i < lmargin; i++ ) + { fputc( ' ', fp ); } + + line_end = textlen; + } + + /* If we have to print another line, get also the last char. */ + line_end++; + + } /* lines of text */ +} + +/** + * Prints the glossary in strict GNU format. + * Differences to arg_print_glossary() are: + * - wraps lines after 80 chars + * - indents lines without shortops + * - does not accept formatstrings + * + * Contributed by Uli Fouquet + */ +void arg_print_glossary_gnu(FILE *fp, void * *argtable ) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + int tabindex; + + for(tabindex = 0; !(table[tabindex]->flag & ARG_TERMINATOR); tabindex++) + { + if (table[tabindex]->glossary) + { + char syntax[200] = ""; + const char *shortopts = table[tabindex]->shortopts; + const char *longopts = table[tabindex]->longopts; + const char *datatype = table[tabindex]->datatype; + const char *glossary = table[tabindex]->glossary; + + if ( !shortopts && longopts ) + { + /* Indent trailing line by 4 spaces... */ + memset( syntax, ' ', 4 ); + *(syntax + 4) = '\0'; + } + + arg_cat_optionv(syntax, + sizeof(syntax), + shortopts, + longopts, + datatype, + table[tabindex]->flag & ARG_HASOPTVALUE, + ", "); + + /* If syntax fits not into column, print glossary in new line... */ + if ( strlen(syntax) > 25 ) + { + fprintf( fp, " %-25s %s\r\n", syntax, "" ); + *syntax = '\0'; + } + + fprintf( fp, " %-25s ", syntax ); + arg_print_formatted( fp, 28, 79, glossary ); + } + } /* for each table entry */ + + fputs("\r\n", fp); +} + + +/** + * Checks the argtable[] array for NULL entries and returns 1 + * if any are found, zero otherwise. + */ +int arg_nullcheck(void * *argtable) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + int tabindex; + /*printf("arg_nullcheck(%p)\n",argtable);*/ + + if (!table) + return 1; + + tabindex = 0; + do + { + /*printf("argtable[%d]=%p\n",tabindex,argtable[tabindex]);*/ + if (!table[tabindex]) + return 1; + } while(!(table[tabindex++]->flag & ARG_TERMINATOR)); + + return 0; +} + + +/* + * arg_free() is deprecated in favour of arg_freetable() due to a flaw in its design. + * The flaw results in memory leak in the (very rare) case that an intermediate + * entry in the argtable array failed its memory allocation while others following + * that entry were still allocated ok. Those subsequent allocations will not be + * deallocated by arg_free(). + * Despite the unlikeliness of the problem occurring, and the even unlikelier event + * that it has any deliterious effect, it is fixed regardless by replacing arg_free() + * with the newer arg_freetable() function. + * We still keep arg_free() for backwards compatibility. + */ +void arg_free(void * *argtable) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + int tabindex = 0; + int flag; + /*printf("arg_free(%p)\n",argtable);*/ + do + { + /* + if we encounter a NULL entry then somewhat incorrectly we presume + we have come to the end of the array. It isnt strictly true because + an intermediate entry could be NULL with other non-NULL entries to follow. + The subsequent argtable entries would then not be freed as they should. + */ + if (table[tabindex] == NULL) + break; + + flag = table[tabindex]->flag; + at3_free(table[tabindex]); + table[tabindex++] = NULL; + + } while(!(flag & ARG_TERMINATOR)); +} + +/* frees each non-NULL element of argtable[], where n is the size of the number of entries in the array */ +void arg_freetable(void * *argtable, size_t n) +{ + struct arg_hdr * *table = (struct arg_hdr * *)argtable; + size_t tabindex = 0; + /*printf("arg_freetable(%p)\n",argtable);*/ + for (tabindex = 0; tabindex < n; tabindex++) + { + if (table[tabindex] == NULL) + continue; + + at3_free(table[tabindex]); + table[tabindex] = NULL; + }; +} + diff --git a/components/vconsole/libconsole/lib/argtable3/argtable3.h b/components/vconsole/libconsole/lib/argtable3/argtable3.h new file mode 100644 index 0000000..37a321f --- /dev/null +++ b/components/vconsole/libconsole/lib/argtable3/argtable3.h @@ -0,0 +1,306 @@ +/******************************************************************************* + * This file is part of the argtable3 library. + * + * Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of STEWART HEITMANN nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL STEWART HEITMANN BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ + +#ifndef ARGTABLE3 +#define ARGTABLE3 + +#include /* FILE */ +#include /* struct tm */ + +#ifdef __cplusplus +extern "C" { +#endif + +#define ARG_REX_ICASE 1 + +/* bit masks for arg_hdr.flag */ +enum +{ + ARG_TERMINATOR=0x1, + ARG_HASVALUE=0x2, + ARG_HASOPTVALUE=0x4 +}; + +typedef void (arg_resetfn)(void *parent); +typedef int (arg_scanfn)(void *parent, const char *argval); +typedef int (arg_checkfn)(void *parent); +typedef void (arg_errorfn)(void *parent, FILE *fp, int error, const char *argval, const char *progname); + + +/* +* The arg_hdr struct defines properties that are common to all arg_xxx structs. +* The argtable library requires each arg_xxx struct to have an arg_hdr +* struct as its first data member. +* The argtable library functions then use this data to identify the +* properties of the command line option, such as its option tags, +* datatype string, and glossary strings, and so on. +* Moreover, the arg_hdr struct contains pointers to custom functions that +* are provided by each arg_xxx struct which perform the tasks of parsing +* that particular arg_xxx arguments, performing post-parse checks, and +* reporting errors. +* These functions are private to the individual arg_xxx source code +* and are the pointer to them are initiliased by that arg_xxx struct's +* constructor function. The user could alter them after construction +* if desired, but the original intention is for them to be set by the +* constructor and left unaltered. +*/ +struct arg_hdr +{ + char flag; /* Modifier flags: ARG_TERMINATOR, ARG_HASVALUE. */ + const char *shortopts; /* String defining the short options */ + const char *longopts; /* String defiing the long options */ + const char *datatype; /* Description of the argument data type */ + const char *glossary; /* Description of the option as shown by arg_print_glossary function */ + int mincount; /* Minimum number of occurences of this option accepted */ + int maxcount; /* Maximum number of occurences if this option accepted */ + void *parent; /* Pointer to parent arg_xxx struct */ + arg_resetfn *resetfn; /* Pointer to parent arg_xxx reset function */ + arg_scanfn *scanfn; /* Pointer to parent arg_xxx scan function */ + arg_checkfn *checkfn; /* Pointer to parent arg_xxx check function */ + arg_errorfn *errorfn; /* Pointer to parent arg_xxx error function */ + void *priv; /* Pointer to private header data for use by arg_xxx functions */ +}; + +struct arg_rem +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ +}; + +struct arg_lit +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ +}; + +struct arg_int +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + int *ival; /* Array of parsed argument values */ +}; + +struct arg_dbl +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + double *dval; /* Array of parsed argument values */ +}; + +struct arg_str +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + const char **sval; /* Array of parsed argument values */ +}; + +struct arg_rex +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args */ + const char **sval; /* Array of parsed argument values */ +}; + +struct arg_file +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of matching command line args*/ + const char **filename; /* Array of parsed filenames (eg: /home/foo.bar) */ + const char **basename; /* Array of parsed basenames (eg: foo.bar) */ + const char **extension; /* Array of parsed extensions (eg: .bar) */ +}; + +struct arg_date +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ + const char *format; /* strptime format string used to parse the date */ + int count; /* Number of matching command line args */ + struct tm *tmval; /* Array of parsed time values */ +}; + +enum {ARG_ELIMIT=1, ARG_EMALLOC, ARG_ENOMATCH, ARG_ELONGOPT, ARG_EMISSARG}; +struct arg_end +{ + struct arg_hdr hdr; /* The mandatory argtable header struct */ + int count; /* Number of errors encountered */ + int *error; /* Array of error codes */ + void **parent; /* Array of pointers to offending arg_xxx struct */ + const char **argval; /* Array of pointers to offending argv[] string */ +}; + + +/**** arg_xxx constructor functions *********************************/ + +struct arg_rem* arg_rem(const char* datatype, const char* glossary); + +struct arg_lit* arg_lit0(const char* shortopts, + const char* longopts, + const char* glossary); +struct arg_lit* arg_lit1(const char* shortopts, + const char* longopts, + const char *glossary); +struct arg_lit* arg_litn(const char* shortopts, + const char* longopts, + int mincount, + int maxcount, + const char *glossary); + +struct arg_key* arg_key0(const char* keyword, + int flags, + const char* glossary); +struct arg_key* arg_key1(const char* keyword, + int flags, + const char* glossary); +struct arg_key* arg_keyn(const char* keyword, + int flags, + int mincount, + int maxcount, + const char* glossary); + +struct arg_int* arg_int0(const char* shortopts, + const char* longopts, + const char* datatype, + const char* glossary); +struct arg_int* arg_int1(const char* shortopts, + const char* longopts, + const char* datatype, + const char *glossary); +struct arg_int* arg_intn(const char* shortopts, + const char* longopts, + const char *datatype, + int mincount, + int maxcount, + const char *glossary); + +struct arg_dbl* arg_dbl0(const char* shortopts, + const char* longopts, + const char* datatype, + const char* glossary); +struct arg_dbl* arg_dbl1(const char* shortopts, + const char* longopts, + const char* datatype, + const char *glossary); +struct arg_dbl* arg_dbln(const char* shortopts, + const char* longopts, + const char *datatype, + int mincount, + int maxcount, + const char *glossary); + +struct arg_str* arg_str0(const char* shortopts, + const char* longopts, + const char* datatype, + const char* glossary); +struct arg_str* arg_str1(const char* shortopts, + const char* longopts, + const char* datatype, + const char *glossary); +struct arg_str* arg_strn(const char* shortopts, + const char* longopts, + const char* datatype, + int mincount, + int maxcount, + const char *glossary); + +struct arg_rex* arg_rex0(const char* shortopts, + const char* longopts, + const char* pattern, + const char* datatype, + int flags, + const char* glossary); +struct arg_rex* arg_rex1(const char* shortopts, + const char* longopts, + const char* pattern, + const char* datatype, + int flags, + const char *glossary); +struct arg_rex* arg_rexn(const char* shortopts, + const char* longopts, + const char* pattern, + const char* datatype, + int mincount, + int maxcount, + int flags, + const char *glossary); + +struct arg_file* arg_file0(const char* shortopts, + const char* longopts, + const char* datatype, + const char* glossary); +struct arg_file* arg_file1(const char* shortopts, + const char* longopts, + const char* datatype, + const char *glossary); +struct arg_file* arg_filen(const char* shortopts, + const char* longopts, + const char* datatype, + int mincount, + int maxcount, + const char *glossary); + +struct arg_date* arg_date0(const char* shortopts, + const char* longopts, + const char* format, + const char* datatype, + const char* glossary); +struct arg_date* arg_date1(const char* shortopts, + const char* longopts, + const char* format, + const char* datatype, + const char *glossary); +struct arg_date* arg_daten(const char* shortopts, + const char* longopts, + const char* format, + const char* datatype, + int mincount, + int maxcount, + const char *glossary); + +struct arg_end* arg_end(int maxerrors); + + +/**** other functions *******************************************/ +int arg_nullcheck(void **argtable); +int arg_parse(int argc, char **argv, void **argtable); +void arg_print_option(FILE *fp, const char *shortopts, const char *longopts, const char *datatype, const char *suffix); +void arg_print_syntax(FILE *fp, void **argtable, const char *suffix); +void arg_print_syntaxv(FILE *fp, void **argtable, const char *suffix); +void arg_print_glossary(FILE *fp, void **argtable, const char *format); +void arg_print_glossary_gnu(FILE *fp, void **argtable); +void arg_print_errors(FILE* fp, struct arg_end* end, const char* progname); +void arg_freetable(void **argtable, size_t n); +void arg_print_formatted(FILE *fp, const unsigned lmargin, const unsigned rmargin, const char *text); + +/**** deprecated functions, for back-compatibility only ********/ +void arg_free(void **argtable); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/components/vconsole/libconsole/src/console.c b/components/vconsole/libconsole/src/console.c new file mode 100644 index 0000000..e132421 --- /dev/null +++ b/components/vconsole/libconsole/src/console.c @@ -0,0 +1,1824 @@ +#include +#include +#include +#include +#include +#include + +#include "console/console.h" +#include "console/prefix_match.h" +#include "console/utils.h" + +// library includes +#include "argtable3.h" +#include "console_linenoise.h" +#include "queue.h" +#include "console_split_argv.h" + + +#if CONSOLE_USE_TERMIOS && CONSOLE_USE_FILE_IO_STREAMS +static int enableRawMode(console_ctx_t *ctx, int fd); +static void disableRawMode(console_ctx_t *ctx, int fd); +#endif + +static int cmd_exit(console_ctx_t *ctx, cmd_signature_t *reg); + +/** + * Console errors - return codes + */ +static const char *err_names[_CONSOLE_ERR_MAX] = { + [CONSOLE_OK] = "OK", + [CONSOLE_ERROR] = "Unknown Error", + [CONSOLE_ERR_NO_MEM] = "Out of memory", + [CONSOLE_ERR_BAD_CALL] = "Illegal function call", + [CONSOLE_ERR_INVALID_ARG] = "Invalid argument(s)", + [CONSOLE_ERR_UNKNOWN_CMD] = "Unknown command", + [CONSOLE_ERR_TIMEOUT] = "Timed out", + [CONSOLE_ERR_IO] = "IO error", + [CONSOLE_ERR_NOT_POSSIBLE] = "Not Possible", +}; + +void console_err_print_ctx(struct console_ctx *ctx, int e) { + console_printf_ctx(ctx, COLOR_RESET, "Err(%d)", e); + + if (e >= CONSOLE_OK && e < _CONSOLE_ERR_MAX) { + console_printf_ctx(ctx, COLOR_RESET, " - %s", err_names[e]); + } +} + +/** + * Empty argtable used internally by commands with no arguments + */ +static struct { + struct arg_end *end; +} s_empty_argtable; + +/** + * Entry in the internal commands table. + */ +typedef struct cmd_item_ { + struct cmd_signature sig; //!< Command signature + const char* group; //!< Command group (the first word, if multi-part) + const char* alias_of; //!< Aliased command name + console_command_t func; //!< Command function pointer + STAILQ_ENTRY(cmd_item_) next; +} cmd_item_t; + +/** + * Commands table + */ +static STAILQ_HEAD(cmd_list_, cmd_item_) s_cmd_list = {}; + +typedef struct cmd_groups_item_ { + const char *group; + const char *description; + unsigned int num_commands; + STAILQ_ENTRY(cmd_groups_item_) next; +} cmd_groups_item_t; + +static STAILQ_HEAD(cmd_groups_, cmd_groups_item_) s_cmd_groups = {}; + +enum fuzzymatch_cmd_result { + FUZZY_CMD_NO_MATCH = 0, + FUZZY_CMD_AMBIGUOUS, + FUZZY_CMD_PREFIX_MATCH, + FUZZY_CMD_EXACT_MATCH, +}; + +/** + * Recognize an entered command, allowing abbreviations and detecting ambiguity + * + * @param[in] name - entered command name + * @param[out] status - status of the recognition; NULL = don't care + * @param[out] pLongestNWords - word count of the longest matched command + * (may be used in case of ambiguity to show all possible alternatives of the same length) + * @return the detected command, if any + */ +static const cmd_item_t *fuzzy_recognize_command( + char *name, + enum fuzzymatch_cmd_result *status, + size_t *pLongestNWords +); + +#define CMDLIST_SHOW_GROUPS 1 //!< Show groups in the command list, also hide commands that are in a group. +#define CMDLIST_SHOW_CMDS 2 //!< Show individual commands in the command list +#define CMDLIST_SHOW_ALIASES 4 //!< Include aliases in the command list +#define CMDLIST_DETAILED 8 //!< Use detailed listing format with descriptions +#define CMDLIST_GROUPCONTENT 16 //!< Showing what's inside a group + +/** + * The "body" of the "ls" command + */ +static console_err_t do_list_commands(console_ctx_t *ctx, const char *filter_group, uint16_t flags); + +/** Line buffer used for command parsing */ +static char s_line_buf[CONSOLE_LINE_BUF_LEN]; +/** Argv array, holds pointers to s_line_buf. */ +static char *s_argv[CONSOLE_MAX_NUM_ARGS]; + +/** Command eval lock, guarding shared state */ +#if CONSOLE_USE_FREERTOS +static console_mutex_t s_console_eval_mutex; +#endif + +/** Flag that console was already inited */ +static volatile bool console_inited = false; +/** Console config (local copy) */ +static console_config_t s_config = CONSOLE_CONFIG_DEFAULTS(); + +/** Global pointer to console context, valid within a command handler only */ +struct console_ctx * console_active_ctx = NULL; + +/** + * Register default console commands (help, ls, exit) + */ +static void register_default_commands(void); + +static const cmd_item_t *find_command_by_name(const char *name); +static const cmd_item_t *find_command_by_handler(console_command_t handler); + +/** Append to a buffer with a maximal capacity */ +static char *strbufcat(char *dest, size_t bufcap, const char*src) { + size_t available = bufcap - strlen(dest) - 1; + if (available == 0) return dest; + return strncat(dest, src, available); +} + +/** Append at most num chars to a buffer with a maximal capacity */ +static char *strnbufcat(char *dest, size_t bufcap, const char*src, size_t num) { + size_t available = bufcap - strlen(dest) - 1; + if (num < available) { + available = num; + } + return strncat(dest, src, available); +} + +/** Append to a buffer with a maximal capacity */ +static inline char *strbufcat1(char *dest, size_t bufcap, char src) { + return strnbufcat(dest, bufcap, &src, 1); +} + +/** + * @brief Callback which provides command completion for linenoise library + * + * When using linenoise for line editing, command completion support + * can be enabled like this: + * + * linenoiseSetCompletionCallback(&console_get_completion); + * + * @param buf the string typed by the user + * @param lc linenoiseCompletions to be filled in + */ +static void console_get_completion(const char *buf, linenoiseCompletions *lc); + +/** + * @brief Callback which provides command hints for linenoise library + * + * When using linenoise for line editing, hints support can be enabled as + * follows: + * + * linenoiseSetHintsCallback((linenoiseHintsCallback*) &console_get_hint); + * + * The extra cast is needed because linenoiseHintsCallback is defined as + * returning a char* instead of const char*. + * + * @param[in] buf line typed by the user + * @param[out] color ANSI color code to be used when displaying the hint + * @param[out] bold set to 1 if hint has to be displayed in bold + * @return string containing the hint text. This string is persistent and should + * not be freed (i.e. linenoiseSetFreeHintsCallback should not be used). + */ +static const char *console_get_hint(const char *buf, int *color, int *bold); + +void __attribute__((weak)) console_internal_error_print(const char *msg) { + printf("\x1b[31m%s\x1b[m\n", msg); +} + +/** + * Fill defaults, preserve `__internal_heap_allocated` and set MAGIC + */ +static void console_ctx_defaults(struct console_ctx *ctx) { + // clear, preserving the HA flag + bool ha = ctx->__internal_heap_allocated; + bzero(ctx, sizeof(console_ctx_t)); + ctx->__internal_heap_allocated = ha; + ctx->__internal_magic = CONSOLE_CTX_MAGIC; + + ctx->exit_allowed = true; + ctx->interactive = true; + ctx->use_colors = true; + + strcpy(ctx->prompt, "> "); +} + + +console_err_t console_init(const console_config_t *config) +{ + if (console_inited) { + return CONSOLE_OK; // no-op + } + + if (config) { + memcpy(&s_config, config, sizeof(struct console_config)); + } + + s_empty_argtable.end = arg_end(1); + + STAILQ_INIT(&s_cmd_list); + STAILQ_INIT(&s_cmd_groups); + +#if CONSOLE_USE_FREERTOS + s_console_eval_mutex = xSemaphoreCreateMutex(); + if (!s_console_eval_mutex) { + return CONSOLE_ERR_NO_MEM; + } +#elif CONSOLE_USE_PTHREADS + if (pthread_mutex_init(&s_console_eval_mutex, NULL) != 0) { + printf("\n mutex init has failed\n"); + return 1; + } +#endif + + // 'console_inited' must be set before calling 'register_default_commands()' + // because there is a check in the register function. + console_inited = true; + + register_default_commands(); + +#if CONSOLE_TESTING_ALLOC_FUNCS + + // test malloc + char *buf = console_malloc(11); + assert(buf); + + strcpy(buf, "0123456789"); + assert(0 == strcmp(buf, "0123456789")); + + // no-op does not change the pointer + char *reallocated = console_realloc(buf, 11, 11); + assert(reallocated == buf); + buf = reallocated; reallocated = NULL; + + // growing does + reallocated = console_realloc(buf, 11, 20); + assert(reallocated != buf); + assert(0 == strcmp(reallocated, "0123456789")); + buf = reallocated; reallocated = NULL; + + // test that we can write into + strcat(buf, "banana"); + assert(0 == strcmp(buf, "0123456789banana")); + + char *copy = console_strdup(buf); + assert(copy != buf); + assert(0 == strcmp(buf, "0123456789banana")); + assert(0 == strcmp(copy, "0123456789banana")); + assert(0 == copy[16]); + + char *copy2 = console_strndup(buf, 5); + assert(0 == strcmp(copy2, "01234")); + assert(0 == copy2[5]); + assert(copy2 != buf); + + uint8_t *calloced = console_calloc(100,1); + for(int i=0;i<100;i++) { + assert(calloced[i] == 0); + } + + console_free(calloced); + console_free(copy); + console_free(copy2); + console_free(buf); + console_free(NULL); + +#endif + + return CONSOLE_OK; +} + +/** + * Extract "command group" from a command name. + * + * Single-word commands have NULL group. + * + * @param[in] name - command signature + * @return the first word of the command, or NULL + */ +static const char *command_name_to_group(const char *name) { + if (pm_count_words(name, " ") <= 1) { + return NULL; + } + const char *end = pm_skip_words(name, " ", 1); + size_t len = end - name; + + // Look if we already have the group defined - avoids a needless strdup + struct cmd_groups_item_ *it = NULL; + STAILQ_FOREACH(it, &s_cmd_groups, next) { + if (strncmp(name, it->group, len) == 0 && it->group[len] == 0) { + return it->group; + } + } + + return console_strndup(name, len); +} + + +static console_err_t do_add_group(const char *name, const char *descr, bool increment_cmds) { + if (!name) return CONSOLE_ERR_INVALID_ARG; + + // iterate groups and look if we already know this one + cmd_groups_item_t *it = NULL; + bool known = false; + STAILQ_FOREACH(it, &s_cmd_groups, next) { + /* Check if command starts with buf */ + if (strcmp(name, it->group) == 0) { + known = true; + if (descr) { + // maybe it was already spawned by a command. + it->description = descr; + } + if (increment_cmds) { + it->num_commands++; + } + break; + } + } + + if (!known) { + // add new group to the group list + cmd_groups_item_t *grp = console_calloc(1, sizeof(cmd_groups_item_t)); + if (!grp) return CONSOLE_ERR_NO_MEM; + grp->group = name; // borrow it forever + grp->description = descr; // borrow it forever + + if (increment_cmds) { + grp->num_commands++; + } + + STAILQ_INSERT_TAIL(&s_cmd_groups, grp, next); + } + + return 0; +} + +console_err_t console_group_add(const char *name, const char *descr) { + return do_add_group(name, descr, false); +} + +static console_err_t add_command_to_list(cmd_item_t *cmd) { + if (cmd->group/* && !cmd->alias_of*/) { + console_err_t rv = do_add_group(cmd->group, NULL, true); + if (rv != CONSOLE_OK) { + return rv; + } + } + + STAILQ_INSERT_TAIL(&s_cmd_list, cmd, next); + return 0; +} + +static console_err_t add_alias(const cmd_item_t *original, const char *alias) +{ + // check for duplicate + const cmd_item_t *existing = find_command_by_name(alias); + if (existing) { + fprintf(stderr, "Console: Command already exists: %s\n", alias); + return CONSOLE_ERR_UNKNOWN_CMD; + } + + cmd_item_t *item2 = (cmd_item_t *) console_calloc(1, sizeof(cmd_item_t)); + if (!item2) return CONSOLE_ERR_NO_MEM; + + memcpy(item2, original, sizeof(cmd_item_t)); + + item2->alias_of = original->sig.command; + item2->sig.command = alias; + item2->group = command_name_to_group(alias); + item2->next.stqe_next = NULL; + + add_command_to_list(item2); + + return CONSOLE_OK; +} + +console_err_t console_cmd_add_alias(const char *original, const char *alias) +{ + // find command to copy + const cmd_item_t *item = find_command_by_name(original); + if (!item) { + fprintf(stderr, "Console: Unknown command to alias: %s\n", original); + return CONSOLE_ERR_UNKNOWN_CMD; + } + + return add_alias(item, alias); +} + +console_err_t console_cmd_add_alias_fn(console_command_t handler, const char *alias) +{ + // find command to copy + const cmd_item_t *item = find_command_by_handler(handler); + if (!item) { + fprintf(stderr, "Console: Unknown command to alias: %p\n", handler); + return CONSOLE_ERR_UNKNOWN_CMD; + } + + return add_alias(item, alias); +} + +static void print_arg_hint_to_buffer(char *argbuf, size_t argbuf_cap, const struct arg_hdr *arg) { + const bool have_value = (arg->flag & ARG_HASVALUE); + const bool have_short = arg->shortopts; + const bool have_long = arg->longopts; + const bool optional = arg->mincount==0; + + strbufcat1(argbuf, argbuf_cap, ' '); + + if (optional) { + strbufcat1(argbuf, argbuf_cap, '['); + } + + if (have_short) { + strbufcat1(argbuf, argbuf_cap, '-'); + strnbufcat(argbuf, argbuf_cap, arg->shortopts, 1); + } + + if (have_long) { + strbufcat(argbuf, argbuf_cap, have_short?"|--":"--"); + strbufcat(argbuf, argbuf_cap, arg->longopts); + } + + if (have_value) { + if (have_short || have_long) { + strbufcat1(argbuf, argbuf_cap, have_short? ' ' : '='); + } + strbufcat(argbuf, argbuf_cap, arg->datatype); + } + + if (optional) { + strbufcat1(argbuf, argbuf_cap, ']'); + } +} + +console_err_t console_cmd_register(console_command_t handler, const char *name) +{ + if (!console_inited) { + console_internal_error_print("console_cmd_register: not inited!"); + return CONSOLE_ERR_BAD_CALL; + } + + if (!handler) { + console_internal_error_print("console_cmd_register: NULL handler!"); + return CONSOLE_ERR_INVALID_ARG; + } + + if (!name || name[0]==0) { + console_internal_error_print("console_cmd_register: empty name!"); + return CONSOLE_ERR_INVALID_ARG; + } + + const cmd_item_t *existing = NULL; + +// // If the command is already registered, create an alias. +// const cmd_item_t *existing = find_command_by_handler(handler); +// if (existing) { +// return add_alias(existing, name); +// } + + // check for duplicate + existing = find_command_by_name(name); + if (existing) { + fprintf(stderr, "Console: Command already exists: %s\n", name); + return CONSOLE_ERR_UNKNOWN_CMD; + } + + cmd_item_t *item = (cmd_item_t *) console_calloc(1, sizeof(cmd_item_t)); + if (item == NULL) { + return CONSOLE_ERR_NO_MEM; + } + + item->func = handler; + item->sig.command = name; + handler(NULL, &item->sig); + item->sig.command = name; // discard possible changes made inside the function + item->group = command_name_to_group(name); + + if (item->sig.argtable) { + if (NULL == item->sig.hint) { + /* Generate hint based on cmd->argtable */ +#if CONSOLE_USE_MEMSTREAM + char *buf = NULL; + size_t buf_size = 0; + FILE *f = open_memstream(&buf, &buf_size); + if (f != NULL) { + arg_print_syntax(f, item->sig.argtable, NULL); + fclose(f); + } + item->sig.hint = buf; // hint stays on heap +#else + // this is a fallback replacement for the argtable version + struct arg_hdr **table = item->sig.argtable; + int tabindex = 0; + const size_t argbuf_cap = CONSOLE_LINE_BUF_LEN; + char *argbuf = s_line_buf; // we can use 's_line_buf' as scratch here, certainly no command is running yet + *argbuf = 0; + for(tabindex = 0; + table[tabindex] + && !(table[tabindex]->flag & ARG_TERMINATOR) + && (tabindex < CONSOLE_MAX_NUM_ARGS); + tabindex++) { + + print_arg_hint_to_buffer(argbuf, argbuf_cap, table[tabindex]); + } + size_t real_len = strlen(argbuf); + if (real_len > 0) { + char *copy = console_malloc(real_len + 1); + if (copy) { + memcpy(copy, argbuf, real_len + 1); + item->sig.hint = copy; // stays on the heap + } + } +#endif + } + } + else { + item->sig.argtable = &s_empty_argtable; + // hint is NULL + } + + // Add the new entry to the list + add_command_to_list(item); + + return CONSOLE_OK; +} + + +size_t console_count_commands(void) +{ + assert(console_inited); + + size_t count = 0; + cmd_item_t *it = NULL; + STAILQ_FOREACH(it, &s_cmd_list, next) { + count++; + } + return count; +} + + +static void console_get_completion(const char *buf, linenoiseCompletions *lc) +{ + assert(console_inited); + assert(NULL != buf); + assert(NULL != lc); + + size_t len = strlen(buf); + if (len == 0) { + return; + } + cmd_item_t *it = NULL; + STAILQ_FOREACH(it, &s_cmd_list, next) { + /* Check if command starts with buf */ + if (strncmp(buf, it->sig.command, len) == 0) { + consLnAddCompletion((linenoiseCompletions *) lc, it->sig.command); + } + } +} + + +static const char *console_get_hint(const char *buf, int *color, int *bold) +{ + assert(console_inited); + assert(NULL != buf); + assert(NULL != color); + assert(NULL != bold); + + size_t len = strlen(buf); + cmd_item_t *it = NULL; + STAILQ_FOREACH(it, &s_cmd_list, next) { + if (strlen(it->sig.command) == len && + strncmp(buf, it->sig.command, len) == 0) { + *color = 35; // purple + *bold = false; + return it->sig.hint ? it->sig.hint : ""; + } + } + return NULL; +} + + +static const cmd_item_t *find_command_by_name(const char *name) +{ + assert(console_inited); + assert(NULL != name); + + const cmd_item_t *cmd = NULL; + cmd_item_t *it = NULL; + STAILQ_FOREACH(it, &s_cmd_list, next) { + if (prefix_multipart_test(name, it->sig.command, " ", PREFIXMATCH_NOABBREV) == PM_TEST_MATCH) { + cmd = it; + break; + } + } + return cmd; +} + +static const cmd_item_t *find_command_by_handler(console_command_t handler) +{ + assert(console_inited); + assert(NULL != handler); + + const cmd_item_t *cmd = NULL; + cmd_item_t *it = NULL; + STAILQ_FOREACH(it, &s_cmd_list, next) { + if (it->func == handler) { + cmd = it; + break; + } + } + return cmd; +} + +/** + * Print help for one command. Must be called in a command context. + * + * @param it - the command + * @param with_args - to show args + * @return success + */ +static console_err_t print_help_onecmd(const cmd_item_t *cmd, bool with_args); + +static void print_command_suggestions(char *cmdline) { + cmd_item_t *it = NULL; + STAILQ_FOREACH(it, &s_cmd_list, next) { + // if a command is multi-part + size_t full_nwords = pm_count_words(it->sig.command, " "); + + for (size_t nwords = full_nwords; nwords >= 1; nwords--) { + char *end = (char *) pm_skip_words(cmdline, " ", nwords); + if (!end) break; + char old_end = *end; // backup the old byte at the end position + *end = 0; // terminate the string there + + if (0 != prefix_multipart_test(cmdline, it->sig.command, " ", PREFIXMATCH_MULTI_PARTIAL)) { + console_color_printf(COLOR_YELLOW, "-> %s\n", it->sig.command); + break; // use the longest match + } + + *end = old_end; + } + } +} + +static void print_ambiguous_command_suggestions(char *cmdline, size_t longest_nwords) { + cmd_item_t *it = NULL; + STAILQ_FOREACH(it, &s_cmd_list, next) { + // if a command is multi-part + size_t nwords = pm_count_words(it->sig.command, " "); + + char *end = (char *) pm_skip_words(cmdline, " ", nwords); + if (!end) continue; + char old_end = *end; // backup the old byte at the end position + *end = 0; // terminate the string there + + if (1 == prefix_multipart_test(cmdline, it->sig.command, " ", 0)) { + // use the longest matching command + if (nwords == longest_nwords) { + console_color_printf(COLOR_YELLOW, "-> %s\n", it->sig.command); + } + } + *end = old_end; + } +} + +/** + * Show command group info, called from the handle function. + * + * s_line_buf must contain the raw unprocessed command. + * + * @param ctx + * @param g + * @return + */ +static console_err_t print_group_info(console_ctx_t *ctx, const cmd_groups_item_t *g) +{ + console_err_t rv; + const char *descr = g->description ? g->description : "Command group:"; + if (ctx->use_colors) { + console_color_printf(COLOR_GREEN, "%s\n", descr); + } else { + console_printf("%s\n", descr); + } + + // catch attempts to show details + bool detailed = (strstr(s_line_buf, "-h") != NULL) || + (strstr(s_line_buf, "-d") != NULL) || + (strstr(s_line_buf, "-v") != NULL); + + // remove the -h etc + char *first_hyphen = strchr(s_line_buf, '-'); + if (first_hyphen) { + *first_hyphen = 0; + } + + rv = do_list_commands(ctx, s_line_buf, (detailed ? CMDLIST_DETAILED : 0) | CMDLIST_SHOW_CMDS | CMDLIST_GROUPCONTENT); + + if (!detailed) { + console_printf("Use -h to show command descriptions.\n"); + } + return rv; +} + +console_err_t console_handle_cmd(console_ctx_t *ctx, const char *cmdline, int *pRetval, const struct cmd_signature **pCommandSig) +{ + if (!console_inited) { + console_internal_error_print("console_handle_cmd: not inited!"); + return CONSOLE_ERR_BAD_CALL; + } + + if (!ctx) { + console_internal_error_print("console_handle_cmd: NULL ctx!"); + return CONSOLE_ERR_INVALID_ARG; + } + + if (!cmdline) { + console_internal_error_print("console_handle_cmd: NULL cmdline!"); + return CONSOLE_ERR_INVALID_ARG; + } + + // cmd_ret may be null + + int argc; + console_err_t rv; + + // ensure retval is inited + if (pRetval) { + *pRetval = 0; + } + + if (pCommandSig) { + *pCommandSig = NULL; + } + +#if CONSOLE_USE_FREERTOS + if (pdPASS != xSemaphoreTake(s_console_eval_mutex, pdMS_TO_TICKS(s_config.execution_lock_timeout_ms))) { + return CONSOLE_ERR_TIMEOUT; + } +#elif CONSOLE_USE_PTHREADS + struct timespec timeoutTime; + clock_gettime(CLOCK_REALTIME, &timeoutTime); + timeoutTime.tv_nsec += s_config.execution_lock_timeout_ms*1000LL; + if(0 != pthread_mutex_timedlock(&s_console_eval_mutex, &timeoutTime)) { + return CONSOLE_ERR_TIMEOUT; + } +#endif + + console_active_ctx = ctx; + + strncpy(s_line_buf, cmdline, CONSOLE_LINE_BUF_LEN); + s_line_buf[CONSOLE_LINE_BUF_LEN - 1] = 0; // ensure it is terminated + + enum fuzzymatch_cmd_result status = FUZZY_CMD_NO_MATCH; + size_t longest_nwords = 0; + const cmd_item_t *cmd = fuzzy_recognize_command(s_line_buf, &status, &longest_nwords); + + if (status == FUZZY_CMD_AMBIGUOUS) { + // check if we have an exact match among groups + cmd_groups_item_t *g = NULL; + STAILQ_FOREACH(g, &s_cmd_groups, next) { + /* Check if command starts with buf */ + const size_t grouplen = strlen(g->group); + if (strncmp(s_line_buf, g->group, grouplen) == 0 && + ((s_line_buf[grouplen] == 0) || (s_line_buf[grouplen] == ' '))) + { + rv = print_group_info(ctx, g); + goto exit; + } + } + } + + if (cmd == NULL) { + // there is no match + if (status == FUZZY_CMD_NO_MATCH) { + + // Check if this is a group name - if so, show group members. + cmd_groups_item_t *g = NULL; + STAILQ_FOREACH(g, &s_cmd_groups, next) { + /* Check if command starts with buf */ + const size_t grouplen = strlen(g->group); + if (strncmp(s_line_buf, g->group, grouplen) == 0 && + (s_line_buf[grouplen] == 0 || s_line_buf[grouplen] == ' ')) + { + rv = print_group_info(ctx, g); + goto exit; + } + } + + // no match in groups... + + if (longest_nwords > 0) { + console_color_printf(COLOR_RED, "Unknown command. Partial matches:\n"); + print_command_suggestions(s_line_buf); + } else { + console_color_printf(COLOR_RED, "Unknown command.\n"); + } + } else if (status == FUZZY_CMD_AMBIGUOUS) { + console_color_printf(COLOR_RED, "Ambiguous command. Possible matches:\n"); + + // This dump is shown in all cases, even non-interactive, to help debugging. + print_ambiguous_command_suggestions(s_line_buf, longest_nwords); + } + + rv = CONSOLE_ERR_UNKNOWN_CMD; + goto exit; + } + + if (ctx->interactive && status != FUZZY_CMD_EXACT_MATCH) { + // Show the recognized command + console_color_printf(COLOR_YELLOW, "%s\n", cmd->sig.command); + } + + argc = console_split_argv(s_line_buf, s_argv, CONSOLE_MAX_NUM_ARGS); + + if (argc == 0) { + rv = CONSOLE_ERR_BAD_CALL; + goto exit; + } + + // help for all commands + for (int i = 1; i < argc; i++) { + if (0 == strcmp(s_argv[i], "-h") || 0 == strcmp(s_argv[i], "-?") || 0 == strcmp(s_argv[i], "--help")) { + print_help_onecmd(cmd, true); + if (pRetval != NULL) { + *pRetval = 0; + } + rv = CONSOLE_OK; + goto exit; + } + } + + // now we know it's the command we want. + // we have to manipulate argv to match the command signature (join multipart command words) + if (longest_nwords > 1) { + // const is discarded here - it's OK. nobody should try to free the strings, as they are normally + // pieces of the global static char array + s_argv[0] = (char *) cmd->sig.command; // This is safe, since user code see it through a const* + + // shift the tail + for (int i = longest_nwords, j = 1; i <= argc; i++, j++) { + s_argv[j] = s_argv[i]; + } + argc -= (int)longest_nwords - 1; + } + + /* Run the command */ + if (pCommandSig) { + *pCommandSig = &cmd->sig; + } + + if (!cmd->sig.custom_args) { + // Parse argv using argtable3 + int nerrors = arg_parse(argc, s_argv, (void **) cmd->sig.argtable); + + if (nerrors != 0) { +#if CONSOLE_USE_MEMSTREAM + // find end + struct arg_hdr **arg = cmd->sig.argtable; + int depth = 0; + while (0 == ((*arg)->flag & ARG_TERMINATOR) && (depth < CONSOLE_MAX_NUM_ARGS)) { + arg++; + depth++; + } + + // temporary stream FILE for argtable + char *buf = NULL; + size_t buf_size = 0; + FILE *f = open_memstream(&buf, &buf_size); + if (!f) { + rv = CONSOLE_ERR_NO_MEM; + goto exit; + } + + arg_print_errors(f, (struct arg_end *) *arg, cmd->sig.command); + + // clean up + fclose(f); + console_print(buf); + free(buf); // allocated by memstream +#else + console_printf_ctx(ctx, COLOR_RED, "bad args\n"); +#endif + rv = CONSOLE_ERR_INVALID_ARG; + goto exit; + } + } + + // Expose some context information to the command handler + ctx->argv = (const char **) &s_argv[0]; + ctx->argc = argc; + ctx->cmd = &cmd->sig; + + int cmd_rv = (*cmd->func)(ctx, NULL); + if (pRetval) { + *pRetval = cmd_rv; + } + + // Clear the context pointers + ctx->argv = NULL; + ctx->argc = 0; + ctx->cmd = NULL; + + rv = CONSOLE_OK; + + // fall through +exit: + console_active_ctx = NULL; + +#if CONSOLE_USE_FREERTOS + xSemaphoreGive(s_console_eval_mutex); +#elif CONSOLE_USE_PTHREADS + pthread_mutex_unlock(&s_console_eval_mutex); +#endif + return rv; +} + + +static console_err_t print_help_onecmd(const cmd_item_t *cmd, bool with_args) +{ + assert(NULL != cmd); + + /* First line: command name and hint + * Pad all the hints to the same column + */ + const char *hint = (cmd->sig.hint) ? cmd->sig.hint : ""; + if (console_active_ctx->use_colors) { + console_printf("\x1b[1m%s\x1b[22;35m%s\x1b[m\n", cmd->sig.command, hint); + } else { + console_printf("%-s%s\n", cmd->sig.command, hint); + } + + bool any_aliases = false; + cmd_item_t *iter; + /* Print all aliases */ + STAILQ_FOREACH(iter, &s_cmd_list, next) { + if (cmd != iter && cmd->func == iter->func) { + if (!any_aliases) { + console_print(" (aliases: "); + } else { + console_print(", "); + } + any_aliases = true; + + if (console_active_ctx->use_colors) { + console_printf("\x1b[1m%s\x1b[m", iter->sig.command); + } else { + console_print(iter->sig.command); + } + } + } + if (any_aliases) { + console_print(")\n"); + } + +#if CONSOLE_USE_MEMSTREAM + // argtable3 needs a FILE* + char *buf = NULL; + size_t buf_size = 0; + FILE *capf = open_memstream(&buf, &buf_size); + if (!capf) return CONSOLE_ERR_NO_MEM; + + if (cmd->sig.help && cmd->sig.help[0] != 0) { + /* Second line: print help. + * Argtable has a nice helper function for this which does line + * wrapping. + */ + console_printf(" "); // arg_print_formatted does not indent the first line + arg_print_formatted(capf, 2, 78, cmd->sig.help); + } + + if (with_args) { + /* Finally, print the list of arguments */ + if (cmd->sig.argtable) { + arg_print_glossary(capf, (void **) cmd->sig.argtable, " %12s %s\n"); + } + fputs("\n", capf); + } + + // clean up the memstream FILE + fclose(capf); + console_print(buf); + free(buf); // allocated by memstream +#else + // this is a fallback replacement for the argtable version + if (cmd->sig.help && cmd->sig.help[0] != 0) { + console_printf(" %s\n", cmd->sig.help); + } + + const size_t argbuf_cap = 32; + char argbuf[32]; + struct arg_hdr **table = cmd->sig.argtable; + int tabindex = 0; + for(tabindex = 0; + table[tabindex] + && !(table[tabindex]->flag & ARG_TERMINATOR) + && (tabindex < CONSOLE_MAX_NUM_ARGS); + tabindex++) { + + *argbuf = 0; + print_arg_hint_to_buffer(argbuf, argbuf_cap, table[tabindex]); + + console_print(" "); + console_print(argbuf); + console_print(" "); + console_println(table[tabindex]->glossary); + } +#endif + + return CONSOLE_OK; +} + +static const cmd_item_t *fuzzy_recognize_command(char *name, enum fuzzymatch_cmd_result *pStatus, size_t *pLongestNWords) { + size_t source_words = pm_count_words(name, " "); + + cmd_item_t *it = NULL; + size_t longest_nwords = 0; + bool ambiguous = false; + bool exact_match = false; + const cmd_item_t *cmd = NULL; + STAILQ_FOREACH(it, &s_cmd_list, next) { + size_t nwords = pm_count_words(it->sig.command, " "); + + char *end = (char*) pm_skip_words(name, " ", nwords); + if (!end) continue; + char old_end = *end; // backup the old byte at the end position + *end = 0; // terminate the string there + + if (1 == prefix_multipart_test(name, it->sig.command, " ", 0)) { + // use the longest matching command + if (!cmd || nwords > longest_nwords) { + cmd = it; + longest_nwords = nwords; + ambiguous = false; // we found a longer match + exact_match = (PM_TEST_MATCH == prefix_multipart_test(name, it->sig.command, " ", PREFIXMATCH_NOABBREV)); + + if (exact_match && nwords == source_words) { + goto fuzzy_done; + } + } else if (nwords == longest_nwords && !exact_match) { + if (PM_TEST_MATCH == prefix_multipart_test(name, it->sig.command, " ", PREFIXMATCH_NOABBREV)) { + ambiguous = false; // we found a longer match + exact_match = true; + cmd = it; + } else { + if (cmd->alias_of == it->sig.command || it->alias_of == cmd->sig.command) { + // We have two aliases of the same command, not really ambiguous + } else { + exact_match = false; + ambiguous = true; // there is an ambiguity between two commands + } + } + } + } + *end = old_end; + } + +fuzzy_done: + if (pLongestNWords) *pLongestNWords = longest_nwords; + + if (ambiguous) { + if (pStatus) *pStatus = FUZZY_CMD_AMBIGUOUS; + return NULL; + } else { + if (pStatus) *pStatus = cmd ? + (exact_match ? FUZZY_CMD_EXACT_MATCH : FUZZY_CMD_PREFIX_MATCH) : + FUZZY_CMD_NO_MATCH; + return cmd; + } +} + +/** + * Command: Show help + */ +static int cmd_help(console_ctx_t *ctx, cmd_signature_t *reg) +{ + // this struct is only used to build the hint + static struct { + struct arg_str *cmd; + struct arg_end *end; + } args; + + if (reg) { + args.cmd = arg_str1(NULL, NULL, "", "Command to describe (can be multi-part)"); + args.end = arg_end(2); + + reg->custom_args = true; + reg->help = "Show the help page for a command, or list all commands and their basic usage"; + reg->argtable = &args; + return 0; + } + + /* recreate the original multi-part command, if given as multiple space-separated arguments */ + + // HACK: using s_line_buf as a scratch buffer + char *const cmdname = s_line_buf; + char *p = cmdname; + *p = 0; // clear the string + for (size_t i = 1; i < ctx->argc; i++) { + p += sprintf(p, "%s ", ctx->argv[i]); + } + if (p != cmdname) p--; // remove the trailing space + *p = 0; // add terminator after the built sequence + + if (*s_line_buf != 0) { + // a command is selected + + enum fuzzymatch_cmd_result status = FUZZY_CMD_NO_MATCH; + size_t longest_nwords = 0; + const cmd_item_t *cmd = fuzzy_recognize_command(s_line_buf, &status, &longest_nwords); + + if (status == FUZZY_CMD_NO_MATCH) { + console_color_printf(COLOR_RED, "\nHelp: No command matches: \"%s\"\n", s_line_buf); + print_command_suggestions(s_line_buf); + return CONSOLE_ERR_UNKNOWN_CMD; + } + + if (status == FUZZY_CMD_AMBIGUOUS) { + console_color_printf(COLOR_RED, "\nHelp: Ambiguous command: \"%s\"\n", s_line_buf); + print_ambiguous_command_suggestions(s_line_buf, longest_nwords); + return CONSOLE_ERR_UNKNOWN_CMD; + } + + assert(cmd != NULL); + + if (!ctx->exit_allowed && cmd->func == cmd_exit) { + console_print("\n\"exit\" is not available in this session.\n"); + return 0; + } + + print_help_onecmd(cmd, true); + return 0; + } else { + cmd_item_t *it; + /* Print summary of each command */ + STAILQ_FOREACH(it, &s_cmd_list, next) { + if (it->alias_of) continue; // aliases are listed as part of the regular entry + + if (!ctx->exit_allowed && it->func == cmd_exit) { + continue; + } + + print_help_onecmd(it, false); + } + return 0; + } +} + +const char* LS_USE_HELP_TO_LEARN_MORE = EXPENDABLE_STRING("Use `cmd -h` or `help cmd` to learn more about a command.\n"); + +/** + * Command: List all commands in a short format. + */ +static int cmd_list(console_ctx_t *ctx, cmd_signature_t *reg) +{ + // this struct is only used to build the hint + static struct { + struct arg_lit *all; + struct arg_lit *aliases; + struct arg_lit *describe; + struct arg_lit *groupsonly; + struct arg_str *group; + struct arg_end *end; + } args; + + if (reg) { + args.group = arg_str0(NULL, NULL, "GROUP", EXPENDABLE_STRING("Show commands in a group, or starting with...")); + args.all = arg_lit0("a", "all", EXPENDABLE_STRING("Show all commands, disable grouping")); + args.describe = arg_lit0("d", "descr", EXPENDABLE_STRING("Show one command per line, with descriptions")); + args.groupsonly = arg_lit0("g", "groups", EXPENDABLE_STRING("Show only groups")); + args.aliases = arg_lit0("A", "aliases", EXPENDABLE_STRING("Include command aliases")); + args.end = arg_end(3); + + reg->argtable = &args; + reg->help = EXPENDABLE_STRING("List available commands"); + return 0; + } + + const char * const filter_group = args.group->count ? args.group->sval[0] : NULL; + + // fake the -a flag if executed as "la" + if (0 == strcmp("la", ctx->argv[0])) { + args.all->count = 1; + } + // fake the -d flag if executed as "ll" + if (0 == strcmp("ll", ctx->argv[0])) { + args.describe->count = 1; + } + + + bool show_groups = !args.all->count && !args.group->count; + bool show_commands = !args.groupsonly->count; + bool show_aliases = args.aliases->count; + + if (args.all->count && filter_group) { + console_color_printf(COLOR_RED, "Filter argument cannot be used together with \"-a\""); + return CONSOLE_ERR_INVALID_ARG; + } + + return do_list_commands(ctx, args.all->count ? NULL : filter_group, + (show_groups ? CMDLIST_SHOW_GROUPS : 0) | + (show_commands ? CMDLIST_SHOW_CMDS : 0) | + (show_aliases ? CMDLIST_SHOW_ALIASES : 0) | + (args.describe->count ? CMDLIST_DETAILED : 0)); +} + +static console_err_t do_list_commands(console_ctx_t *ctx, const char *filter_group, uint16_t flags) { + + #define SKIPLIST_LEN 32 + uint32_t skipmask[SKIPLIST_LEN] = {};// should be enough + + bool have_spaces = pm_count_words(filter_group, " \t") > 0; + int first_word_len = pm_word_len(filter_group, " \t"); + + // Count commands + int count = 0; + int index = 0; + int max_cmd_len = 0; + + cmd_item_t *it; + cmd_groups_item_t *grp; + + const bool show_groups = flags & CMDLIST_SHOW_GROUPS; + const bool show_commands = flags & CMDLIST_SHOW_CMDS; + const bool show_aliases = flags & CMDLIST_SHOW_ALIASES; + const bool detailed = flags & CMDLIST_DETAILED; + const bool ingroup = flags & CMDLIST_GROUPCONTENT; + + bool any_matches = false; + bool any_aliases_shown = false; + bool any_groups_shown = false; + + if (show_groups) { + any_matches = true; + STAILQ_FOREACH(grp, &s_cmd_groups, next) { + int len = (int) strlen(grp->group); + if (len > max_cmd_len) { + max_cmd_len = len; + } + count++; + index++; + any_groups_shown = true; + } + } + + if (show_commands) { + STAILQ_FOREACH(it, &s_cmd_list, next) { + if (!ctx->exit_allowed && it->func == cmd_exit) { + goto skip; + } + if (!show_aliases && it->alias_of) { + goto skip; + } + + if (filter_group) { + // user tries to filter. + // that means groups are hidden and then "all" listing is used. + + bool grp_matches = (it->group && + 0 == strncasecmp(filter_group, it->group, MAX(first_word_len, strlen(it->group))) && + (have_spaces || it->group[strlen(filter_group)]==0) + ); + bool prefix_matches = 0 == strncasecmp(filter_group, it->sig.command, strlen(filter_group)); + + if (!(grp_matches || (!ingroup && prefix_matches))) { + goto skip; + } + + if (grp_matches && !prefix_matches) { + // bad prefix + goto skip; + } + } + else { + // no filter word, this is the basic listing + + if (show_groups && it->group) { + // do not show command that is grouped + goto skip; + } + } + + int len = (int) strlen(it->sig.command); + if (len > max_cmd_len) { + max_cmd_len = len; + } + index++; + count++; + any_matches = true; + if (it->alias_of) { + any_aliases_shown = true; + } + continue; + skip: + assert((index >> 5) < SKIPLIST_LEN); + skipmask[index >> 5] |= (1 << (index & 31)); + index++; + } + } + + if (!any_matches) { + console_print("No matching command or group found.\n"); + return CONSOLE_OK; + } + + if (detailed) { + // Show descriptions for all matched commands + index = 0; + if (show_groups) { + STAILQ_FOREACH(grp, &s_cmd_groups, next) { + if (skipmask[index>>5] & (1<<(index&31))) { + index++; continue; + } + index++; + + if (console_active_ctx->use_colors) { + console_printf("\x1b[1m%s\x1b[m\x1b[22;35m Group with %d sub-commands\x1b[m\n", grp->group, grp->num_commands); + if (grp->description) { + console_printf(" %s\n", grp->description); + } + } else { + console_printf("%s - Group with %d sub-commands\n", grp->group, grp->num_commands); + if (grp->description) { + console_printf(" %s\n", grp->description); + } + } + } + } + + if (show_commands) { + STAILQ_FOREACH(it, &s_cmd_list, next) { + if (skipmask[index >> 5] & (1 << (index & 31))) { + index++; + continue; + } + index++; + + print_help_onecmd(it, false); + } + } + + console_print("\n"); + console_print(LS_USE_HELP_TO_LEARN_MORE); + return CONSOLE_OK; + } + + + // this is a multi-column ls-like list of all commands + + const int COLW = max_cmd_len + 2; + + const int COLN = ( + count < 6 ? 1 : count <= 12 ? 3 : + (80 / COLW)); + + const int LINELEN = COLW * COLN + 1; + + // resolve number of rows needed for the full output + const int paddedcount = count + (COLN>1?(COLN - count % COLN):0); // round up to a multiple of COLN + const int rows = paddedcount / COLN; + + const size_t buffers_cap = (size_t) rows * LINELEN; + char *buffers = console_malloc(buffers_cap); + + if (!buffers) { + return CONSOLE_ERR_NO_MEM; + } + + memset(buffers, ' ', buffers_cap); + buffers[buffers_cap-1] = 0; + + int col = 0; + int row = 0; + index = 0; + + if (show_groups) { + STAILQ_FOREACH(grp, &s_cmd_groups, next) { + if (skipmask[index>>5] & (1<<(index&31))) { + index++; continue; + } + index++; + + // see command printing below for explanation + const size_t offset = row * LINELEN + col * COLW; + int grpln = (int)strlen(grp->group); + snprintf(buffers + offset, COLW+1, "%s/", grp->group); + buffers[offset + grpln + 1] = ' '; // replace the terminator with space + row++; + if (row >= rows) { row = 0; col++; } + } + } + + if (show_commands) { + STAILQ_FOREACH(it, &s_cmd_list, next) { + if (skipmask[index >> 5] & (1 << (index & 31))) { + index++; + continue; + } + index++; + + // this always replaces the earlier '\0' in the line and adds a new one. + // lines are 1 char longer than needed to accomodate the last column's '\0' without + // overwriting the next line's first char. + const size_t offset = row * LINELEN + col * COLW; + bool alias = (it->alias_of != NULL); + snprintf(buffers + offset, COLW + 1, "%s%-*s", (alias ? "*" : ""), COLW - alias, it->sig.command); + buffers[offset + COLW] = ' '; // replace the terminator with space + row++; + if (row >= rows) { + row = 0; + col++; + } + } + } + + for (int i = 0; i < rows; i++) { + console_write(buffers + i * LINELEN, LINELEN); + console_write("\n", 1); + } + console_free(buffers); + console_write("\n", 1); + + EXPENDABLE_CODE({ + if (any_groups_shown) { + console_print("\"/\" marks a group. Use \"ls GROUP\" to see its members.\n"); + } + + if (any_aliases_shown) { + console_print("\"*\" marks an alias.\n"); + } + }) + + console_print(LS_USE_HELP_TO_LEARN_MORE); + return CONSOLE_OK; +} + +/** + * Command: Exit the console + */ +static int cmd_exit(console_ctx_t *ctx, cmd_signature_t *reg) +{ + if (reg) { + assert(NULL == ctx); + assert(NULL != reg); + + reg->help = EXPENDABLE_STRING("Quit console."); + reg->no_history = true; + return 0; + } + + // These will catch bugs when exiting console while testing + assert(NULL != ctx); + assert(NULL == reg); + assert(NULL != ctx->argv); + assert(NULL != ctx->cmd); + + if (!ctx->exit_allowed) { + console_println("Exit not allowed in this session."); + return CONSOLE_ERR_NOT_POSSIBLE; + } + + ctx->exit_requested = true; + console_println("Leaving console."); + + return 0; +} + +/** + * Command: Delay + */ +static int cmd_sleep(console_ctx_t *ctx, cmd_signature_t *reg) +{ + (void) ctx; // unused + static struct { + struct arg_int *seconds; + struct arg_int *millis; + struct arg_end *end; + } args; + + if (reg) { + args.millis = arg_int0("m", "millis", "", "milliseconds"); + args.seconds = arg_int0(NULL, NULL, "", "seconds"); + args.end = arg_end(2); + reg->argtable = &args; + reg->help = EXPENDABLE_STRING("Stop the console for a given time (max 60s); useful in batched scripts. Both parameters may be combined."); + return 0; + } + + uint32_t s = args.seconds->count ? args.seconds->ival[0] : 0; + uint32_t ms = s*1000 + (args.millis->count ? args.millis->ival[0] : 0); + + if (ms > 60000) { + console_color_printf(COLOR_RED, "Interval out of range\n"); + return CONSOLE_ERR_INVALID_ARG; + } + + if (ms > 0) { + vTaskDelay(pdMS_TO_TICKS(ms)); + } + + return 0; +} + +static void register_default_commands() +{ + console_cmd_register(cmd_list, "list"); + console_cmd_add_alias_fn(cmd_list, "ls"); + console_cmd_add_alias_fn(cmd_list, "la"); + console_cmd_add_alias_fn(cmd_list, "ll"); + + console_cmd_register(cmd_help, "help"); + + console_cmd_register(cmd_exit, "exit"); + console_cmd_add_alias_fn(cmd_exit, "quit"); + console_cmd_add_alias_fn(cmd_exit, "q"); + + console_cmd_register(cmd_sleep, "sleep"); +} + + +console_ctx_t *console_ctx_init( + console_ctx_t *ctx, +#if CONSOLE_USE_FILE_IO_STREAMS + FILE* inf, FILE* outf +#else + void * ioctx +#endif +) { + if (ctx == NULL) { + ctx = console_calloc(sizeof(struct console_ctx), 1); + if (!ctx) { + console_internal_error_print("console_ctx_init alloc fail!"); + // Alloc failed + return NULL; + } + ctx->__internal_heap_allocated = true; + } + + // fill defaults and set magic + console_ctx_defaults(ctx); + +#if CONSOLE_USE_FILE_IO_STREAMS + ctx->in = inf; + ctx->out = outf; + +#else + ctx->ioctx = ioctx; +#endif + + return ctx; +} + +void console_ctx_destroy(console_ctx_t *ctx) +{ + if (!ctx) { + console_internal_error_print("console_ctx_destroy NULL param!"); + return; + } + + if (ctx->__internal_magic != CONSOLE_CTX_MAGIC) { + console_internal_error_print("console_ctx_destroy bad magic!"); + return; + } + + if (ctx->__internal_heap_allocated) { + console_free(ctx); + } +} + +void *console_task_posix(void *param) +{ + console_task(param); + return NULL; +} + +static int my_linenoiseWrite(void *ctx, const char *text, int len) { + return console_write_ctx(ctx, text, len); +} + +static int my_linenoiseRead(void *ctx, char *dest, int count) { + return console_read_ctx(ctx, dest, count); +} + +void console_task(void *param) +{ + if (!console_inited) { + console_internal_error_print("console_task - console not inited!"); + return; + } + + console_ctx_t *ctx = param; + if (!ctx) { + console_internal_error_print("console_task NULL param!"); + return; + } + + if (ctx->__internal_magic != CONSOLE_CTX_MAGIC) { + console_internal_error_print("console_task bad magic!"); + return; + } + + struct linenoiseState ln; + consLnStateInit(&ln); + + // multi-line does not work reliably + consLnSetMultiLine(&ln, 0); + + // Set pointer to the prompt buffer + consLnSetPrompt(&ln, ctx->prompt); + consLnSetBuf(&ln, ctx->line_buffer, CONSOLE_LINE_BUF_LEN); + + ln.allowCtrlDExit = ctx->exit_allowed; + + /* Tell linenoise where to get command completions and hints */ + consLnSetCompletionCallback(&ln, &console_get_completion); + consLnSetHintsCallback(&ln, (linenoiseHintsCallback *) &console_get_hint); + +#if CONSOLE_USE_TERMIOS + if (ctx->in) { + enableRawMode(ctx, fileno(ctx->in)); + } +#endif + + consLnSetReadWrite(&ln, my_linenoiseRead, my_linenoiseWrite, ctx); + + /* Set command history size */ + consLnHistorySetMaxLen(&ln, CONSOLE_HISTORY_LEN); + +#if CONSOLE_FILE_SUPPORT + if (ctx->history_file) { + consLnHistoryLoad(&ln, ctx->history_file); + } +#endif + + /* Main loop */ + while (true) { + // Shutdown check + if (ctx->exit_allowed && ctx->exit_requested) { + // exiting, save history + goto exit; + } + + /* Get a line using linenoise. + * The line is returned when ENTER is pressed. + */ + + if (ctx->loop_handler) { + ctx->loop_handler(ctx); + } + + int len = consLnReadLine(&ln); + + if (len == 0) { /* Ignore empty lines */ + continue; + } + + if (len < 0) { + // This happens if ^C is input at stdin + //console_internal_error_print("read < 1"); + goto exit; + } + + /* Try to run the command */ + int ret; + bool add_to_history = true; + const struct cmd_signature *the_cmd = NULL; + int err = console_handle_cmd(ctx, ln.buf, &ret, &the_cmd); + if (err == CONSOLE_ERR_UNKNOWN_CMD) { + if (ctx->use_colors) { + console_print_ctx(ctx, "\x1b[31;1mUnrecognized command:\x1b[22m \""); + } else { + console_print_ctx(ctx, "Unrecognized command: \""); + } + + // pseudo-hexdump to make it more obvious when the terminal sends garbage + // (this will miss some codes - like ESC - because linenoise tries to sanitize the line) + for (char *pc = ln.buf; *pc != 0; pc++) { + const char c = *pc; + if (c >= 32 && c < 127) { + console_write_ctx(ctx, &c, 1); + } + else { + console_printf_ctx(ctx, COLOR_RESET, "\\x%02X", c); + } + } + + if (ctx->use_colors) { + console_print_ctx(ctx, "\"\x1b[m\n"); + } else { + console_print_ctx(ctx, "\"\n"); + } + EXPENDABLE_CODE({ + console_print_ctx(ctx, "Use `ls` or `help` for a list of commands.\n"); + }) + } + else if (err == CONSOLE_ERR_BAD_CALL) { + add_to_history = false; + // command was empty + } + else if (err == CONSOLE_OK && ret != 0) { + if (ctx->use_colors) { + console_print_ctx(ctx, "\x1b[31;1mCommand err:\x1b[22m "); + console_err_print_ctx(ctx, ret); + console_print_ctx(ctx, "\nUse `-h` for help.\x1b[m\n"); + } else { + console_print_ctx(ctx, "Command err: "); + console_err_print_ctx(ctx, ret); + console_print_ctx(ctx, "\nUse `-h` for help.\n"); + } + } + else if (err != CONSOLE_OK) { + if (ctx->use_colors) { + console_print_ctx(ctx, "\x1b[31;1mConsole err:\x1b[22m "); + console_err_print_ctx(ctx, err); + console_print_ctx(ctx, "\x1b[m\n"); + } else { + console_print_ctx(ctx, "Console err: "); + console_err_print_ctx(ctx, err); + console_print_ctx(ctx, "\n"); + } + } + + if (the_cmd && the_cmd->no_history) { + add_to_history = false; + } + + if (add_to_history) { + /* Add the command to the history */ + consLnHistoryAdd(&ln, ln.buf); + } + } + +exit: + +#if CONSOLE_FILE_SUPPORT + if (ctx->history_file) { + consLnHistorySave(&ln, ctx->history_file); + } +#endif + + consLnHistoryFree(&ln); + // ln lives on stack, do not free! + +#if CONSOLE_USE_TERMIOS && CONSOLE_USE_FILE_IO_STREAMS + if (ctx->in) { + // Restore STDIN to normal state + disableRawMode(ctx, fileno(ctx->in)); + } +#endif + + // Run shutdown handler. Shutdown handler could release user data & destroy the context, for example. + if (ctx->shutdown_handler) { + ctx->shutdown_handler(ctx); + } +} + +#if CONSOLE_USE_TERMIOS && CONSOLE_USE_FILE_IO_STREAMS + +#include +#include + +/* Raw mode: 1960 magic shit. */ +static int enableRawMode(console_ctx_t *ctx, int fd) { + struct termios raw; + + if (!isatty(fd)) goto fatal; + + if (tcgetattr(fd,&ctx->orig_termios) == -1) goto fatal; + + raw = ctx->orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag = OPOST | ONLCR; + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; + return 0; + + fatal: + errno = ENOTTY; + return -1; +} + +static void disableRawMode(console_ctx_t *ctx, int fd) { + tcsetattr(fd, TCSAFLUSH, &ctx->orig_termios); +} + +#endif diff --git a/components/vconsole/libconsole/src/console_filecap.c b/components/vconsole/libconsole/src/console_filecap.c new file mode 100644 index 0000000..1ecec66 --- /dev/null +++ b/components/vconsole/libconsole/src/console_filecap.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include + +#if CONSOLE_USE_MEMSTREAM + +console_err_t console_filecap_init(console_filecap_t *cap) { + cap->buf = NULL; + cap->buf_size = 0; + cap->file = open_memstream(&cap->buf, &cap->buf_size); + if (!cap->file) { + return CONSOLE_ERR_NO_MEM; + } + return CONSOLE_OK; +} + +void console_filecap_end(console_filecap_t *cap) { + // clean up + if (cap->file) { + fclose(cap->file); + cap->file = NULL; + } + if (cap->buf) { + free(cap->buf); // allocated by memstream + cap->buf = NULL; + } +} + +void console_filecap_print_end(console_filecap_t *cap) { + fflush(cap->file); + fclose(cap->file); + cap->file = NULL; + + if (cap->buf) { + console_write(cap->buf, cap->buf_size); + free(cap->buf); // allocated by memstream + cap->buf = NULL; + } +} + +#endif diff --git a/components/vconsole/libconsole/src/console_io.c b/components/vconsole/libconsole/src/console_io.c new file mode 100644 index 0000000..a89e30f --- /dev/null +++ b/components/vconsole/libconsole/src/console_io.c @@ -0,0 +1,194 @@ +// enable "vasprintf" from stdio.h +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "console/console.h" + +#include +#include +#include +#include +#include + +// These are either implemented using unix file descriptors, or in a platform specific way through a void* context +// - then the user code must provide the implementations. +#if CONSOLE_USE_FILE_IO_STREAMS +#include + +bool console_have_stdin_ctx(console_ctx_t *ctx) +{ + return ctx && ctx->in && + !feof(ctx->in); +} + +/** + * Write to console context. + * + * This is also a Linenoise write callback. + * + * Return number of characters written, -1 on error. + */ +int __attribute__((weak)) console_write_ctx(console_ctx_t *ctx, const char *text, size_t len) { + if (!ctx || !ctx->out) { + return -1; + } + + size_t written = fwrite(text, 1, len, ctx->out); + if (written != len) { + return -1; + } + + return (int) written; +} + +/** + * Read from console context's input stream. + * + * This is also a Linenoise read callback. + * + * Return number of characters read, -1 on error + */ +int __attribute__((weak)) console_read_ctx(console_ctx_t *ctx, char *dest, size_t count) { + if (!console_have_stdin_ctx(ctx)) return -1; + ssize_t readn = fread(dest, 1, (size_t) count, ctx->in); + return (int) readn; +} + +#if CONSOLE_USE_TERMIOS +#include + +int console_can_read_ctx(console_ctx_t *ctx) { + if (!console_have_stdin_ctx(ctx)) return -1; + + int fd = fileno(ctx->in); + + struct termios original; + tcgetattr(fd, &original); + + struct termios term; + memcpy(&term, &original, sizeof(term)); + + term.c_lflag &= ~ICANON; + tcsetattr(fd, TCSANOW, &term); + + int characters_buffered = 0; + ioctl(fd, FIONREAD, &characters_buffered); + + tcsetattr(fd, TCSANOW, &original); + + return characters_buffered; +} +#endif // CONSOLE_USE_TERMIOS + +#endif // CONSOLE_USE_FILEDES_IO + +ssize_t console_printf_ctx(console_ctx_t *ctx, console_color_t color, const char *format, ...) { + if (!ctx) return -1; + va_list list; + va_start(list, format); + ssize_t len = console_vprintf_ctx(ctx, color, format, list); + va_end(list); + return len; +} + +ssize_t console_vprintf_ctx(console_ctx_t *ctx, console_color_t color, const char *format, va_list args) { + if (!ctx) return -1; + + if (ctx->use_colors && color != COLOR_RESET) { + switch(color) { + case COLOR_BLACK: + console_write_ctx(ctx, "\x1b[30;1m", 7); + break; + case COLOR_RED: + console_write_ctx(ctx, "\x1b[31;1m", 7); + break; + case COLOR_GREEN: + console_write_ctx(ctx, "\x1b[32;1m", 7); + break; + case COLOR_YELLOW: + console_write_ctx(ctx, "\x1b[33;1m", 7); + break; + case COLOR_BLUE: + console_write_ctx(ctx, "\x1b[34;1m", 7); + break; + case COLOR_MAGENTA: + console_write_ctx(ctx, "\x1b[35;1m", 7); + break; + case COLOR_CYAN: + console_write_ctx(ctx, "\x1b[36;1m", 7); + break; + //case COLOR_WHITE: + default: + console_write_ctx(ctx, "\x1b[37;1m", 7); + break; + } + } + + char *buf = NULL; + ssize_t len = vasprintf(&buf, format, args); + if (buf && len >= 0) { + // change to actual len written, can also result in -1 on error + len = console_write_ctx(ctx, buf, (size_t) len); + free(buf); // allocated by vasprintf + } + + if (ctx->use_colors && color != COLOR_RESET) { + console_write_ctx(ctx, "\x1b[0m", 4); + len += 7+4; + } + return len; +} + +int console_print_ctx(console_ctx_t *ctx, const char *text) { + if (!ctx) return -1; + + return console_write_ctx(ctx, text, (int) strlen(text)); +} + +ssize_t console_println_ctx(console_ctx_t *ctx, const char *text) { + if (!ctx) return -1; + + ssize_t n = console_write_ctx(ctx, text, (int) strlen(text)); + if (n < 0) return n; + ssize_t m = console_write_ctx(ctx, "\n", 2); + if (m < 0) return m; + return n + m; +} + +// ---------------- convenience functions ------------------- + +bool console_have_stdin(void) { + if (!console_context_available()) return false; + return console_have_stdin_ctx(console_active_ctx); +} + +int console_can_read(void) { + if (!console_have_stdin()) return -1; // Input not available + return console_can_read_ctx(console_active_ctx); +} + +/** + * Linenoise read callback. + * + * Return number of characters read, -1 on error + */ +ssize_t console_read(char *dest, size_t count) { + if (!console_have_stdin()) return -1; + return console_read_ctx(console_active_ctx, dest, count); +} + +/** + * Linenoise write callback. + * + * Return number of characters written, -1 on error. + */ +ssize_t console_write(const char *text, size_t len) { + if (!console_context_available()) return -1; + return console_write_ctx(console_active_ctx, text, len); +} + +ssize_t console_println(const char *text) { + if (!console_context_available()) return -1; + return console_println_ctx(console_active_ctx, text); +} diff --git a/components/vconsole/libconsole/src/console_linenoise.c b/components/vconsole/libconsole/src/console_linenoise.c new file mode 100644 index 0000000..d4e4666 --- /dev/null +++ b/components/vconsole/libconsole/src/console_linenoise.c @@ -0,0 +1,1129 @@ +/* linenoise.c -- guerrilla line editing library against the idea that a + * line editing lib needs to be 20,000 lines of C code. + * + * You can find the latest source code at: + * + * http://github.com/antirez/linenoise + * + * Does a number of crazy assumptions that happen to be true in 99.9999% of + * the 2010 UNIX computers around. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2016, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ------------------------------------------------------------------------ + * + * References: + * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html + * + * List of escape sequences used by this program, we do everything just + * with three sequences. In order to be so cheap we may have some + * flickering effect with some slow terminal, but the lesser sequences + * the more compatible. + * + * EL (Erase Line) + * Sequence: ESC [ n K + * Effect: if n is 0 or missing, clear from cursor to end of line + * Effect: if n is 1, clear from beginning of line to cursor + * Effect: if n is 2, clear entire line + * + * CUF (CUrsor Forward) + * Sequence: ESC [ n C + * Effect: moves cursor forward n chars + * + * CUB (CUrsor Backward) + * Sequence: ESC [ n D + * Effect: moves cursor backward n chars + * + * The following is used to get the terminal width if getting + * the width with the TIOCGWINSZ ioctl fails + * + * DSR (Device Status Report) + * Sequence: ESC [ 6 n + * Effect: reports the current cusor position as ESC [ n ; m R + * where n is the row and m is the column + * + * When multi line mode is enabled, we also use an additional escape + * sequence. However multi line editing is disabled by default. + * + * CUU (Cursor Up) + * Sequence: ESC [ n A + * Effect: moves cursor up of n chars. + * + * CUD (Cursor Down) + * Sequence: ESC [ n B + * Effect: moves cursor down of n chars. + * + * When linenoiseClearScreen() is called, two additional escape sequences + * are used in order to clear the screen and position the cursor at home + * position. + * + * CUP (Cursor position) + * Sequence: ESC [ H + * Effect: moves the cursor to upper left corner + * + * ED (Erase display) + * Sequence: ESC [ 2 J + * Effect: clear the whole screen + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "console_linenoise.h" + +extern void console_internal_error_print(const char *msg); + +#define ln_read(buf, cap) (ls->read ? ls->read(ls->rwctx, buf, cap) : -1) +#define ln_write(text, len) (ls->write ? ls->write(ls->rwctx, text, len) : -1) +#define ln_writez(text) ln_write(text, strlen(text)) + +void consLnStateInit(struct linenoiseState *ls) +{ + bzero(ls, sizeof(struct linenoiseState)); + ls->mlmode = false; /* Multi line mode. Default is single line. */ + ls->dumbmode = false; /* Dumb mode where line editing is disabled. Off by default */ + ls->echomode = true; /* Echo (meaningful only in dumb mode) */ + ls->history_max_len = 16; + ls->history_len = 0; + ls->allowCtrlDExit = true; + ls->history = NULL; + ls->buf = NULL; /* Edited line buffer. */ + ls->buflen = 0; /* Edited line buffer size. */ + ls->prompt = NULL; /* Prompt to display. */ + ls->plen = 0; /* Prompt length. */ + ls->pos = 0; /* Current cursor position. */ + ls->oldpos = 0; /* Previous refresh cursor position. */ + ls->len = 0; /* Current edited line length. */ + ls->cols = 0; /* Number of columns in terminal. */ + ls->maxrows = 0; /* Maximum num of rows used so far (multiline mode) */ + ls->history_index = 0; /* The history index we are currently editing. */ + ls->rwctx = NULL; + ls->read = NULL; + ls->write = NULL; +} + +enum KEY_ACTION { + KEY_NULL = 0, /* NULL */ + CTRL_A = 1, /* Ctrl+a */ + CTRL_B = 2, /* Ctrl-b */ + CTRL_C = 3, /* Ctrl-c */ + CTRL_D = 4, /* Ctrl-d */ + CTRL_E = 5, /* Ctrl-e */ + CTRL_F = 6, /* Ctrl-f */ + CTRL_H = 8, /* Ctrl-h */ + TAB = 9, /* Tab */ + CTRL_K = 11, /* Ctrl+k */ + CTRL_L = 12, /* Ctrl+l */ + ENTER = 10, /* Enter */ + RETURN = 13, /* LF */ + CTRL_N = 14, /* Ctrl-n */ + CTRL_P = 16, /* Ctrl-p */ + CTRL_T = 20, /* Ctrl-t */ + CTRL_U = 21, /* Ctrl+u */ + CTRL_W = 23, /* Ctrl+w */ + ESC = 27, /* Escape */ + BACKSPACE = 127 /* Backspace */ +}; + +static void refreshLine(struct linenoiseState *ls); + +/* ======================= Low level terminal handling ====================== */ + +/* Try to get the number of columns in the current terminal, or assume 80 + * if it fails. */ +static inline int getColumns() +{ + return 80; +} + +/* Clear the screen. Used to handle ctrl+l */ +void consLnClearScreen(struct linenoiseState *ls) +{ + ln_writez("\x1b[H\x1b[2J"); +} + +/* Beep, used for completion when there is nothing to complete or when all + * the choices were already shown. */ +static void linenoiseBeep(struct linenoiseState *ls) +{ + ln_write("\x07", 1); +} + +/* ============================== Completion ================================ */ + +/* Free a list of completion option populated by linenoiseAddCompletion(). */ +static void freeCompletions(linenoiseCompletions *lc) +{ + size_t i; + for (i = 0; i < lc->len; i++) { + console_free(lc->cvec[i]); + } + + if (lc->cvec != NULL) { + console_free(lc->cvec); + } +} + +/* This is an helper function for linenoiseEdit() and is called when the + * user types the key in order to complete the string currently in the + * input. + * + * The state of the editing is encapsulated into the pointed linenoiseState + * structure as described in the structure definition. */ +static int completeLine(struct linenoiseState *ls) +{ + linenoiseCompletions lc = {0, NULL}; + int nread; + int nwritten; + char c = 0; + + ls->completionCallback(ls->buf, &lc); + if (lc.len == 0) { + linenoiseBeep(ls); + } + else { + size_t stop = 0; + size_t i = 0; + + while (!stop) { + /* Show completion or original buffer */ + if (i < lc.len) { + struct linenoiseState saved = *ls; + + ls->len = ls->pos = (int) strlen(lc.cvec[i]); + ls->buf = lc.cvec[i]; + refreshLine(ls); + ls->len = saved.len; + ls->pos = saved.pos; + ls->buf = saved.buf; + } + else { + refreshLine(ls); + } + + nread = ln_read(&c, 1); + if (nread <= 0) { + freeCompletions(&lc); + return -1; + } + + switch (c) { + case TAB: /* tab */ + i = (i + 1) % (lc.len + 1); + if (i == lc.len) linenoiseBeep(ls); + break; + case ESC: /* escape */ + /* Re-show original buffer */ + if (i < lc.len) refreshLine(ls); + stop = 1; + break; + default: + /* Update buffer and return */ + if (i < lc.len) { + nwritten = snprintf(ls->buf, ls->buflen, "%s", lc.cvec[i]); + ls->len = ls->pos = nwritten; + } + stop = 1; + break; + } + } + } + + freeCompletions(&lc); + return c; /* Return last read character */ +} + +/* This function is used by the callback function registered by the user + * in order to add completion options given the input string when the + * user typed . See the example.c source code for a very easy to + * understand example. */ +void consLnAddCompletion(linenoiseCompletions *lc, const char *text) +{ + size_t len = strlen(text); + char *copy; + char **cvec; + + copy = console_calloc(len + 1, 1); + if (copy == NULL) return; + memcpy(copy, text, len + 1); + cvec = console_realloc(lc->cvec, sizeof(char *) * (lc->len), sizeof(char *) * (lc->len + 1)); + if (cvec == NULL) { + console_free(copy); + return; + } + lc->cvec = cvec; + lc->cvec[lc->len++] = copy; +} + +/* =========================== Line editing ================================= */ + +/* We define a very simple "append buffer" structure, that is an heap + * allocated string where we can append to. This is useful in order to + * write all the escape sequences in a buffer and flush them to the standard + * output in a single call, to avoid flickering effects. */ +struct abuf { + char *b; + int len; +}; + +static void abInit(struct abuf *ab) +{ + ab->b = NULL; + ab->len = 0; +} + +static void abAppend(struct abuf *ab, const char *s, size_t len) +{ + char *new = console_realloc(ab->b, ab->len, ab->len + len); + if (new == NULL) return; + memcpy(new + ab->len, s, len); + ab->b = new; + ab->len += len; +} + +static void abFree(struct abuf *ab) +{ + console_free(ab->b); +} + +/* Helper of refreshSingleLine() and refreshMultiLine() to show hints + * to the right of the prompt. */ +static void refreshShowHints(struct abuf *ab, struct linenoiseState *ls, int plen) +{ + char seq[64]; + if (ls->hintsCallback && plen + ls->len < ls->cols) { + int color = -1; + int bold = 0; + char *hint = ls->hintsCallback(ls->buf, &color, &bold); + if (hint) { + size_t hintlen = strlen(hint); + int hintmaxlen = ls->cols - (plen + ls->len); + if ((int)hintlen > hintmaxlen) hintlen = hintmaxlen; + if (bold == 1 && color == -1) color = 37; + if (color != -1 || bold != 0) + snprintf(seq, 64, "\033[%d;%d;49m", bold, color); + abAppend(ab, seq, strlen(seq)); + abAppend(ab, hint, hintlen); + if (color != -1 || bold != 0) + abAppend(ab, "\033[0m", 4); + /* Call the function to free the hint returned. */ + if (ls->freeHintsCallback) ls->freeHintsCallback(hint); + } + } +} + +/* Single line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. */ +static void refreshSingleLine(struct linenoiseState *ls) +{ + char seq[64]; + size_t plen = ls->plen; + char *buf = ls->buf; + size_t len = ls->len; + size_t pos = ls->pos; + struct abuf ab; + + while (((int)(plen + pos)) >= ls->cols) { + buf++; + len--; + pos--; + } + while (((int)(plen + len)) > ls->cols) { + len--; + } + + abInit(&ab); + /* Cursor to left edge */ + snprintf(seq, 64, "\r"); + abAppend(&ab, seq, strlen(seq)); + /* Write the prompt and the current buffer content */ + abAppend(&ab, ls->prompt, strlen(ls->prompt)); + abAppend(&ab, buf, len); + /* Show hits if any. */ + refreshShowHints(&ab, ls, plen); + /* Erase to right */ + snprintf(seq, 64, "\x1b[0K"); + abAppend(&ab, seq, strlen(seq)); + /* Move cursor to original position. */ + snprintf(seq, 64, "\r\x1b[%dC", (int) (pos + plen)); + abAppend(&ab, seq, strlen(seq)); + if (ln_write(ab.b, ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +/* Multi line low level line refresh. + * + * Rewrite the currently edited line accordingly to the buffer content, + * cursor position, and number of columns of the terminal. */ +static void refreshMultiLine(struct linenoiseState *ls) +{ + char seq[64]; + int plen = ls->plen; + int rows = (plen + ls->len + ls->cols - 1) / ls->cols; /* rows used by current buf. */ + int rpos = (plen + ls->oldpos + ls->cols) / ls->cols; /* cursor relative row. */ + int rpos2; /* rpos after refresh. */ + int col; /* colum position, zero-based. */ + int old_rows = ls->maxrows; + int j; + struct abuf ab; + + /* Update maxrows if needed. */ + if (rows > (int) ls->maxrows) ls->maxrows = rows; + + /* First step: clear all the lines used before. To do so start by + * going to the last row. */ + abInit(&ab); + if (old_rows - rpos > 0) { + + snprintf(seq, 64, "\x1b[%dB", old_rows - rpos); + abAppend(&ab, seq, strlen(seq)); + } + + /* Now for every row clear it, go up. */ + for (j = 0; j < old_rows - 1; j++) { + + snprintf(seq, 64, "\r\x1b[0K\x1b[1A"); + abAppend(&ab, seq, strlen(seq)); + } + + /* Clean the top line. */ + + snprintf(seq, 64, "\r\x1b[0K"); + abAppend(&ab, seq, strlen(seq)); + + /* Write the prompt and the current buffer content */ + abAppend(&ab, ls->prompt, strlen(ls->prompt)); + abAppend(&ab, ls->buf, ls->len); + + /* Show hits if any. */ + refreshShowHints(&ab, ls, plen); + + /* If we are at the very end of the screen with our prompt, we need to + * emit a newline and move the prompt to the first column. */ + if (ls->pos && + ls->pos == ls->len && + (ls->pos + plen) % ls->cols == 0) { + + abAppend(&ab, "\n", 1); + snprintf(seq, 64, "\r"); + abAppend(&ab, seq, strlen(seq)); + rows++; + if (rows > (int) ls->maxrows) ls->maxrows = rows; + } + + /* Move cursor to right position. */ + rpos2 = (plen + ls->pos + ls->cols) / ls->cols; /* current cursor relative row. */ + + + /* Go up till we reach the expected positon. */ + if (rows - rpos2 > 0) { + + snprintf(seq, 64, "\x1b[%dA", rows - rpos2); + abAppend(&ab, seq, strlen(seq)); + } + + /* Set column. */ + col = (plen + (int) ls->pos) % (int) ls->cols; + + if (col) + snprintf(seq, 64, "\r\x1b[%dC", col); + else + snprintf(seq, 64, "\r"); + abAppend(&ab, seq, strlen(seq)); + + + ls->oldpos = ls->pos; + + if (ln_write(ab.b, ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +/* Calls the two low level functions refreshSingleLine() or + * refreshMultiLine() according to the selected mode. */ +static void refreshLine(struct linenoiseState *ls) +{ + if (ls->mlmode) + refreshMultiLine(ls); + else + refreshSingleLine(ls); +} + +/* Insert the character 'c' at cursor current position. + * + * On error writing to the terminal -1 is returned, otherwise 0. */ +static int linenoiseEditInsert(struct linenoiseState *ls, char c) +{ + if (ls->len < (int)ls->buflen) { + if (ls->len == ls->pos) { + ls->buf[ls->pos] = c; + ls->pos++; + ls->len++; + ls->buf[ls->len] = '\0'; + if ((!ls->mlmode && (int)(ls->plen + ls->len) < ls->cols && !ls->hintsCallback)) { + /* Avoid a full update of the line in the + * trivial case. */ + if (ln_write(&c, 1) == -1) return -1; + } + else { + refreshLine(ls); + } + } + else { + memmove(ls->buf + ls->pos + 1, ls->buf + ls->pos, ls->len - ls->pos); + ls->buf[ls->pos] = c; + ls->len++; + ls->pos++; + ls->buf[ls->len] = '\0'; + refreshLine(ls); + } + } + return 0; +} + +/* Move cursor on the left. */ +static void linenoiseEditMoveLeft(struct linenoiseState *ls) +{ + if (ls->pos > 0) { + ls->pos--; + refreshLine(ls); + } +} + +/* Move cursor on the right. */ +static void linenoiseEditMoveRight(struct linenoiseState *ls) +{ + if (ls->pos != ls->len) { + ls->pos++; + refreshLine(ls); + } +} + +/* Move cursor to the start of the line. */ +static void linenoiseEditMoveHome(struct linenoiseState *ls) +{ + if (ls->pos != 0) { + ls->pos = 0; + refreshLine(ls); + } +} + +/* Move cursor to the end of the line. */ +static void linenoiseEditMoveEnd(struct linenoiseState *ls) +{ + if (ls->pos != ls->len) { + ls->pos = ls->len; + refreshLine(ls); + } +} + +/* Substitute the currently edited line with the next or previous history + * entry as specified by 'dir'. */ +enum linenoise_history_dir { + LINENOISE_HISTORY_NEXT, + LINENOISE_HISTORY_PREV, +}; + +static void linenoiseEditHistoryNext(struct linenoiseState *ls, enum linenoise_history_dir dir) +{ + if (ls->history_len > 1) { + /* Update the current history entry before to + * overwrite it with the next one. */ + console_free(ls->history[ls->history_len - 1 - ls->history_index]); + ls->history[ls->history_len - 1 - ls->history_index] = console_strdup(ls->buf); + /* Show the new entry */ + ls->history_index += (dir == LINENOISE_HISTORY_PREV) ? 1 : -1; + if (ls->history_index < 0) { + ls->history_index = 0; + return; + } + else if (ls->history_index >= ls->history_len) { + ls->history_index = ls->history_len - 1; + return; + } + strncpy(ls->buf, ls->history[ls->history_len - 1 - ls->history_index], ls->buflen); + ls->buf[ls->buflen - 1] = '\0'; + ls->len = ls->pos = (int) strlen(ls->buf); + refreshLine(ls); + } +} + +/* Delete the character at the right of the cursor without altering the cursor + * position. Basically this is what happens with the "Delete" keyboard key. */ +static void linenoiseEditDelete(struct linenoiseState *ls) +{ + if (ls->len > 0 && ls->pos < ls->len) { + memmove(ls->buf + ls->pos, ls->buf + ls->pos + 1, ls->len - ls->pos - 1); + ls->len--; + ls->buf[ls->len] = '\0'; + refreshLine(ls); + } +} + +/* Backspace implementation. */ +static void linenoiseEditBackspace(struct linenoiseState *ls) +{ + if (ls->pos > 0 && ls->len > 0) { + memmove(ls->buf + ls->pos - 1, ls->buf + ls->pos, ls->len - ls->pos); + ls->pos--; + ls->len--; + ls->buf[ls->len] = '\0'; + refreshLine(ls); + } +} + +/* Delete the previosu word, maintaining the cursor at the start of the + * current word. */ +static void linenoiseEditDeletePrevWord(struct linenoiseState *ls) +{ + size_t old_pos = ls->pos; + size_t diff; + + while (ls->pos > 0 && ls->buf[ls->pos - 1] == ' ') + ls->pos--; + while (ls->pos > 0 && ls->buf[ls->pos - 1] != ' ') + ls->pos--; + diff = old_pos - ls->pos; + memmove(ls->buf + ls->pos, ls->buf + old_pos, ls->len - old_pos + 1); + ls->len -= diff; + refreshLine(ls); +} + +/** Get line's length, skipping over ANSI codes */ +static uint32_t getAnsiLen(const char *prompt) +{ + enum { + GAL_BASE, + GAL_ESC, + GAL_DROP_NEXT, + GAL_DROP_UNTIL_ALPHA, + GAL_DROP_UNTIL_X, + } state = GAL_BASE; + + char c; + char x = 0; + uint32_t count = 0; + while ((c = *prompt++) != 0) { + switch (state) { + default: + case GAL_BASE: + if (c == 27) { + state = GAL_ESC; + } + else { + count++; + } + break; + case GAL_ESC: + switch (c) { + case '[': + state = GAL_DROP_UNTIL_ALPHA; + break; + case '#': + state = GAL_DROP_NEXT; + break; + case 'K': + x = 'r'; + state = GAL_DROP_UNTIL_X; + break; + case '(': + case ')': + state = GAL_DROP_NEXT; + break; + default: + state = GAL_BASE; + } + break; + case GAL_DROP_NEXT: + state = GAL_BASE; + break; + case GAL_DROP_UNTIL_ALPHA: + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + state = GAL_BASE; + } + break; + case GAL_DROP_UNTIL_X: + if (c == x) { + state = GAL_BASE; + } + break; + } + } + + return count; +} + +/* This function is the core of the line editing capability of linenoise. + * It expects 'fd' to be already in "raw mode" so that every key pressed + * will be returned ASAP to read(). + * + * The resulting string is put into 'buf' when the user type enter, or + * when ctrl+d is typed. + * + * The function returns the length of the current buffer. */ +static int linenoiseEdit(struct linenoiseState *ls) +{ + if (!ls->buf) { + console_internal_error_print("LN ed no buf"); + return -1; + } + + ls->plen = getAnsiLen(ls->prompt); + ls->oldpos = ls->pos = 0; + ls->len = 0; + ls->cols = getColumns(); + ls->maxrows = 0; + ls->history_index = 0; + + /* Buffer starts empty. */ + ls->buf[0] = '\0'; + + /* The latest history entry is always our current buffer, that + * initially is just an empty string. */ + consLnHistoryAdd(ls, ""); + + if (ln_writez(ls->prompt) == -1) { + console_internal_error_print("LN ed prompt print err"); + return -1; + } + + while (1) { + char c = 0; + int nread; + char seq[3]; + + nread = ln_read(&c, 1); + if (nread <= 0) { + console_internal_error_print("LN ed read err"); + return ls->len; // read error + } + + if (c == 0) continue; // discard null bytes + + /* Only autocomplete when the callback is set. It returns < 0 when + * there was an error reading from fd. Otherwise it will return the + * character that should be handled next. */ + if (c == TAB && ls->completionCallback != NULL) { + int c2 = completeLine(ls); + /* Return on errors */ + if (c2 < 0) return ls->len; + /* Read next character when 0 */ + if (c2 == 0) continue; + c = (char) c2; + } + + switch (c) { + case ENTER: /* enter */ + case RETURN: /* \n */ + ls->history_len--; + console_free(ls->history[ls->history_len]); + if (ls->mlmode) linenoiseEditMoveEnd(ls); + if (ls->hintsCallback) { + /* Force a refresh without hints to leave the previous + * line as the user typed it after a newline. */ + linenoiseHintsCallback *hc = ls->hintsCallback; + ls->hintsCallback = NULL; + refreshLine(ls); + ls->hintsCallback = hc; + } + return (int) ls->len; + case CTRL_C: /* ctrl-c */ + if (ls->len == 0 && ls->allowCtrlDExit) { + errno = EAGAIN; + //console_internal_error_print("LN Ctrl+C, exit"); + return -1; + } else { + // Same as Ctrl+U + ls->buf[0] = '\0'; + ls->pos = ls->len = 0; + refreshLine(ls); + } + break; + case BACKSPACE: /* backspace */ + case 8: /* ctrl-h */ + linenoiseEditBackspace(ls); + break; + case CTRL_D: /* ctrl-d, remove char at right of cursor, or if the + line is empty, act as end-of-file. */ + if (ls->len > 0) { + linenoiseEditDelete(ls); + } + else { + if (ls->allowCtrlDExit) { + ls->history_len--; + console_free(ls->history[ls->history_len]); + //console_internal_error_print("LN Ctrl+D, exit"); + return -1; + } + } + break; + case CTRL_T: /* ctrl-t, swaps current character with previous. */ + if (ls->pos > 0 && ls->pos < ls->len) { + char aux = ls->buf[ls->pos - 1]; + ls->buf[ls->pos - 1] = ls->buf[ls->pos]; + ls->buf[ls->pos] = aux; + if (ls->pos != ls->len - 1) ls->pos++; + refreshLine(ls); + } + break; + case CTRL_B: /* ctrl-b */ + linenoiseEditMoveLeft(ls); + break; + case CTRL_F: /* ctrl-f */ + linenoiseEditMoveRight(ls); + break; + case CTRL_P: /* ctrl-p */ + linenoiseEditHistoryNext(ls, LINENOISE_HISTORY_PREV); + break; + case CTRL_N: /* ctrl-n */ + linenoiseEditHistoryNext(ls, LINENOISE_HISTORY_NEXT); + break; + case ESC: /* escape sequence */ + /* Read the next two bytes representing the escape sequence. */ + if (ln_read(seq, 2) < 2) break; + + /* ESC [ sequences. */ + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + /* Extended escape, read additional byte. */ + if (ln_read(seq + 2, 1) == -1) break; + if (seq[2] == '~') { + switch (seq[1]) { + case '3': /* Delete key. */ + linenoiseEditDelete(ls); + break; + } + } + } + else { + switch (seq[1]) { + case 'A': /* Up */ + linenoiseEditHistoryNext(ls, LINENOISE_HISTORY_PREV); + break; + case 'B': /* Down */ + linenoiseEditHistoryNext(ls, LINENOISE_HISTORY_NEXT); + break; + case 'C': /* Right */ + linenoiseEditMoveRight(ls); + break; + case 'D': /* Left */ + linenoiseEditMoveLeft(ls); + break; + case 'H': /* Home */ + linenoiseEditMoveHome(ls); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(ls); + break; + } + } + } + + /* ESC O sequences. */ + else if (seq[0] == 'O') { + switch (seq[1]) { + case 'H': /* Home */ + linenoiseEditMoveHome(ls); + break; + case 'F': /* End*/ + linenoiseEditMoveEnd(ls); + break; + } + } + break; + default: + if (linenoiseEditInsert(ls, c)) return -1; + break; + case CTRL_U: /* Ctrl+u, delete the whole line. */ + ls->buf[0] = '\0'; + ls->pos = ls->len = 0; + refreshLine(ls); + break; + case CTRL_K: /* Ctrl+k, delete from current to end of line. */ + ls->buf[ls->pos] = '\0'; + ls->len = ls->pos; + refreshLine(ls); + break; + case CTRL_A: /* Ctrl+a, go to the start of the line */ + linenoiseEditMoveHome(ls); + break; + case CTRL_E: /* ctrl+e, go to the end of the line */ + linenoiseEditMoveEnd(ls); + break; + case CTRL_L: /* ctrl+l, clear screen */ + consLnClearScreen(ls); + refreshLine(ls); + break; + case CTRL_W: /* ctrl+w, delete previous word */ + linenoiseEditDeletePrevWord(ls); + break; + } + } +} + +static int linenoiseRaw(struct linenoiseState *ls) +{ + int count; + + if (ls->buflen == 0) { + errno = EINVAL; + console_internal_error_print("LN raw buflen 0"); + return -1; + } + + count = linenoiseEdit(ls); + + if (count >= 0) ln_write("\r\n", 2); + + return count; +} + +static bool last_line_ended_with_0D = false; + +static int linenoiseDumb(struct linenoiseState *ls) +{ + /* dumb terminal, fall back to fgets */ + ln_writez(ls->prompt); + int count = 0; + char c = 0; + while (count < (int)ls->buflen) { + int num = ln_read(&c, 1); + if (num == 0) break; + if (c == 0) continue; + + int shouldprint = 1; + if (c == '\n' || c == '\r') { + // prevent CRLF acting as two newlines + if (c == '\n' && last_line_ended_with_0D) { + last_line_ended_with_0D = false; + continue; + } + + if (c == '\r') { + last_line_ended_with_0D = true; + } + break; + } + else if (c >= 0x1c && c <= 0x1f) { + shouldprint = 0; /* consume arrow keys */ + } + else if (c == BACKSPACE || c == 0x8) { + if (count > 0) { + ls->buf[count - 1] = 0; + count--; + if (ls->echomode) ln_write("\x08 ", 2); // second x08 is printed by the fputc below + } + else { + shouldprint = 0; + } + } + else { + ls->buf[count] = c; + ++count; + } + + if (ls->echomode && shouldprint) { + ln_write(&c, 1); /* echo */ + } + } + + ls->buf[count] = 0; + + ln_write("\r\n", 2); + return count; +} + +static void sanitize(char *src) +{ + char *dst = src; + for (int c = *src; c != 0; src++, c = *src) { + if (isprint(c)) { + *dst = (char) c; + ++dst; + } + } + *dst = 0; +} + +/* The high level function that is the main API of the linenoise library. */ +int consLnReadLine(struct linenoiseState *ls) +{ + int count = 0; + if (!ls->dumbmode) { + count = linenoiseRaw(ls); + } + else { + count = linenoiseDumb(ls); + } + + if (count > 0) { + sanitize(ls->buf); + count = (int) strlen(ls->buf); + } + + return count; +} + +/* ================================ History ================================= */ + +/** Free history buffer for instance */ +void consLnHistoryFree(struct linenoiseState *ls) +{ + if (ls->history) { + for (int j = 0; j < ls->history_len; j++) { + console_free(ls->history[j]); + } + console_free(ls->history); + } + ls->history = NULL; +} + +/* This is the API call to add a new entry in the linenoise history. + * It uses a fixed array of char pointers that are shifted (memmoved) + * when the history max length is reached in order to remove the older + * entry and make room for the new one, so it is not exactly suitable for huge + * histories, but will work well for a few hundred of entries. + * + * Using a circular buffer is smarter, but a bit more complex to handle. */ +int consLnHistoryAdd(struct linenoiseState *ls, const char *line) +{ + char *linecopy; + + if (ls->history_max_len == 0) return 0; + + /* Initialization on first call. */ + if (ls->history == NULL) { + ls->history = console_calloc(sizeof(char *) * ls->history_max_len, 1); + if (ls->history == NULL) return 0; + memset(ls->history, 0, (sizeof(char *) * ls->history_max_len)); + } + + /* Don't add duplicated lines. */ + if (ls->history_len && !strcmp(ls->history[ls->history_len - 1], line)) return 0; + + /* Add an heap allocated copy of the line in the history. + * If we reached the max length, remove the older line. */ + linecopy = console_strdup(line); + if (!linecopy) return 0; + if (ls->history_len == ls->history_max_len) { + console_free(ls->history[0]); + memmove(ls->history, ls->history + 1, sizeof(char *) * (ls->history_max_len - 1)); + ls->history_len--; + } + ls->history[ls->history_len] = linecopy; + ls->history_len++; + return 1; +} + +/* Set the maximum length for the history. This function can be called even + * if there is already some history, the function will make sure to retain + * just the latest 'len' elements if the new history length value is smaller + * than the amount of items already inside the history. */ +int consLnHistorySetMaxLen(struct linenoiseState *ls, int len) +{ + char **new; + + if (len < 1) return 0; + if (ls->history) { + int tocopy = ls->history_len; + + new = console_calloc(sizeof(char *) * len, 1); + if (new == NULL) return 0; + + /* If we can't copy everything, free the elements we'll not use. */ + if (len < tocopy) { + int j; + + for (j = 0; j < tocopy - len; j++) console_free(ls->history[j]); + tocopy = len; + } + memset(new, 0, sizeof(char *) * len); + memcpy(new, ls->history + (ls->history_len - tocopy), sizeof(char *) * tocopy); + console_free(ls->history); + ls->history = new; + } + ls->history_max_len = len; + if (ls->history_len > ls->history_max_len) + ls->history_len = ls->history_max_len; + return 1; +} + +#if CONSOLE_FILE_SUPPORT +/* Save the history in the specified file. On success 0 is returned + * otherwise -1 is returned. */ +int consLnHistorySave(struct linenoiseState *ls, const char *filename) +{ + FILE *fp; + int j; + + fp = fopen(filename, "w"); + if (fp == NULL) return -1; + for (j = 0; j < ls->history_len; j++) + fprintf(fp, "%s\n", ls->history[j]); + fclose(fp); + return 0; +} + +/* Load the history from the specified file. If the file does not exist + * zero is returned and no operation is performed. + * + * If the file exists and the operation succeeded 0 is returned, otherwise + * on error -1 is returned. */ +int consLnHistoryLoad(struct linenoiseState *ls, const char *filename) +{ + FILE *fp = fopen(filename, "r"); + char buf[LINENOISE_HISTORY_MAX_LINE]; + + if (fp == NULL) return -1; + + // Discard old history, if any + consLnHistoryFree(ls); + + while (fgets(buf, LINENOISE_HISTORY_MAX_LINE, fp) != NULL) { + char *p; + + p = strchr(buf, '\r'); + if (!p) p = strchr(buf, '\n'); + if (p) *p = '\0'; + consLnHistoryAdd(ls, buf); + } + fclose(fp); + return 0; +} +#endif diff --git a/components/vconsole/libconsole/src/console_linenoise.h b/components/vconsole/libconsole/src/console_linenoise.h new file mode 100644 index 0000000..4a3f7c6 --- /dev/null +++ b/components/vconsole/libconsole/src/console_linenoise.h @@ -0,0 +1,248 @@ +/* linenoise.h -- VERSION 1.0 + * + * Guerrilla line editing library against the idea that a line editing lib + * needs to be 20,000 lines of C code. + * + * See linenoise.c for more information. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010-2014, Salvatore Sanfilippo + * Copyright (c) 2010-2013, Pieter Noordhuis + * + * THIS IS A MODIFIED VERSION THAT REMOVES GLOBAL STATE AND SUPPORTS + * OTHER IO THAN STDOUT/STDIN. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LINENOISE_H +#define LINENOISE_H + +#ifndef LINENOISE_HISTORY_MAX_LINE +#define LINENOISE_HISTORY_MAX_LINE 512 +#endif + +#include +#include +#include +#include + +#include "console/config.h" + +typedef struct linenoiseCompletions { + size_t len; + char **cvec; +} linenoiseCompletions; + +/** + * Get completions + */ +typedef void (linenoiseCompletionCallback)(const char *typed, linenoiseCompletions *); + +/** + * Get a hint for typed text + */ +typedef char* (linenoiseHintsCallback)(const char *typed, int *color, int *bold); + +/** + * Dispose of a hint returned by `linenoiseHintsCallback()` + */ +typedef void (linenoiseFreeHintsCallback)(void *); + +/** + * Linenoise write callback. + * + * Return number of characters written, -1 on error. + * "ctx" may be a fd cast to void* or other value passed during LN init + */ +typedef int (linenoiseWrite)(void *ctx, const char *text, int len); + +/** + * Linenoise read callback. + * + * Return number of characters read, -1 on error; + * "ctx" may be a fd cast to void* or other value passed during LN init + */ +typedef int (linenoiseRead)(void *ctx, char *dest, int count); + +/** + * Linenoise state; exposed in header to allow static allocation + * + * Use `linenoiseStateInit()` to set default values. + * + * To shutdown, use `linenoiseHistoryFree()` and then free the struct as needed. + */ +struct linenoiseState { + bool mlmode; /* Multi line mode. Default is single line. */ + bool dumbmode; /* Dumb mode where line editing is disabled. Off by default */ + bool echomode; /* Echo (meaningful only in dumb mode) */ + bool allowCtrlDExit; + int history_max_len; + int history_len; + char **history; + + char *buf; /* Edited line buffer. */ + size_t buflen; /* Edited line buffer size. */ + const char *prompt; /* Prompt to display. */ + size_t plen; /* Prompt length. */ + int pos; /* Current cursor position. */ + int oldpos; /* Previous refresh cursor position. */ + int len; /* Current edited line length. */ + int cols; /* Number of columns in terminal. */ + int maxrows; /* Maximum num of rows used so far (multiline mode) */ + int history_index; /* The history index we are currently editing. */ + + linenoiseCompletionCallback *completionCallback; + linenoiseHintsCallback *hintsCallback; + linenoiseFreeHintsCallback *freeHintsCallback; + + void *rwctx; + linenoiseWrite *write; + linenoiseRead *read; +}; + +/** + * Initialize the state struct to defaults. + * + * Before the library is used, also set prompt, buffer, + * the read/write callbacks, r/w context (if used), + * completion, hint and free-hint callbacks + */ +void consLnStateInit(struct linenoiseState *ls); + +/** Set buffer and its capacity */ +static inline void consLnSetBuf(struct linenoiseState *ls, char *buf, size_t cap) { + ls->buf = buf; + ls->buflen = cap - 1; /* Make sure there is always space for the nulterm */ +} + +/** Set prompt pointer. Can be constant or dynamically allocated. + * Will NOT be mutated by the library. */ +static inline void consLnSetPrompt(struct linenoiseState *ls, const char *prompt) { + ls->prompt = prompt; +} + +/** Set buffer and its capacity */ +static inline void consLnSetReadWrite(struct linenoiseState *ls, linenoiseRead *read, linenoiseWrite *write, void *ctx) { + ls->read = read; + ls->write = write; + ls->rwctx = ctx; +} + +/** Set completion CB */ +static inline void consLnSetCompletionCallback(struct linenoiseState *ls, linenoiseCompletionCallback *compl) { + ls->completionCallback = compl; +} + +/** Set hint CB */ +static inline void consLnSetHintsCallback(struct linenoiseState *ls, linenoiseHintsCallback *hints) { + ls->hintsCallback = hints; +} + +/** Set free hints CB */ +static inline void consLnSetFreeHintsCallback(struct linenoiseState *ls, linenoiseFreeHintsCallback *freeh) { + ls->freeHintsCallback = freeh; +} + +/** This function is used by the callback function registered by the user + * in order to add completion options given the input string when the + * user typed . See the example.c source code for a very easy to + * understand example. + * + * The completion will be duplicated, it does not need to live past calling + * this function. + * */ +void consLnAddCompletion(linenoiseCompletions *lc, const char *text); + +/** The high level function that is the main API of the linenoise library. */ +int consLnReadLine(struct linenoiseState *ls); // buffer is in "ls" + +/** This is the API call to add a new entry in the linenoise history. + * It uses a fixed array of char pointers that are shifted (memmoved) + * when the history max length is reached in order to remove the older + * entry and make room for the new one, so it is not exactly suitable for huge + * histories, but will work well for a few hundred of entries. + * + * Using a circular buffer is smarter, but a bit more complex to handle. */ +int consLnHistoryAdd(struct linenoiseState *ls, const char *line); + +/** Set the maximum length for the history. This function can be called even + * if there is already some history, the function will make sure to retain + * just the latest 'len' elements if the new history length value is smaller + * than the amount of items already inside the history. */ +int consLnHistorySetMaxLen(struct linenoiseState *ls, int len); + +#if CONSOLE_FILE_SUPPORT +/** Save the history in the specified file. On success 0 is returned, + * on error -1 is returned. */ +int consLnHistorySave(struct linenoiseState *ls, const char *filename); + +/** Load the history from the specified file. If the file does not exist + * zero is returned and no operation is performed. + * + * If the file exists and the operation succeeded 0 is returned, otherwise + * on error -1 is returned. */ +int consLnHistoryLoad(struct linenoiseState *ls, const char *filename); +#endif + +/** Free history buffer for instance */ +void consLnHistoryFree(struct linenoiseState *ls); + +/** Clear the screen. Used to handle ctrl+l */ +void consLnClearScreen(struct linenoiseState *ls); + +/** Set if to use or not the multi line mode. */ +static inline void consLnSetMultiLine(struct linenoiseState *ls, int ml) { + ls->mlmode = ml; +} + +/** Get ml mode state */ +static inline bool consLnGetMultiLine(struct linenoiseState *ls) { + return ls->mlmode; +} + +/** Set if terminal does not recognize escape sequences */ +static inline void consLnSetDumbMode(struct linenoiseState *ls, int dumb) { + ls->dumbmode = dumb; +} + +/** Get dumb mode state */ +static inline bool consLnGetDumbMode(struct linenoiseState *ls) { + return ls->dumbmode; +} + +/** Enable/disable echo on keypress (only applies to dumb mode) */ +static inline void consLnSetEchoMode(struct linenoiseState *ls, int set) { + ls->echomode = set; +} + +/** Get echo mode state */ +static inline bool consLnGetEchoMode(struct linenoiseState *ls) { + return ls->echomode; +} + +#endif /* LINENOISE_H */ diff --git a/components/vconsole/libconsole/src/console_prefix_match.c b/components/vconsole/libconsole/src/console_prefix_match.c new file mode 100644 index 0000000..5629b00 --- /dev/null +++ b/components/vconsole/libconsole/src/console_prefix_match.c @@ -0,0 +1,196 @@ +#include +#include +#include +#include "console/prefix_match.h" + +int prefix_match(const char *value, const char **options, int flags) { + flags &= ~PREFIXMATCH_MULTI_PARTIAL; // this doesn't make sense here + bool case_sensitive = PREFIXMATCH_CASE_SENSITIVE == (flags & PREFIXMATCH_CASE_SENSITIVE); + bool can_abbrev = 0 == (flags & PREFIXMATCH_NOABBREV); + + int (*cmpfn) (const char *, const char *) = case_sensitive ? strcmp : strcasecmp; + int (*ncmpfn) (const char *, const char *, size_t) = case_sensitive ? strncmp : strncasecmp; + + if (!value || !options) return -1; + size_t input_len = strlen(value); + const char *option = NULL; + int counter = 0; + int result = -1; + while (NULL != (option = options[counter])) { + if (cmpfn(option, value) == 0) { + return counter; // full exact match + } else { + // Test for partial match + if (can_abbrev && ncmpfn(value, option, input_len) == 0) { + if (result == -1) { + result = counter; // first partial match + } else { + // ambiguous match + return -1; + } + } + } + counter++; + } + return result; +} + +size_t pm_word_len(const char * restrict word, const char * restrict delims) { + char d; + const char *dp = delims; + size_t word_len = 0; + if (!word || !delims) return 0; + while ('\0' != (d = *dp++)) { + char *end = strchr(word, d); + if (NULL == end) continue; + size_t len = end - word; + if (!word_len || len < word_len) { + word_len = len; + } + } + if (!word_len) { + word_len = strlen(word); + } + return word_len; +} + +size_t pm_count_words(const char * restrict sentence, const char * restrict delims) { + char c; + size_t n = 0; + bool in_word = false; + if (!sentence || !delims) return 0; + while (0 != (c = *sentence++)) { + bool is_delim = NULL != strchr(delims, c); + if (is_delim && in_word) { + in_word = false; + } else if (!in_word && !is_delim) { + n++; + in_word = true; + } + } + return n; +} + +const char *pm_skip_words(const char * restrict sentence, const char * restrict delims, size_t skip) { + char c; + size_t n = 0; + bool in_word = false; + if (!sentence || !delims) return NULL; + while (0 != (c = *sentence++)) { + bool is_delim = NULL != strchr(delims, c); + if (is_delim && in_word) { + in_word = false; + skip--; + if (skip == 0) { + return sentence - 1; + } + } else if (!in_word && !is_delim) { + n++; + in_word = true; + } + } + return sentence - 1; +} + +enum pm_test_result prefix_multipart_test( + const char * restrict tested, + const char* restrict full, + const char * restrict delims, + int flags +) { + bool case_sensitive = PREFIXMATCH_CASE_SENSITIVE == (flags & PREFIXMATCH_CASE_SENSITIVE); + bool can_abbrev = 0 == (flags & PREFIXMATCH_NOABBREV); + + int (*ncmpfn) (const char *, const char *, size_t) = case_sensitive ? strncmp : strncasecmp; + // lazy shortcut first... + if ((case_sensitive && 0 == strcmp(tested, full)) || (!case_sensitive && 0 == strcasecmp(tested, full))) { + return PM_TEST_MATCH; // full match + } + + const char *word_t = tested; + const char *word_f = full; + size_t word_t_len = 0; + size_t word_f_len = 0; + while (1) { + word_t += word_t_len; + word_f += word_f_len; + + // advance past leading delims, if any + while (*word_t != '\0' && NULL != strchr(delims, *word_t)) word_t++; + while (*word_f != '\0' && NULL != strchr(delims, *word_f)) word_f++; + + // test for terminator + if (*word_t == '\0' && *word_f == '\0') { + // both ended at the same number of words + return PM_TEST_MATCH; // full match + } + + if (*word_t == '\0' || *word_f == '\0') { + // sentences ended at different length + if (0 != (flags & PREFIXMATCH_MULTI_PARTIAL) && *word_f != '\0') { // word prefix match (a is a prefix of b) + return PM_TEST_MATCH_MULTI_PARTIAL; + } else { + return PM_TEST_NO_MATCH; + } + } + + // find end of the words + word_t_len = pm_word_len(word_t, delims); + word_f_len = pm_word_len(word_f, delims); + + if (word_t_len > word_f_len || (!can_abbrev && word_t_len != word_f_len)) { + return PM_TEST_NO_MATCH; + } + + int cmp = ncmpfn(word_t, word_f, word_t_len); + if (0 != cmp) { // words differ + return PM_TEST_NO_MATCH; + } + } +} + +int prefix_multipart_match(const char * restrict value, const char **options, const char * restrict delims, int flags) { + bool multi_partial = 0 != (flags & PREFIXMATCH_MULTI_PARTIAL); + flags &= ~PREFIXMATCH_MULTI_PARTIAL; // turn it off for passing the to test fn + bool can_abbrev = 0 == (flags & PREFIXMATCH_NOABBREV); + + if (!value || !options) return -1; + const char *option = NULL; + int counter = 0; + int result = -1; + int result_partial = -1; // -1=none yet, -2=ambiguous multi-word partial, >=0=index + int result_partial_nwords = 0; + while (NULL != (option = options[counter])) { + if (PM_TEST_MATCH == prefix_multipart_test(value, option, delims, flags | PREFIXMATCH_NOABBREV)) { + return counter; // full exact match + } else if (can_abbrev) { + // Test for partial match + if (PM_TEST_MATCH == prefix_multipart_test(value, option, delims, flags)) { + if (result == -1) { + result = counter; // first partial match in all words + } else { + return -1; + } + } else if (multi_partial && PM_TEST_MATCH_MULTI_PARTIAL == prefix_multipart_test(value, option, delims, flags | PREFIXMATCH_MULTI_PARTIAL)) { + int nwords = pm_count_words(option, delims); + if (result_partial == -1 || result_partial_nwords < nwords) { + result_partial = counter; // first partial match + result_partial_nwords = nwords; + } else { + result_partial = -2; + } + } + } + counter++; + } + + if (result != -1) { + return result; + } + + if (result_partial >= 0) { + return result_partial; + } + + return -1; +} diff --git a/components/vconsole/libconsole/src/console_split_argv.c b/components/vconsole/libconsole/src/console_split_argv.c new file mode 100644 index 0000000..383375f --- /dev/null +++ b/components/vconsole/libconsole/src/console_split_argv.c @@ -0,0 +1,120 @@ +// Copyright 2016-2017 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#define SS_FLAG_ESCAPE 0x8 + +typedef enum { + /* parsing the space between arguments */ + SS_SPACE = 0x0, + /* parsing an argument which isn't quoted */ + SS_ARG = 0x1, + /* parsing a quoted argument */ + SS_QUOTED_ARG = 0x2, + /* parsing an escape sequence within unquoted argument */ + SS_ARG_ESCAPED = SS_ARG | SS_FLAG_ESCAPE, + /* parsing an escape sequence within a quoted argument */ + SS_QUOTED_ARG_ESCAPED = SS_QUOTED_ARG | SS_FLAG_ESCAPE, +} split_state_t; + +size_t console_split_argv(char *line, char **argv, size_t argv_size) +{ + const int QUOTE = '"'; + const int ESCAPE = '\\'; + const int SPACE = ' '; + split_state_t state = SS_SPACE; + int argc = 0; + char *next_arg_start = line; + char *out_ptr = line; + for (char *in_ptr = line; argc < (int)(argv_size - 1); ++in_ptr) { + int char_in = (unsigned char) *in_ptr; + if (char_in == 0) { + break; + } + int char_out = -1; + + /* helper function, called when done with an argument */ + void end_arg() { + char_out = 0; + argv[argc++] = next_arg_start; + state = SS_SPACE; + } + + switch (state) { + case SS_SPACE: + if (char_in == SPACE) { + /* skip space */ + } else if (char_in == QUOTE) { + next_arg_start = out_ptr; + state = SS_QUOTED_ARG; + } else if (char_in == ESCAPE) { + next_arg_start = out_ptr; + state = SS_ARG_ESCAPED; + } else { + next_arg_start = out_ptr; + state = SS_ARG; + char_out = char_in; + } + break; + + case SS_QUOTED_ARG: + if (char_in == QUOTE) { + end_arg(); + } else if (char_in == ESCAPE) { + state = SS_QUOTED_ARG_ESCAPED; + } else { + char_out = char_in; + } + break; + + case SS_ARG_ESCAPED: + case SS_QUOTED_ARG_ESCAPED: + if (char_in == ESCAPE || char_in == QUOTE || char_in == SPACE) { + char_out = char_in; + } else { + /* unrecognized escape character, skip */ + } + state = (split_state_t) (state & (~SS_FLAG_ESCAPE)); + break; + + case SS_ARG: + if (char_in == SPACE) { + end_arg(); + } else if (char_in == ESCAPE) { + state = SS_ARG_ESCAPED; + } else { + char_out = char_in; + } + break; + } + /* need to output anything? */ + if (char_out >= 0) { + *out_ptr = char_out; + ++out_ptr; + } + } + /* make sure the final argument is terminated */ + *out_ptr = 0; + /* finalize the last argument */ + if (state != SS_SPACE && argc < ((int)argv_size - 1)) { + argv[argc++] = next_arg_start; + } + /* add a NULL at the end of argv */ + argv[argc] = NULL; + + return argc; +} diff --git a/components/vconsole/libconsole/src/console_split_argv.h b/components/vconsole/libconsole/src/console_split_argv.h new file mode 100644 index 0000000..f126d70 --- /dev/null +++ b/components/vconsole/libconsole/src/console_split_argv.h @@ -0,0 +1,36 @@ +/** + * Command argument splitting + * + * Created on 2020/02/28. + */ + +#ifndef VCOM_CONSOLE_SPLIT_ARGV_H +#define VCOM_CONSOLE_SPLIT_ARGV_H + +/** + * @brief Split command line into arguments in place + * + * - This function finds whitespace-separated arguments in the given input line. + * + * 'abc def 1 20 .3' -> [ 'abc', 'def', '1', '20', '.3' ] + * + * - Argument which include spaces may be surrounded with quotes. In this case + * spaces are preserved and quotes are stripped. + * + * 'abc "123 456" def' -> [ 'abc', '123 456', 'def' ] + * + * - Escape sequences may be used to produce backslash, double quote, and space: + * + * 'a\ b\\c\"' -> [ 'a b\c"' ] + * + * Pointers to at most argv_size - 1 arguments are returned in argv array. + * The pointer after the last one (i.e. argv[argc]) is set to NULL. + * + * @param line pointer to buffer to parse; it is modified in place + * @param argv array where the pointers to arguments are written + * @param argv_size number of elements in argv_array (max. number of arguments) + * @return number of arguments found (argc) + */ +size_t console_split_argv(char *line, char **argv, size_t argv_size); + +#endif //VCOM_CONSOLE_SPLIT_ARGV_H diff --git a/components/vconsole/libconsole/src/console_utils.c b/components/vconsole/libconsole/src/console_utils.c new file mode 100644 index 0000000..5eacfc9 --- /dev/null +++ b/components/vconsole/libconsole/src/console_utils.c @@ -0,0 +1,204 @@ +// +// Created by MightyPork on 2020/03/11. +// + +#include +#include +#include "console/console.h" +#include "console/utils.h" + +#if (CONSOLE_HAVE_CSP && !CSP_POSIX) || CONSOLE_TESTING_ALLOC_FUNCS + #include +#else + #include +#endif + +// Based on: https://stackoverflow.com/a/7776146/2180189 +void console_hexdump(const void *data, size_t len) +{ + size_t i; + char asciibuf[17]; + const unsigned char *pc = (const unsigned char *) data; + + // Length checks. + + if (len <= 0) { + console_print("BAD LENGTH\r\n"); + return; + } + + // Process every byte in the data. + for (i = 0; i < len; i++) { + size_t ai = i % 16; + + // Multiple of 16 means new line (with line offset). + if (ai == 0) { + // Don't print ASCII buffer for the "zeroth" line. + if (i != 0) { + console_printf(" |%s|\r\n", asciibuf); + } + + // Output the offset. + + console_printf("%4d :", (int)i); + } + + // Now the hex code for the specific character. + console_printf(" %02x", pc[i]); + + // And buffer a printable ASCII character for later. + if ((pc[i] < 0x20) || (pc[i] > 0x7e)) { + asciibuf[ai] = '.'; + } + else { + asciibuf[ai] = (char) pc[i]; + } + + asciibuf[ai + 1] = '\0'; + } + + // Pad out last line if not exactly 16 characters. + while ((i % 16) != 0) { + console_print(" "); + i++; + } + + // And print the final ASCII buffer. + console_printf(" |%s|\r\n", asciibuf); +} + +static int hex_char_decode(char x) +{ + if (x >= '0' && x <= '9') { + return x - '0'; + } + else if (x >= 'a' && x <= 'f') { + return 10 + (x - 'a'); + } + else if (x >= 'A' && x <= 'F') { + return 10 + (x - 'A'); + } + else { + return -1; + } +} + +int console_base16_decode(const char *hex, void *dest, size_t capacity) +{ + if (!hex || !dest) return -1; + + const char *px = (const char *) hex; + uint8_t *pd = (unsigned char *) dest; + int hlen = (int) strlen(hex); + if (hlen % 2) { + return -2; + } + int blen = hlen / 2; + if (blen > (int) capacity) { + return -3; + } + + int v; + uint8_t byte; + for (int i = 0; i < blen; i++) { + v = hex_char_decode(*px++); + if (v == -1) { + return -2; + } + + byte = (v & 0x0F) << 4; + + v = hex_char_decode(*px++); + if (v == -1) { + return -2; + } + + byte |= (v & 0x0F); + + *pd++ = byte; + } + + return blen; +} + +void * __attribute__((malloc)) console_malloc(size_t size) { + #if (CONSOLE_HAVE_CSP && !CSP_POSIX) || CONSOLE_TESTING_ALLOC_FUNCS + return csp_malloc(size); + #else + return malloc(size); + #endif +} + +void * console_realloc(void *ptr, size_t oldsize, size_t newsize) { + #if (CONSOLE_HAVE_CSP && !CSP_POSIX) || CONSOLE_TESTING_ALLOC_FUNCS + // csp / freertos do not provide realloc, simply allocate a new one and copy data over. + + if (oldsize >= newsize) { + // no realloc needed + if (!ptr) { // NULL was given, act as malloc + ptr = csp_malloc(newsize); + } + return ptr; + } + + void *tmp = csp_malloc(newsize); + if (!tmp) { + // "If realloc() fails, the original block is left untouched; it is not freed or moved." + return NULL; + } + if (ptr) { + // copy if there is anything to copy from + memcpy(tmp, ptr, oldsize); // we know oldsize < newsize, otherwise the check above returns. + // discard the old buffer + csp_free(ptr); + } + return tmp; + #else + (void) oldsize; // unused + return realloc(ptr, newsize); + #endif +} + +void * __attribute__((malloc,alloc_size(1,2))) console_calloc(size_t nmemb, size_t size) { + #if (CONSOLE_HAVE_CSP && !CSP_POSIX) || CONSOLE_TESTING_ALLOC_FUNCS + return csp_calloc(nmemb, size); + #else + return calloc(nmemb, size); + #endif +} + +void console_free(void *ptr) { + #if (CONSOLE_HAVE_CSP && !CSP_POSIX) || CONSOLE_TESTING_ALLOC_FUNCS + return csp_free(ptr); + #else + return free(ptr); + #endif +} + +char * console_strdup(const char *ptr) { + #if (CONSOLE_HAVE_CSP && !CSP_POSIX) || CONSOLE_TESTING_ALLOC_FUNCS + if (!ptr) return NULL; + size_t len = strlen(ptr); + char *copy = console_malloc(len+1); + if (!copy) return NULL; + strncpy(copy, ptr, len); + copy[len]=0; + return copy; + #else + return strdup(ptr); + #endif +} + +char * console_strndup(const char *ptr, size_t maxlen) { + #if (CONSOLE_HAVE_CSP && !CSP_POSIX) || CONSOLE_TESTING_ALLOC_FUNCS + if (!ptr) return NULL; + size_t len = strnlen(ptr, maxlen); + char *copy = console_malloc(len+1); + if (!copy) return NULL; + strncpy(copy, ptr, len); + copy[len]=0; + return copy; + #else + return strndup(ptr, maxlen); + #endif +} diff --git a/components/vconsole/libconsole/src/queue.h b/components/vconsole/libconsole/src/queue.h new file mode 100644 index 0000000..29ee670 --- /dev/null +++ b/components/vconsole/libconsole/src/queue.h @@ -0,0 +1,645 @@ +/*- + * Copyright (c) 1991, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * @(#)queue.h 8.5 (Berkeley) 8/20/94 + * $FreeBSD$ + */ + +#ifndef _SYS_QUEUE_H_ +#define _SYS_QUEUE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * This file defines four types of data structures: singly-linked lists, + * singly-linked tail queues, lists and tail queues. + * + * A singly-linked list is headed by a single forward pointer. The elements + * are singly linked for minimum space and pointer manipulation overhead at + * the expense of O(n) removal for arbitrary elements. New elements can be + * added to the list after an existing element or at the head of the list. + * Elements being removed from the head of the list should use the explicit + * macro for this purpose for optimum efficiency. A singly-linked list may + * only be traversed in the forward direction. Singly-linked lists are ideal + * for applications with large datasets and few or no removals or for + * implementing a LIFO queue. + * + * A singly-linked tail queue is headed by a pair of pointers, one to the + * head of the list and the other to the tail of the list. The elements are + * singly linked for minimum space and pointer manipulation overhead at the + * expense of O(n) removal for arbitrary elements. New elements can be added + * to the list after an existing element, at the head of the list, or at the + * end of the list. Elements being removed from the head of the tail queue + * should use the explicit macro for this purpose for optimum efficiency. + * A singly-linked tail queue may only be traversed in the forward direction. + * Singly-linked tail queues are ideal for applications with large datasets + * and few or no removals or for implementing a FIFO queue. + * + * A list is headed by a single forward pointer (or an array of forward + * pointers for a hash table header). The elements are doubly linked + * so that an arbitrary element can be removed without a need to + * traverse the list. New elements can be added to the list before + * or after an existing element or at the head of the list. A list + * may only be traversed in the forward direction. + * + * A tail queue is headed by a pair of pointers, one to the head of the + * list and the other to the tail of the list. The elements are doubly + * linked so that an arbitrary element can be removed without a need to + * traverse the list. New elements can be added to the list before or + * after an existing element, at the head of the list, or at the end of + * the list. A tail queue may be traversed in either direction. + * + * For details on the use of these macros, see the queue(3) manual page. + * + * + * SLIST LIST STAILQ TAILQ + * _HEAD + + + + + * _HEAD_INITIALIZER + + + + + * _ENTRY + + + + + * _INIT + + + + + * _EMPTY + + + + + * _FIRST + + + + + * _NEXT + + + + + * _PREV - - - + + * _LAST - - + + + * _FOREACH + + + + + * _FOREACH_SAFE + + + + + * _FOREACH_REVERSE - - - + + * _FOREACH_REVERSE_SAFE - - - + + * _INSERT_HEAD + + + + + * _INSERT_BEFORE - + - + + * _INSERT_AFTER + + + + + * _INSERT_TAIL - - + + + * _CONCAT - - + + + * _REMOVE_AFTER + - + - + * _REMOVE_HEAD + - + - + * _REMOVE + + + + + * + */ +#ifdef QUEUE_MACRO_DEBUG +/* Store the last 2 places the queue element or head was altered */ +struct qm_trace { + char * lastfile; + int lastline; + char * prevfile; + int prevline; +}; + +#define TRACEBUF struct qm_trace trace; +#define TRASHIT(x) do {(x) = (void *)-1;} while (0) +#define QMD_SAVELINK(name, link) void **name = (void *)&(link) + +#define QMD_TRACE_HEAD(head) do { \ + (head)->trace.prevline = (head)->trace.lastline; \ + (head)->trace.prevfile = (head)->trace.lastfile; \ + (head)->trace.lastline = __LINE__; \ + (head)->trace.lastfile = __FILE__; \ +} while (0) + +#define QMD_TRACE_ELEM(elem) do { \ + (elem)->trace.prevline = (elem)->trace.lastline; \ + (elem)->trace.prevfile = (elem)->trace.lastfile; \ + (elem)->trace.lastline = __LINE__; \ + (elem)->trace.lastfile = __FILE__; \ +} while (0) + +#else +#define QMD_TRACE_ELEM(elem) +#define QMD_TRACE_HEAD(head) +#define QMD_SAVELINK(name, link) +#define TRACEBUF +#define TRASHIT(x) +#endif /* QUEUE_MACRO_DEBUG */ + +/* + * Singly-linked List declarations. + */ +#define SLIST_HEAD(name, type) \ +struct name { \ + struct type *slh_first; /* first element */ \ +} + +#define SLIST_HEAD_INITIALIZER(head) \ + { NULL } + +#define SLIST_ENTRY(type) \ +struct { \ + struct type *sle_next; /* next element */ \ +} + +/* + * Singly-linked List functions. + */ +#define SLIST_EMPTY(head) ((head)->slh_first == NULL) + +#define SLIST_FIRST(head) ((head)->slh_first) + +#define SLIST_FOREACH(var, head, field) \ + for ((var) = SLIST_FIRST((head)); \ + (var); \ + (var) = SLIST_NEXT((var), field)) + +#define SLIST_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = SLIST_FIRST((head)); \ + (var) && ((tvar) = SLIST_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define SLIST_FOREACH_PREVPTR(var, varp, head, field) \ + for ((varp) = &SLIST_FIRST((head)); \ + ((var) = *(varp)) != NULL; \ + (varp) = &SLIST_NEXT((var), field)) + +#define SLIST_INIT(head) do { \ + SLIST_FIRST((head)) = NULL; \ +} while (0) + +#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \ + SLIST_NEXT((elm), field) = SLIST_NEXT((slistelm), field); \ + SLIST_NEXT((slistelm), field) = (elm); \ +} while (0) + +#define SLIST_INSERT_HEAD(head, elm, field) do { \ + SLIST_NEXT((elm), field) = SLIST_FIRST((head)); \ + SLIST_FIRST((head)) = (elm); \ +} while (0) + +#define SLIST_NEXT(elm, field) ((elm)->field.sle_next) + +#define SLIST_REMOVE(head, elm, type, field) do { \ + QMD_SAVELINK(oldnext, (elm)->field.sle_next); \ + if (SLIST_FIRST((head)) == (elm)) { \ + SLIST_REMOVE_HEAD((head), field); \ + } \ + else { \ + struct type *curelm = SLIST_FIRST((head)); \ + while (SLIST_NEXT(curelm, field) != (elm)) \ + curelm = SLIST_NEXT(curelm, field); \ + SLIST_REMOVE_AFTER(curelm, field); \ + } \ + TRASHIT(*oldnext); \ +} while (0) + +#define SLIST_REMOVE_AFTER(elm, field) do { \ + SLIST_NEXT(elm, field) = \ + SLIST_NEXT(SLIST_NEXT(elm, field), field); \ +} while (0) + +#define SLIST_REMOVE_HEAD(head, field) do { \ + SLIST_FIRST((head)) = SLIST_NEXT(SLIST_FIRST((head)), field); \ +} while (0) + +/* + * Singly-linked Tail queue declarations. + */ +#define STAILQ_HEAD(name, type) \ +struct name { \ + struct type *stqh_first;/* first element */ \ + struct type **stqh_last;/* addr of last next element */ \ +} + +#define STAILQ_HEAD_INITIALIZER(head) \ + { NULL, &(head).stqh_first } + +#define STAILQ_ENTRY(type) \ +struct { \ + struct type *stqe_next; /* next element */ \ +} + +/* + * Singly-linked Tail queue functions. + */ +#define STAILQ_CONCAT(head1, head2) do { \ + if (!STAILQ_EMPTY((head2))) { \ + *(head1)->stqh_last = (head2)->stqh_first; \ + (head1)->stqh_last = (head2)->stqh_last; \ + STAILQ_INIT((head2)); \ + } \ +} while (0) + +#define STAILQ_EMPTY(head) ((head)->stqh_first == NULL) + +#define STAILQ_FIRST(head) ((head)->stqh_first) + +#define STAILQ_FOREACH(var, head, field) \ + for((var) = STAILQ_FIRST((head)); \ + (var); \ + (var) = STAILQ_NEXT((var), field)) + + +#define STAILQ_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = STAILQ_FIRST((head)); \ + (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define STAILQ_INIT(head) do { \ + STAILQ_FIRST((head)) = NULL; \ + (head)->stqh_last = &STAILQ_FIRST((head)); \ +} while (0) + +#define STAILQ_INSERT_AFTER(head, tqelm, elm, field) do { \ + if ((STAILQ_NEXT((elm), field) = STAILQ_NEXT((tqelm), field)) == NULL)\ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ + STAILQ_NEXT((tqelm), field) = (elm); \ +} while (0) + +#define STAILQ_INSERT_HEAD(head, elm, field) do { \ + if ((STAILQ_NEXT((elm), field) = STAILQ_FIRST((head))) == NULL) \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ + STAILQ_FIRST((head)) = (elm); \ +} while (0) + +#define STAILQ_INSERT_TAIL(head, elm, field) do { \ + STAILQ_NEXT((elm), field) = NULL; \ + *(head)->stqh_last = (elm); \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ +} while (0) + +#define STAILQ_LAST(head, type, field) \ + (STAILQ_EMPTY((head)) ? \ + NULL : \ + ((struct type *)(void *) \ + ((char *)((head)->stqh_last) - __offsetof(struct type, field)))) + +#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next) + +#define STAILQ_REMOVE(head, elm, type, field) do { \ + QMD_SAVELINK(oldnext, (elm)->field.stqe_next); \ + if (STAILQ_FIRST((head)) == (elm)) { \ + STAILQ_REMOVE_HEAD((head), field); \ + } \ + else { \ + struct type *curelm = STAILQ_FIRST((head)); \ + while (STAILQ_NEXT(curelm, field) != (elm)) \ + curelm = STAILQ_NEXT(curelm, field); \ + STAILQ_REMOVE_AFTER(head, curelm, field); \ + } \ + TRASHIT(*oldnext); \ +} while (0) + +#define STAILQ_REMOVE_HEAD(head, field) do { \ + if ((STAILQ_FIRST((head)) = \ + STAILQ_NEXT(STAILQ_FIRST((head)), field)) == NULL) \ + (head)->stqh_last = &STAILQ_FIRST((head)); \ +} while (0) + +#define STAILQ_REMOVE_AFTER(head, elm, field) do { \ + if ((STAILQ_NEXT(elm, field) = \ + STAILQ_NEXT(STAILQ_NEXT(elm, field), field)) == NULL) \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ +} while (0) + +#define STAILQ_SWAP(head1, head2, type) do { \ + struct type *swap_first = STAILQ_FIRST(head1); \ + struct type **swap_last = (head1)->stqh_last; \ + STAILQ_FIRST(head1) = STAILQ_FIRST(head2); \ + (head1)->stqh_last = (head2)->stqh_last; \ + STAILQ_FIRST(head2) = swap_first; \ + (head2)->stqh_last = swap_last; \ + if (STAILQ_EMPTY(head1)) \ + (head1)->stqh_last = &STAILQ_FIRST(head1); \ + if (STAILQ_EMPTY(head2)) \ + (head2)->stqh_last = &STAILQ_FIRST(head2); \ +} while (0) + +#define STAILQ_INSERT_CHAIN_HEAD(head, elm_chead, elm_ctail, field) do { \ + if ((STAILQ_NEXT(elm_ctail, field) = STAILQ_FIRST(head)) == NULL ) { \ + (head)->stqh_last = &STAILQ_NEXT(elm_ctail, field); \ + } \ + STAILQ_FIRST(head) = (elm_chead); \ +} while (0) + + +/* + * List declarations. + */ +#define LIST_HEAD(name, type) \ +struct name { \ + struct type *lh_first; /* first element */ \ +} + +#define LIST_HEAD_INITIALIZER(head) \ + { NULL } + +#define LIST_ENTRY(type) \ +struct { \ + struct type *le_next; /* next element */ \ + struct type **le_prev; /* address of previous next element */ \ +} + +/* + * List functions. + */ + +#if (defined(_KERNEL) && defined(INVARIANTS)) +#define QMD_LIST_CHECK_HEAD(head, field) do { \ + if (LIST_FIRST((head)) != NULL && \ + LIST_FIRST((head))->field.le_prev != \ + &LIST_FIRST((head))) \ + panic("Bad list head %p first->prev != head", (head)); \ +} while (0) + +#define QMD_LIST_CHECK_NEXT(elm, field) do { \ + if (LIST_NEXT((elm), field) != NULL && \ + LIST_NEXT((elm), field)->field.le_prev != \ + &((elm)->field.le_next)) \ + panic("Bad link elm %p next->prev != elm", (elm)); \ +} while (0) + +#define QMD_LIST_CHECK_PREV(elm, field) do { \ + if (*(elm)->field.le_prev != (elm)) \ + panic("Bad link elm %p prev->next != elm", (elm)); \ +} while (0) +#else +#define QMD_LIST_CHECK_HEAD(head, field) +#define QMD_LIST_CHECK_NEXT(elm, field) +#define QMD_LIST_CHECK_PREV(elm, field) +#endif /* (_KERNEL && INVARIANTS) */ + +#define LIST_EMPTY(head) ((head)->lh_first == NULL) + +#define LIST_FIRST(head) ((head)->lh_first) + +#define LIST_FOREACH(var, head, field) \ + for ((var) = LIST_FIRST((head)); \ + (var); \ + (var) = LIST_NEXT((var), field)) + +#define LIST_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = LIST_FIRST((head)); \ + (var) && ((tvar) = LIST_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define LIST_INIT(head) do { \ + LIST_FIRST((head)) = NULL; \ +} while (0) + +#define LIST_INSERT_AFTER(listelm, elm, field) do { \ + QMD_LIST_CHECK_NEXT(listelm, field); \ + if ((LIST_NEXT((elm), field) = LIST_NEXT((listelm), field)) != NULL)\ + LIST_NEXT((listelm), field)->field.le_prev = \ + &LIST_NEXT((elm), field); \ + LIST_NEXT((listelm), field) = (elm); \ + (elm)->field.le_prev = &LIST_NEXT((listelm), field); \ +} while (0) + +#define LIST_INSERT_BEFORE(listelm, elm, field) do { \ + QMD_LIST_CHECK_PREV(listelm, field); \ + (elm)->field.le_prev = (listelm)->field.le_prev; \ + LIST_NEXT((elm), field) = (listelm); \ + *(listelm)->field.le_prev = (elm); \ + (listelm)->field.le_prev = &LIST_NEXT((elm), field); \ +} while (0) + +#define LIST_INSERT_HEAD(head, elm, field) do { \ + QMD_LIST_CHECK_HEAD((head), field); \ + if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \ + LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\ + LIST_FIRST((head)) = (elm); \ + (elm)->field.le_prev = &LIST_FIRST((head)); \ +} while (0) + +#define LIST_NEXT(elm, field) ((elm)->field.le_next) + +#define LIST_REMOVE(elm, field) do { \ + QMD_SAVELINK(oldnext, (elm)->field.le_next); \ + QMD_SAVELINK(oldprev, (elm)->field.le_prev); \ + QMD_LIST_CHECK_NEXT(elm, field); \ + QMD_LIST_CHECK_PREV(elm, field); \ + if (LIST_NEXT((elm), field) != NULL) \ + LIST_NEXT((elm), field)->field.le_prev = \ + (elm)->field.le_prev; \ + *(elm)->field.le_prev = LIST_NEXT((elm), field); \ + TRASHIT(*oldnext); \ + TRASHIT(*oldprev); \ +} while (0) + +#define LIST_SWAP(head1, head2, type, field) do { \ + struct type *swap_tmp = LIST_FIRST((head1)); \ + LIST_FIRST((head1)) = LIST_FIRST((head2)); \ + LIST_FIRST((head2)) = swap_tmp; \ + if ((swap_tmp = LIST_FIRST((head1))) != NULL) \ + swap_tmp->field.le_prev = &LIST_FIRST((head1)); \ + if ((swap_tmp = LIST_FIRST((head2))) != NULL) \ + swap_tmp->field.le_prev = &LIST_FIRST((head2)); \ +} while (0) + +/* + * Tail queue declarations. + */ +#define TAILQ_HEAD(name, type) \ +struct name { \ + struct type *tqh_first; /* first element */ \ + struct type **tqh_last; /* addr of last next element */ \ + TRACEBUF \ +} + +#define TAILQ_HEAD_INITIALIZER(head) \ + { NULL, &(head).tqh_first } + +#define TAILQ_ENTRY(type) \ +struct { \ + struct type *tqe_next; /* next element */ \ + struct type **tqe_prev; /* address of previous next element */ \ + TRACEBUF \ +} + +/* + * Tail queue functions. + */ +#if (defined(_KERNEL) && defined(INVARIANTS)) +#define QMD_TAILQ_CHECK_HEAD(head, field) do { \ + if (!TAILQ_EMPTY(head) && \ + TAILQ_FIRST((head))->field.tqe_prev != \ + &TAILQ_FIRST((head))) \ + panic("Bad tailq head %p first->prev != head", (head)); \ +} while (0) + +#define QMD_TAILQ_CHECK_TAIL(head, field) do { \ + if (*(head)->tqh_last != NULL) \ + panic("Bad tailq NEXT(%p->tqh_last) != NULL", (head)); \ +} while (0) + +#define QMD_TAILQ_CHECK_NEXT(elm, field) do { \ + if (TAILQ_NEXT((elm), field) != NULL && \ + TAILQ_NEXT((elm), field)->field.tqe_prev != \ + &((elm)->field.tqe_next)) \ + panic("Bad link elm %p next->prev != elm", (elm)); \ +} while (0) + +#define QMD_TAILQ_CHECK_PREV(elm, field) do { \ + if (*(elm)->field.tqe_prev != (elm)) \ + panic("Bad link elm %p prev->next != elm", (elm)); \ +} while (0) +#else +#define QMD_TAILQ_CHECK_HEAD(head, field) +#define QMD_TAILQ_CHECK_TAIL(head, headname) +#define QMD_TAILQ_CHECK_NEXT(elm, field) +#define QMD_TAILQ_CHECK_PREV(elm, field) +#endif /* (_KERNEL && INVARIANTS) */ + +#define TAILQ_CONCAT(head1, head2, field) do { \ + if (!TAILQ_EMPTY(head2)) { \ + *(head1)->tqh_last = (head2)->tqh_first; \ + (head2)->tqh_first->field.tqe_prev = (head1)->tqh_last; \ + (head1)->tqh_last = (head2)->tqh_last; \ + TAILQ_INIT((head2)); \ + QMD_TRACE_HEAD(head1); \ + QMD_TRACE_HEAD(head2); \ + } \ +} while (0) + +#define TAILQ_EMPTY(head) ((head)->tqh_first == NULL) + +#define TAILQ_FIRST(head) ((head)->tqh_first) + +#define TAILQ_FOREACH(var, head, field) \ + for ((var) = TAILQ_FIRST((head)); \ + (var); \ + (var) = TAILQ_NEXT((var), field)) + +#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = TAILQ_FIRST((head)); \ + (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \ + for ((var) = TAILQ_LAST((head), headname); \ + (var); \ + (var) = TAILQ_PREV((var), headname, field)) + +#define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \ + for ((var) = TAILQ_LAST((head), headname); \ + (var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \ + (var) = (tvar)) + +#define TAILQ_INIT(head) do { \ + TAILQ_FIRST((head)) = NULL; \ + (head)->tqh_last = &TAILQ_FIRST((head)); \ + QMD_TRACE_HEAD(head); \ +} while (0) + +#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \ + QMD_TAILQ_CHECK_NEXT(listelm, field); \ + if ((TAILQ_NEXT((elm), field) = TAILQ_NEXT((listelm), field)) != NULL)\ + TAILQ_NEXT((elm), field)->field.tqe_prev = \ + &TAILQ_NEXT((elm), field); \ + else { \ + (head)->tqh_last = &TAILQ_NEXT((elm), field); \ + QMD_TRACE_HEAD(head); \ + } \ + TAILQ_NEXT((listelm), field) = (elm); \ + (elm)->field.tqe_prev = &TAILQ_NEXT((listelm), field); \ + QMD_TRACE_ELEM(&(elm)->field); \ + QMD_TRACE_ELEM(&listelm->field); \ +} while (0) + +#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \ + QMD_TAILQ_CHECK_PREV(listelm, field); \ + (elm)->field.tqe_prev = (listelm)->field.tqe_prev; \ + TAILQ_NEXT((elm), field) = (listelm); \ + *(listelm)->field.tqe_prev = (elm); \ + (listelm)->field.tqe_prev = &TAILQ_NEXT((elm), field); \ + QMD_TRACE_ELEM(&(elm)->field); \ + QMD_TRACE_ELEM(&listelm->field); \ +} while (0) + +#define TAILQ_INSERT_HEAD(head, elm, field) do { \ + QMD_TAILQ_CHECK_HEAD(head, field); \ + if ((TAILQ_NEXT((elm), field) = TAILQ_FIRST((head))) != NULL) \ + TAILQ_FIRST((head))->field.tqe_prev = \ + &TAILQ_NEXT((elm), field); \ + else \ + (head)->tqh_last = &TAILQ_NEXT((elm), field); \ + TAILQ_FIRST((head)) = (elm); \ + (elm)->field.tqe_prev = &TAILQ_FIRST((head)); \ + QMD_TRACE_HEAD(head); \ + QMD_TRACE_ELEM(&(elm)->field); \ +} while (0) + +#define TAILQ_INSERT_TAIL(head, elm, field) do { \ + QMD_TAILQ_CHECK_TAIL(head, field); \ + TAILQ_NEXT((elm), field) = NULL; \ + (elm)->field.tqe_prev = (head)->tqh_last; \ + *(head)->tqh_last = (elm); \ + (head)->tqh_last = &TAILQ_NEXT((elm), field); \ + QMD_TRACE_HEAD(head); \ + QMD_TRACE_ELEM(&(elm)->field); \ +} while (0) + +#define TAILQ_LAST(head, headname) \ + (*(((struct headname *)((head)->tqh_last))->tqh_last)) + +#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next) + +#define TAILQ_PREV(elm, headname, field) \ + (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last)) + +#define TAILQ_REMOVE(head, elm, field) do { \ + QMD_SAVELINK(oldnext, (elm)->field.tqe_next); \ + QMD_SAVELINK(oldprev, (elm)->field.tqe_prev); \ + QMD_TAILQ_CHECK_NEXT(elm, field); \ + QMD_TAILQ_CHECK_PREV(elm, field); \ + if ((TAILQ_NEXT((elm), field)) != NULL) \ + TAILQ_NEXT((elm), field)->field.tqe_prev = \ + (elm)->field.tqe_prev; \ + else { \ + (head)->tqh_last = (elm)->field.tqe_prev; \ + QMD_TRACE_HEAD(head); \ + } \ + *(elm)->field.tqe_prev = TAILQ_NEXT((elm), field); \ + TRASHIT(*oldnext); \ + TRASHIT(*oldprev); \ + QMD_TRACE_ELEM(&(elm)->field); \ +} while (0) + +#define TAILQ_SWAP(head1, head2, type, field) do { \ + struct type *swap_first = (head1)->tqh_first; \ + struct type **swap_last = (head1)->tqh_last; \ + (head1)->tqh_first = (head2)->tqh_first; \ + (head1)->tqh_last = (head2)->tqh_last; \ + (head2)->tqh_first = swap_first; \ + (head2)->tqh_last = swap_last; \ + if ((swap_first = (head1)->tqh_first) != NULL) \ + swap_first->field.tqe_prev = &(head1)->tqh_first; \ + else \ + (head1)->tqh_last = &(head1)->tqh_first; \ + if ((swap_first = (head2)->tqh_first) != NULL) \ + swap_first->field.tqe_prev = &(head2)->tqh_first; \ + else \ + (head2)->tqh_last = &(head2)->tqh_first; \ +} while (0) + +#ifdef __cplusplus +} +#endif + +#endif /* !_SYS_QUEUE_H_ */ diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..841f614 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,46 @@ +idf_component_register(SRCS + app_main.c + settings.c + shutdown_handlers.c + sntp_cli.c + utils.c + wifi_conn.c + console/console_ioimpl.c + console/console_server.c + console/register_cmds.c + console/telnet_parser.c + web/websrv.c + console/commands/cmd_dump.c + console/commands/cmd_factory_reset.c + console/commands/cmd_heap.c + console/commands/cmd_ip.c + console/commands/cmd_restart.c + console/commands/cmd_tasks.c + console/commands/cmd_version.c + console/commands/cmd_wifi.c + console/commands/cmd_pw.c + INCLUDE_DIRS ".") + +find_package(Git REQUIRED) + +execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE _commit_hash +) +# TODO what's this? +execute_process( + COMMAND ${GIT_EXECUTABLE} rev-list --count HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE _revision_number +) +string(REGEX REPLACE "\n" "" _commit_hash "${_commit_hash}") +string(REGEX REPLACE "\n" "" _revision_number "${_revision_number}") +string(TIMESTAMP _build_time_stamp) + +configure_file( + "gitversion.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/config/gitversion.h" +) + +include_directories("${CMAKE_CURRENT_BINARY_DIR}/config") diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..3800750 --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,23 @@ +menu "IRBLASTER Configuration" + + menu "Pin mapping" + config PIN_I2C_SDA0 + int "I2C0 SDA" + default 18 + + config PIN_I2C_SCL0 + int "I2C0 SCL" + default 19 + endmenu + + menu "Console" + config CONSOLE_TELNET_PORT + int "Integrated telnet server listening port" + default 23 + + config CONSOLE_PW_LEN + int "Console max pw len" + default 32 + endmenu + +endmenu diff --git a/main/app_main.c b/main/app_main.c new file mode 100644 index 0000000..8a8d96d --- /dev/null +++ b/main/app_main.c @@ -0,0 +1,62 @@ +#include + +#include +#include + +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_system.h" +#include "nvs_flash.h" + +#include "driver/adc.h" +#include "application.h" +#include "settings.h" + +#include "console/console_server.h" + +#include "web/websrv.h" +#include "wifi_conn.h" + +#include "console/register_cmds.h" + +static const char *TAG = "main"; + +void app_main(void) { + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_register_shutdown_handler(cspemu_run_shutdown_handlers)); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + settings_init(); + settings_load(); + + // Start IDF service for pin change interrupts + ESP_ERROR_CHECK(gpio_install_isr_service(0)); + + ESP_LOGD(TAG, "initing netif"); + ESP_ERROR_CHECK(esp_netif_init()); + if (g_Settings.wifi_enabled && (g_Settings.sta_enabled || g_Settings.ap_enabled)) { + initialise_wifi(); + + websrv_init(); + g_State.wifi_inited = true; + } else { + // initialise the bare minimum so wifi config can be changed + ESP_LOGD(TAG, "initing wifi"); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_LOGD(TAG, "set storage, set sta"); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_FLASH)); + g_State.wifi_inited = true; + } + + console_init(NULL); + register_console_commands(); + + console_setup_uart_stdio(); + ESP_ERROR_CHECK(console_start_stdio(NULL, NULL)); + + telnetsrv_start(CONSOLE_TELNET_PORT); + ESP_LOGI(TAG, "Startup finished, free heap = %u, cmds %"PRIu32, esp_get_free_heap_size(), console_count_commands()); +} diff --git a/main/application.h b/main/application.h new file mode 100644 index 0000000..da55f40 --- /dev/null +++ b/main/application.h @@ -0,0 +1,26 @@ +/** + * Globals + */ + +#ifndef _APPLICATION_H +#define _APPLICATION_H + +#include +#include +#include "sdkconfig.h" +#include "settings.h" +#include "gitversion.h" + +#define EG_WIFI_CONNECTED_BIT BIT0 +extern EventGroupHandle_t g_wifi_event_group; + +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +esp_err_t cspemu_add_shutdown_handler(shutdown_handler_t handler); +void cspemu_run_shutdown_handlers(void); + +#define APP_NAME "APP" +#define APP_VERSION "v1" + +#endif //_APPLICATION_H diff --git a/main/component.mk b/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/main/console/cmd_common.h b/main/console/cmd_common.h new file mode 100644 index 0000000..b067790 --- /dev/null +++ b/main/console/cmd_common.h @@ -0,0 +1,48 @@ +// +// Created by MightyPork on 2018/12/02. +// + +#ifndef CSPEMU_CMD_COMMON_H +#define CSPEMU_CMD_COMMON_H + +#include "console_server.h" + +//// prototypes (TODO remove..) +//void csp_vprintf(const char *format, va_list args); +//void csp_printf(const char *format, ...); + +#define EOL "\r\n" + +#define console_printf_e(format, ...) console_printf("\x1b[31m" format "\x1b[m", ##__VA_ARGS__) +#define console_printf_w(format, ...) console_printf("\x1b[33m" format "\x1b[m", ##__VA_ARGS__) +#define console_fputs(str) console_print(str) +#define console_fputsn(str, len) console_write(str, len) +#define console_fputc(c) console_write(&c, 1) + +#define MSG_ON "\x1b[32mON\x1b[m" +#define MSG_ENABLED "\x1b[32mENABLED\x1b[m" +#define MSG_OFF "\x1b[31mOFF\x1b[m" +#define MSG_DISABLED "\x1b[31mDISABLED\x1b[m" + +//#define ARG_OPTIONAL_NODE_ID() cmd_args.node->count ? cmd_args.node->ival[0] : csp_get_address() + +#if 0 +struct cmd_no_args_s { + struct arg_end *end; +}; + +extern struct cmd_no_args_s cmd_no_args; + +#define CMD_CHECK_ARGS(struct_var) do { \ + int nerrors = arg_parse(argc, argv, (void**) &struct_var); \ + if (nerrors != 0) { \ + console_print_errors(struct_var.end, argv[0]); \ + return 1; \ + } \ +} while(0) + +/** Report & return error if any args were given to this command */ +#define CMD_CHECK_NO_ARGS() CMD_CHECK_ARGS(cmd_no_args) +#endif + +#endif //CSPEMU_CMD_COMMON_H diff --git a/main/console/commands/cmd_dump.c b/main/console/commands/cmd_dump.c new file mode 100644 index 0000000..0370666 --- /dev/null +++ b/main/console/commands/cmd_dump.c @@ -0,0 +1,34 @@ +// +// Created by MightyPork on 2018/12/08. +// +#include "settings.h" +#include "console/cmd_common.h" +#include + + +static int cmd_dump(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.end = arg_end(1); + + reg->argtable = &cmd_args; + reg->command = "dump"; + reg->help = "Dump node info"; + return 0; + } + + console_printf("WiFi enabled: %d\r\n", g_Settings.wifi_enabled); + + // TODO show more settings + + return 0; +} + +void register_cmd_dump(void) +{ + console_cmd_register(cmd_dump, "dump"); +} diff --git a/main/console/commands/cmd_factory_reset.c b/main/console/commands/cmd_factory_reset.c new file mode 100644 index 0000000..e844960 --- /dev/null +++ b/main/console/commands/cmd_factory_reset.c @@ -0,0 +1,50 @@ +// +// Created by MightyPork on 2018/12/08. +// + +#include +#include +#include +#include +#include + +#include "console/cmd_common.h" +#include + + +static int cmd_factory_reset(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_str *magic; + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.magic = arg_str1(NULL, NULL, "", "Passphrase to prevent accidental erase. Must be 'confirm'"); + cmd_args.end = arg_end(2); + + reg->argtable = &cmd_args; + reg->command = "factory_reset"; + reg->help = "Wipe non-volatile data memory, restoring everything to defaults."; + return 0; + } + + if (streq(cmd_args.magic->sval[0], "confirm")) { + nvs_flash_erase(); + + console_printf("Non-volatile memory erased.\r\n" + "Restarting to apply changes...\r\n\r\n"); + vTaskDelay(pdMS_TO_TICKS(500)); + esp_restart(); + } else { + console_printf("Incorrect passphrase.\r\n"); + return 1; + } + + return 0; +} + +void register_cmd_factory_reset(void) +{ + console_cmd_register(cmd_factory_reset, "factory_reset"); +} diff --git a/main/console/commands/cmd_heap.c b/main/console/commands/cmd_heap.c new file mode 100644 index 0000000..e84588a --- /dev/null +++ b/main/console/commands/cmd_heap.c @@ -0,0 +1,38 @@ +// +// Created by MightyPork on 2018/12/08. +// + +#include +#include + +#include "console/cmd_common.h" +#include + + +/* 'heap' command prints minumum heap size */ +static int cmd_heap(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.end = arg_end(1); + + reg->argtable = &cmd_args; + reg->command = "heap"; + reg->help = "Get emulator heap usage stats (for debug)"; + return 0; + } + + uint32_t heap_size = heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT); + uint32_t current_free = esp_get_free_heap_size(); + console_printf("free heap: %u bytes\r\n" + " lowest: %u bytes\r\n", current_free, heap_size); + return 0; +} + +void register_cmd_heap(void) +{ + console_cmd_register(cmd_heap, "heap"); +} diff --git a/main/console/commands/cmd_ip.c b/main/console/commands/cmd_ip.c new file mode 100644 index 0000000..43bf88d --- /dev/null +++ b/main/console/commands/cmd_ip.c @@ -0,0 +1,341 @@ +#include +#include +#include +#include +#include + +#include "console/cmd_common.h" +#include +#include +#include +#include +#include +#include + +static int cmd_ip_status(console_ctx_t *ctx, cmd_signature_t *reg) +{ + EMPTY_CMD_SETUP("Show IP status"); + + if (!g_Settings.wifi_enabled || !g_State.wifi_inited) { + console_color_printf(COLOR_RED, "WiFi interface is disabled\n"); + return 0; + } + + wifi_config_t config; + if (ESP_OK == esp_wifi_get_config(ESP_IF_WIFI_STA, &config)) { + if (config.sta.ssid[0]) { + EventBits_t bits = xEventGroupGetBits(g_wifi_event_group); + if (bits & WIFI_CONNECTED_BIT) { + console_color_printf(COLOR_GREEN, "Connected to SSID \"%s\"\n", config.sta.ssid); + } else if (bits & WIFI_FAIL_BIT) { + console_color_printf(COLOR_RED, "WiFi connection failed.\n"); + console_printf("Saved SSID = \"%s\"\n", config.sta.ssid); + return 0; + } else { + console_color_printf(COLOR_RED, "Not connected, retries may be in progress.\n"); + console_printf("Saved SSID = \"%s\"\n", config.sta.ssid); + return 0; + } + + tcpip_adapter_ip_info_t ipinfo; + if (ESP_OK == tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ipinfo)) { + // ! inet_ntoa uses a global static buffer, cant use multiple in one printf call + console_printf("DHCP: %s\n", g_Settings.dhcp_enable ? MSG_ENABLED : MSG_DISABLED " (static IP)"); + console_printf("IP: %s\n", inet_ntoa(ipinfo.ip.addr)); + console_printf("Mask: %s\n", inet_ntoa(ipinfo.netmask.addr)); + console_printf("Gateway: %s\n", inet_ntoa(ipinfo.gw.addr)); + + if (!g_Settings.dhcp_enable) { + console_printf("DNS: %s\n", inet_ntoa(g_Settings.static_dns)); + } + + uint8_t mac[6]; + if (ESP_OK == esp_wifi_get_mac(ESP_IF_WIFI_STA, mac)) { + console_printf("MAC: %02x:%02x:%02x:%02x:%02x:%02x\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } + } else { + console_color_printf(COLOR_RED, "No IP address assigned!\n"); + } + } + else { + console_color_printf(COLOR_RED, "SSID not configured!\n"); + } + } + + return 0; +} + +static int cmd_ip_pingwd(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_str *cmd; + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.cmd = arg_str0(NULL, NULL, "", "Enable or disable. Omit to check current state"); + cmd_args.end = arg_end(1); + + reg->argtable = &cmd_args; + reg->help = "Enable/disable ping watchdog, or check the current setting. The watchdog periodically pings the gateway and restarts WiFi on failure."; + return 0; + } + + if (cmd_args.cmd->count) { + int b = parse_boolean_arg(cmd_args.cmd->sval[0]); + if (b < 0) return CONSOLE_ERR_INVALID_ARG; + g_Settings.dhcp_wd_enable = b; + settings_persist(SETTINGS_dhcp_wd_enable); + } + + console_printf("Ping WD = %s\n", g_Settings.dhcp_wd_enable? MSG_ENABLED : MSG_DISABLED); + + if (cmd_args.cmd->count) { + console_printf("Restart to apply changes\n"); + } + + return 0; +} + +static void ping_success_print_cb(int bytes, const char *ip, int seq, int elapsed_ms) { + console_printf("Rx %d bytes from %s: icmp_seq=%d time=%d ms\n", bytes, ip, seq, (int) elapsed_ms); +} + +static void ping_fail_print_cb(int seq) { + console_printf("Request timeout for icmp_seq %d\n", seq); +} + +static int cmd_ip_ping(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_str *ip; + struct arg_int *num; + struct arg_int *timeout; + struct arg_int *interval; + struct arg_int *size; + struct arg_end *end; + } args; + + if (reg) { + args.ip = arg_str1(NULL, NULL, "", "Target IP"); + args.num = arg_int0("n", NULL, "", "Ping count (def 1)"); + args.timeout = arg_int0("t", NULL, "", "Timeout (def 3000)"); + args.interval = arg_int0("i", NULL, "", "Interval (def 1000)"); + args.size = arg_int0("s", NULL, "", "Payload size (def 32)"); + args.end = arg_end(5); + + reg->argtable = &args; + reg->help = "Ping a host using ICMP"; + return 0; + } + + ping_opts_t opts = PING_CONFIG_DEFAULT(); + opts.success_cb = ping_success_print_cb; + opts.fail_cb = ping_fail_print_cb; + opts.count = args.num->count ? args.num->ival[0] : 1; + opts.payload_size = args.size->count ? args.size->ival[0] : 32; + opts.interval_ms = args.interval->count ? args.interval->ival[0] : 1000; + opts.timeout_ms = args.timeout->count ? args.timeout->ival[0] : 3000; + + if (0 == inet_aton(args.ip->sval[0], &opts.ip_addr)) { + // we could have received a domain name here + struct hostent * ent = gethostbyname(args.ip->sval[0]); + if (!ent) { + console_println("Could not resolve"); + return CONSOLE_ERR_IO; + } + + memcpy(&opts.ip_addr, ent->h_addr_list[0], sizeof(ip4_addr_t)); + console_printf("Resolved as %s\n", inet_ntoa(opts.ip_addr)); + } + + ping_result_t result = {}; + esp_err_t ret = ping(&opts, &result); + + if (ret != ESP_OK) { + console_println("Ping error"); + return ret; + } else { + console_printf("%d tx, %d rx, %.1f%% loss, latency min %d ms, max %d ms\n", + result.sent, + result.received, + result.loss_pt, + result.min_time_ms, + result.max_time_ms); + } + + return 0; +} + +static int cmd_ip_static_set(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_str *ip; + struct arg_str *gw; + struct arg_str *mask; + struct arg_str *dns; + struct arg_str *cmd; + struct arg_end *end; + } args; + + if (reg) { + args.ip = arg_str0("a", NULL, "", "Set IP address"); + args.gw = arg_str0("g", NULL, "", "Set gateway address"); + args.mask = arg_str0("n", NULL, "", "Set netmask"); + args.dns = arg_str0("d", NULL, "", "Set DNS server IP"); + args.cmd = arg_str0(NULL, NULL, "{enable|disable}", "Enable or disable static IP"); + args.end = arg_end(1); + + reg->argtable = &args; + reg->help = "Configure, enable/disable, or check config of static IP."; + return 0; + } + + bool any_change = false; + + if (args.ip->count) { + uint32_t a = 0; + if (!inet_aton(args.ip->sval[0], &a)) { + console_println("Invalid IP"); + return CONSOLE_ERR_INVALID_ARG; + } + g_Settings.static_ip = a; // aton output is already in network byte order + settings_persist(SETTINGS_static_ip); + any_change = true; + } + + if (args.gw->count) { + uint32_t a = 0; + if (!inet_aton(args.gw->sval[0], &a)) { + console_println("Invalid GW"); + return CONSOLE_ERR_INVALID_ARG; + } + g_Settings.static_ip_gw = a; // aton output is already in network byte order + settings_persist(SETTINGS_static_ip_gw); + any_change = true; + } + + if (args.mask->count) { + uint32_t a = 0; + if (!inet_aton(args.mask->sval[0], &a)) { + console_println("Invalid mask"); + return CONSOLE_ERR_INVALID_ARG; + } + g_Settings.static_ip_mask = a; // aton output is already in network byte order + settings_persist(SETTINGS_static_ip_mask); + any_change = true; + } + + if (args.dns->count) { + uint32_t a = 0; + if (!inet_aton(args.dns->sval[0], &a)) { + console_println("Invalid DNS IP"); + return CONSOLE_ERR_INVALID_ARG; + } + g_Settings.static_dns = a; // aton output is already in network byte order + settings_persist(SETTINGS_static_dns); + any_change = true; + } + + if (args.cmd->count) { + int b = parse_boolean_arg(args.cmd->sval[0]); + if (b < 0) return CONSOLE_ERR_INVALID_ARG; + g_Settings.dhcp_enable = !b; + settings_persist(SETTINGS_dhcp_enable); + any_change = true; + } + + console_printf("Static IP: %s\n", g_Settings.dhcp_enable ? MSG_DISABLED : MSG_ENABLED); + console_printf("- IP: %s\n", inet_ntoa(g_Settings.static_ip)); + console_printf("- Mask: %s\n", inet_ntoa(g_Settings.static_ip_mask)); + console_printf("- Gateway: %s\n", inet_ntoa(g_Settings.static_ip_gw)); + console_printf("- DNS: %s\n", inet_ntoa(g_Settings.static_dns)); + + if (any_change) { + console_println("Restart to apply changes."); + } + + return 0; +} + +static int cmd_ip_ntp(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_str *cmd; + struct arg_str *addr; + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.cmd = arg_str0(NULL, NULL, "", "Enable or disable autostart. Omit to check current state. start = start now"); + cmd_args.addr = arg_str0("s", NULL, "", "Set NTP server"); + cmd_args.end = arg_end(2); + + reg->argtable = &cmd_args; + reg->help = "Check or modify NTP client setting, or start it manually."; + return 0; + } + + if (cmd_args.addr->count) { + strncpy(g_Settings.ntp_srv, cmd_args.addr->sval[0], NTP_SRV_LEN); + g_Settings.ntp_srv[NTP_SRV_LEN-1] = 0; + settings_persist(SETTINGS_ntp_srv); + console_printf("NTP server set to %s\n", g_Settings.ntp_srv); + } + + if (cmd_args.cmd->count) { + if (streq(cmd_args.cmd->sval[0], "start")) { + bool started = sntp_cli_start(); + if (started) { + console_printf("NTP client started manually.\n"); + } else { + console_color_printf(COLOR_RED, "Start failed. Client may be already running.\n"); + } + return 0; + } + + int b = parse_boolean_arg(cmd_args.cmd->sval[0]); + if (b < 0) return CONSOLE_ERR_INVALID_ARG; + g_Settings.ntp_enable = b; + + settings_persist(SETTINGS_ntp_enable); + } + + console_printf("Client status: %s\n", g_Settings.ntp_enable? MSG_ENABLED : MSG_DISABLED); + console_printf("NTP server: %s\n", g_Settings.ntp_srv); + + /* show the current date */ + time_t now; + struct tm timeinfo; + time(&now); + localtime_r(&now, &timeinfo); + + if(timeinfo.tm_year < (2016 - 1900)) { + console_printf("Device time is not valid.\n"); + } else { + char strftime_buf[64]; + strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo); + console_printf("The current UTC date/time is: %s\n", strftime_buf); + } + + if (cmd_args.cmd->count) { + // if it was "start", we returned early. + console_printf("Restart to apply changes\n"); + } + + return 0; +} + +void register_cmd_ip(void) +{ + console_group_add("ip", "IP status and settings"); + console_cmd_register(cmd_ip_status, "ip status"); + console_cmd_register(cmd_ip_pingwd, "ip wd"); + console_cmd_register(cmd_ip_ntp, "ip ntp"); + console_cmd_register(cmd_ip_ping, "ip ping"); + console_cmd_register(cmd_ip_static_set, "ip static"); + + // this may be used for shortcuts like "ip in" + console_cmd_add_alias_fn(cmd_ip_status, "ip info"); +} diff --git a/main/console/commands/cmd_pw.c b/main/console/commands/cmd_pw.c new file mode 100644 index 0000000..0876b82 --- /dev/null +++ b/main/console/commands/cmd_pw.c @@ -0,0 +1,52 @@ +// +// Set telnet pw +// + +#include +#include +#include +#include + +#include "console/cmd_common.h" +#include +#include +#include + +static int cmd_clear(console_ctx_t *ctx, cmd_signature_t *reg) +{ + EMPTY_CMD_SETUP("Clear access password"); + console_printf("Access password cleared.\n"); + g_Settings.console_pw[0] = 0; + settings_persist(SETTINGS_console_pw); + return 0; +} + +static int cmd_set(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_str *pw; + struct arg_end *end; + } args; + + if (reg) { + args.pw = arg_str1(NULL, NULL, "", EXPENDABLE_STRING("New password")); + args.end = arg_end(1); + + reg->argtable = &args; + reg->help = EXPENDABLE_STRING("Set access password"); + return CONSOLE_OK; + } + + strncpy(g_Settings.console_pw, args.pw->sval[0], CONSOLE_PW_LEN); + console_printf("Access pw set to: \"%.*s\"\n", CONSOLE_PW_LEN, args.pw->sval[0]); + settings_persist(SETTINGS_console_pw); + return 0; +} + +void register_cmd_pw(void) +{ + console_group_add("pw", "Access password"); + + console_cmd_register(cmd_set, "pw set"); + console_cmd_register(cmd_clear, "pw clear"); +} diff --git a/main/console/commands/cmd_restart.c b/main/console/commands/cmd_restart.c new file mode 100644 index 0000000..a5364d0 --- /dev/null +++ b/main/console/commands/cmd_restart.c @@ -0,0 +1,43 @@ +// +// Created by MightyPork on 2018/12/08. +// + +#include "console/cmd_common.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "application.h" +#include + + +/** 'restart' command restarts the program */ +static int cmd_restart(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.end = arg_end(1); + + reg->argtable = &cmd_args; + reg->command = "restart"; + reg->help = "Restart the emulator"; + return 0; + } + + console_printf("Restarting...\r\n"); + + // try to cleanly close all connections + telnetsrv_kick_all(); + vTaskDelay(pdMS_TO_TICKS(100)); + + esp_restart(); +} + +void register_cmd_restart(void) +{ + console_cmd_register(cmd_restart, "restart"); +} + diff --git a/main/console/commands/cmd_tasks.c b/main/console/commands/cmd_tasks.c new file mode 100644 index 0000000..b789f5c --- /dev/null +++ b/main/console/commands/cmd_tasks.c @@ -0,0 +1,51 @@ +// +// Created by MightyPork on 2018/12/08. +// + +#include + +#include +#include + +#include "console/cmd_common.h" +#include + + +static const char* TAG = "cmd_tasks"; + +static int cmd_tasks_info(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.end = arg_end(1); + + reg->argtable = &cmd_args; + reg->command = "ps"; + reg->help = "List running emulator tasks (for debug)"; + return 0; + } + + const size_t bytes_per_task = 40; /* see vTaskList description */ + char *task_list_buffer = calloc(uxTaskGetNumberOfTasks() * bytes_per_task, 1); + if (task_list_buffer == NULL) { + ESP_LOGE(TAG, "failed to allocate buffer for vTaskList output"); + return 1; + } + console_fputs("\x1b[1mTask Name\tStatus\tPrio\tHWM\tTask#"); +#ifdef CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID + console_fputs("\tAffinity"); +#endif + console_fputs("\x1b[m\r\n"); + vTaskList(task_list_buffer); + console_fputs(task_list_buffer); + free(task_list_buffer); + return 0; +} + +void register_cmd_tasks() +{ + console_cmd_register(cmd_tasks_info, "ps"); +} diff --git a/main/console/commands/cmd_version.c b/main/console/commands/cmd_version.c new file mode 100644 index 0000000..97511f9 --- /dev/null +++ b/main/console/commands/cmd_version.c @@ -0,0 +1,52 @@ +// +// Created by MightyPork on 2018/12/08. +// + +#include "console/cmd_common.h" + +#include +#include + +#include "application.h" +#include + + +/** 'version' command */ +static int cmd_version(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.end = arg_end(1); + + reg->argtable = &cmd_args; + reg->command = "version"; + reg->help = "Get version of the chip, SDK, and firmware."; + return 0; + } + + esp_chip_info_t info; + esp_chip_info(&info); + console_printf("IDF Version: %s\n", esp_get_idf_version()); + console_printf("Firmware: %s\n", APP_VERSION GIT_COUNT); + console_printf(" git: %s\n", GIT_HASH); + console_printf(" builded: %s\n", BUILD_TIMESTAMP); + console_printf("Chip model: %s\n", info.model == CHIP_ESP32 ? "ESP32" : "Unknow"); + console_printf(" cores: %d\n", info.cores); + console_printf(" feature: %s%s%s%s%d%s\n", + info.features & CHIP_FEATURE_WIFI_BGN ? "/802.11bgn" : "", + info.features & CHIP_FEATURE_BLE ? "/BLE" : "", + info.features & CHIP_FEATURE_BT ? "/BT" : "", + info.features & CHIP_FEATURE_EMB_FLASH ? "/Embedded-Flash:" : "/External-Flash:", + spi_flash_get_chip_size() / (1024 * 1024), "MB"); + console_printf("rev.number: %d\n", info.revision); + + return 0; +} + +void register_cmd_version(void) +{ + console_cmd_register(cmd_version, "version"); +} diff --git a/main/console/commands/cmd_wifi.c b/main/console/commands/cmd_wifi.c new file mode 100644 index 0000000..3b3d5f8 --- /dev/null +++ b/main/console/commands/cmd_wifi.c @@ -0,0 +1,360 @@ +// +// Created by MightyPork on 2018/12/08. +// + +#include +#include +#include +#include + +#include "console/cmd_common.h" +#include +#include +#include + + +static int cmd_disable(console_ctx_t *ctx, cmd_signature_t *reg) +{ + EMPTY_CMD_SETUP("Disable WiFi"); + console_printf("WiFi "MSG_DISABLED"\nRestart to apply.\n"); + g_Settings.wifi_enabled = false; + settings_persist(SETTINGS_wifi_enabled); + return 0; +} + +static int cmd_enable(console_ctx_t *ctx, cmd_signature_t *reg) +{ + EMPTY_CMD_SETUP("Enable WiFi"); + console_printf("WiFi "MSG_ENABLED"\nRestart to apply.\n"); + g_Settings.wifi_enabled = true; + settings_persist(SETTINGS_wifi_enabled); + return 0; +} + +static const char *en_dis_cmds[] = { + [0] = "disable", + [1] = "enable", + NULL +}; + +static int cmd_ap_conf(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_str *cmd; + struct arg_str *ssid; + struct arg_str *pw; + struct arg_str *ip; + struct arg_end *end; + } args; + + if (reg) { + args.cmd = arg_str0(NULL, NULL, "{enable|disable}", EXPENDABLE_STRING("Command")); + args.ssid = arg_str0("s", NULL, "", EXPENDABLE_STRING("Set AP SSID")); + args.pw = arg_str0("p", NULL, "", EXPENDABLE_STRING("Set AP WPA2 password. Empty for open.")); + args.ip = arg_str0("a", NULL, "", "Set IP address (server + gateway). Always /24"); + args.end = arg_end(4); + + reg->argtable = &args; + reg->help = EXPENDABLE_STRING("Configure WiFi AP mode"); + return CONSOLE_OK; + } + + if (!g_State.wifi_inited) { + console_printf("\x1b[31;1mWiFi is disabled!\x1b[22m\nEnable with `wifi enable`, restart to apply.\x1b[m\n"); + return 0; + } + + if (args.cmd->count) { + int match = prefix_match(args.cmd->sval[0], en_dis_cmds, 0); + + switch (match) { + case 0: + console_printf("AP mode "MSG_DISABLED"\nRestart to apply.\n"); + g_Settings.ap_enabled = false; + settings_persist(SETTINGS_ap_enabled); + break; + + case 1: + console_printf("AP mode "MSG_ENABLED"\nRestart to apply.\n"); + g_Settings.ap_enabled = true; + settings_persist(SETTINGS_ap_enabled); + break; + + default: + return CONSOLE_ERR_INVALID_ARG; + } + } else { + // No cmd + console_printf("AP mode: %s\n", g_Settings.ap_enabled? MSG_ENABLED: MSG_DISABLED); + } + + if (args.ip->count) { + uint32_t a = 0; + if (!inet_aton(args.ip->sval[0], &a)) { + console_println("Invalid IP"); + return CONSOLE_ERR_INVALID_ARG; + } + g_Settings.ap_ip = a; // aton output is already in network byte order + settings_persist(SETTINGS_ap_ip); + + console_println("AP IP changed, restart to apply.\n"); + } + + bool changed = false; + wifi_config_t apconf = {}; + ESP_ERROR_CHECK(esp_wifi_get_config(ESP_IF_WIFI_AP, &apconf)); + + if (args.ssid->count) { + //apconf.ap.authmode = WIFI_AUTH_OPEN; + strcpy((char*)apconf.ap.ssid, args.ssid->sval[0]); + apconf.ap.ssid_len = strlen(args.ssid->sval[0]); + changed = true; + } + + if (args.pw->count) { + size_t len = strlen(args.pw->sval[0]); + if (len < 8 && len != 0) { + console_println("AP pw must be 8 chars or more!"); + return CONSOLE_ERR_INVALID_ARG; + } + + strcpy((char*)apconf.ap.password, args.pw->sval[0]); + if (len == 0) { + // if no pw is set, the AP will be open + apconf.ap.authmode = WIFI_AUTH_OPEN; + } else { + apconf.ap.authmode = WIFI_AUTH_WPA2_PSK; + } + changed = true; + } + + if (changed) { + esp_err_t rv = esp_wifi_set_config(ESP_IF_WIFI_AP, &apconf); + if (rv != ESP_OK) { + console_printf("Error set config: %s\n", esp_err_to_name(rv)); + return -1; + } + } + + console_printf("AP SSID: \"%s\"\n", apconf.ap.ssid); + console_printf("AP PW: \"%s\"\n", apconf.ap.password); + console_printf("AP own IP: %s/24\n", inet_ntoa(g_Settings.ap_ip)); + return 0; +} + +static int cmd_sta_conf(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_str *cmd; + struct arg_end *end; + } args; + + if (reg) { + args.cmd = arg_str0(NULL, NULL, "{enable|disable}", EXPENDABLE_STRING("Command")); + args.end = arg_end(1); + + reg->argtable = &args; + reg->help = EXPENDABLE_STRING("Configure WiFi STA mode"); + return CONSOLE_OK; + } + + if (!g_State.wifi_inited) { + console_printf("\x1b[31;1mWiFi is disabled!\x1b[22m\nEnable with `wifi enable`, restart to apply.\x1b[m\n"); + return 0; + } + + if (args.cmd->count) { + int match = prefix_match(args.cmd->sval[0], en_dis_cmds, 0); + + switch (match) { + case 0: + console_printf("STA mode "MSG_DISABLED"\nRestart to apply.\n"); + g_Settings.sta_enabled = false; + settings_persist(SETTINGS_sta_enabled); + break; + + case 1: + console_printf("STA mode "MSG_ENABLED"\nRestart to apply.\n"); + g_Settings.sta_enabled = true; + settings_persist(SETTINGS_sta_enabled); + break; + + default: + return CONSOLE_ERR_INVALID_ARG; + } + } else { + // No cmd + console_printf("STA mode: %s\n", g_Settings.sta_enabled? MSG_ENABLED: MSG_DISABLED); + } + + return 0; +} + +/** Disconnect from WiFi and forget creds */ +static int cmd_sta_forget(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_end *end; + } args; + + if (reg) { + args.end = arg_end(1); + + reg->argtable = &args; + reg->command = "wifi forget"; + reg->help = "Disconnect from WiFi AP and erase stored credentials"; + return 0; + } + + console_printf("Removing saved WiFi credentials and disconnecting.\n"); + + if (!g_State.wifi_inited) { + console_printf("\x1b[31;1mWiFi is disabled!\x1b[22m\nEnable with `wifi enable`, restart to apply.\x1b[m\n"); + return 0; + } + + wifi_config_t wificonf; + esp_wifi_get_config(WIFI_IF_STA, &wificonf); + wificonf.sta.ssid[0] = 0; + wificonf.sta.password[0] = 0; + esp_wifi_set_config(WIFI_IF_STA, &wificonf); + esp_wifi_disconnect(); + + return 0; +} + +#define DEF_WIFI_TIMEOUT 10000 + +static bool wifi_join(const char* ssid, const char* pass, int timeout_ms) +{ + wifi_config_t wifi_config = {}; + strncpy((char*) wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid)); + if (pass) { + strncpy((char*) wifi_config.sta.password, pass, sizeof(wifi_config.sta.password)); + } + + ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) ); + ESP_ERROR_CHECK( esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) ); + + xEventGroupClearBits(g_wifi_event_group, WIFI_CONNECTED_BIT|WIFI_FAIL_BIT); + + ESP_ERROR_CHECK( esp_wifi_disconnect() ); + ESP_ERROR_CHECK( esp_wifi_connect() ); + + int bits = xEventGroupWaitBits(g_wifi_event_group, WIFI_CONNECTED_BIT|WIFI_FAIL_BIT, + /* clear */ 0, /* wait for all */0, pdMS_TO_TICKS(timeout_ms)); + + return (bits & EG_WIFI_CONNECTED_BIT) != 0; +} + +static int cmd_join(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_int *timeout; + struct arg_str *ssid; + struct arg_str *password; + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.timeout = arg_int0("t", "timeout", "", "Connection timeout, ms"); + cmd_args.ssid = arg_str1(NULL, NULL, "", "SSID of AP"); + cmd_args.password = arg_str0(NULL, NULL, "", "PSK of AP"); + cmd_args.end = arg_end(2); + + reg->argtable = &cmd_args; + reg->command = "wifi join"; + reg->help = "Join WiFi AP as a station"; + return 0; + } + + if (!g_State.wifi_inited) { + console_printf("\x1b[31;1mWiFi is disabled!\x1b[22m\n" + "Enable with `wifi enable`, restart to apply.\x1b[m\n"); + return 0; + } + + console_printf("Connecting to '%s'\n", cmd_args.ssid->sval[0]); + + int tmeo = cmd_args.timeout->count ? cmd_args.timeout->ival[0] : DEF_WIFI_TIMEOUT; + + bool connected = wifi_join(cmd_args.ssid->sval[0], + cmd_args.password->sval[0], + tmeo); + if (!connected) { + console_printf("Connection timed out\n"); + + // erase config + wifi_config_t wifi_config = {}; + ESP_ERROR_CHECK( esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) ); + + return 1; + } + console_printf("Connected\n"); + return 0; +} + +static int cmd_wifi_status(console_ctx_t *ctx, cmd_signature_t *reg) +{ + static struct { + struct arg_end *end; + } cmd_args; + + if (reg) { + cmd_args.end = arg_end(1); + + reg->argtable = &cmd_args; + reg->command = "wifi status"; + reg->help = "Check WiFi / IP status"; + return 0; + } + + console_printf("WiFi support: %s\n", g_Settings.wifi_enabled ? MSG_ENABLED : MSG_DISABLED); + console_printf("STA mode: %s\n", g_Settings.sta_enabled? MSG_ENABLED: MSG_DISABLED); + console_printf("AP mode: %s\n", g_Settings.ap_enabled? MSG_ENABLED: MSG_DISABLED); + + console_printf("\n"); + + if (g_Settings.wifi_enabled) { + wifi_config_t config; + if (ESP_OK == esp_wifi_get_config(ESP_IF_WIFI_STA, &config)) { + if (config.sta.ssid[0]) { + console_printf("Configured to connect to SSID \"%s\".\n" + "Use `wifi forget` to drop saved credentials.\n\n", + config.sta.ssid); + + tcpip_adapter_ip_info_t ipinfo; + if (ESP_OK == tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ipinfo)) { + // ! inet_ntoa uses a global static buffer, cant use multiple in one printf call + console_printf("IP: %s, ", inet_ntoa(ipinfo.ip.addr)); + console_printf("Mask: %s, ", inet_ntoa(ipinfo.netmask.addr)); + console_printf("Gateway: %s\n", inet_ntoa(ipinfo.gw.addr)); + } else { + console_printf("No IP!\n"); + } + } + else { + console_printf("No network SSID configured.\n" + "Use `wifi join` to connect to a WiFi network.\n"); + } + } + } + + return 0; +} + +void register_cmd_wifi(void) +{ + console_group_add("wifi", "WiFi configuration"); + + console_cmd_register(cmd_enable, "wifi enable"); + console_cmd_register(cmd_disable, "wifi disable"); + + console_cmd_register(cmd_sta_conf, "wifi sta"); + console_cmd_register(cmd_sta_forget, "wifi forget"); + console_cmd_register(cmd_join, "wifi join"); + + console_cmd_register(cmd_ap_conf, "wifi ap"); + + console_cmd_register(cmd_wifi_status, "wifi status"); +} diff --git a/main/console/console_ioimpl.c b/main/console/console_ioimpl.c new file mode 100644 index 0000000..9acef49 --- /dev/null +++ b/main/console/console_ioimpl.c @@ -0,0 +1,418 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include +#include +#include +#include +#include +#include "console_ioimpl.h" +#include "tasks.h" +#include "telnet_parser.h" +#include "cmd_common.h" + +static const char *TAG = "console-io"; + +void console_internal_error_print(const char *msg) { + ESP_LOGE(TAG, "CONSOLE ERR: %s", msg); +} + +void console_setup_uart_stdio(void) +{ + assert(CONFIG_ESP_CONSOLE_UART_NUM == UART_NUM_0); + + /* Minicom, screen, idf_monitor send CR when ENTER key is pressed */ + esp_vfs_dev_uart_port_set_rx_line_endings(UART_NUM_0, ESP_LINE_ENDINGS_CR); + /* Move the caret to the beginning of the next line on '\n' */ + esp_vfs_dev_uart_port_set_tx_line_endings(UART_NUM_0, ESP_LINE_ENDINGS_CRLF); // this is the default anyway + + /* Disable buffering on stdin and stdout */ + setvbuf(stdin, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); + +// fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); // make input non-blocking + +#if 0 + /* Configure UART. Note that REF_TICK is used so that the baud rate remains + * correct while APB frequency is changing in light sleep mode. + */ + const uart_config_t uart_config = { + .baud_rate = CONFIG_CONSOLE_UART_BAUDRATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .use_ref_tick = true + }; + ESP_ERROR_CHECK( uart_param_config(CONFIG_CONSOLE_UART_NUM, &uart_config) ); +#endif + + /* Install UART driver for interrupt-driven reads and writes */ + ESP_ERROR_CHECK(uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, + /* rxbuf */ 256, /* txbuf */ 0, /* que */ 0, /* uart que */ NULL, /* alloc flags */ 0)); + + uart_flush(CONFIG_ESP_CONSOLE_UART_NUM); + + /* Tell VFS to use UART driver */ + esp_vfs_dev_uart_use_driver(CONFIG_ESP_CONSOLE_UART_NUM); +} + +static void my_console_task_freertos(void *param) { + console_ctx_t *ctx = param; + assert(CONSOLE_CTX_MAGIC == ctx->__internal_magic); // just make sure it's OK + + vTaskDelay(pdMS_TO_TICKS(50)); // ?? + + bool logged_in = true; + { + struct console_ioimpl *io = ctx->ioctx; + assert(CONSOLE_IOIMPL_MAGIC == io->__magic); + + if (io->kind == CONSOLE_IO_TELNET) { + const size_t pwlen = strnlen(g_Settings.console_pw, CONSOLE_PW_LEN); + if (pwlen != 0) { + ESP_LOGE(TAG, "Pw=\"%.*s\"", pwlen, g_Settings.console_pw); + + console_print_ctx(&io->ctx, "Password: "); + + // Make the prompt fancy with asterisks and stuff. Backspace is not supported. + int pos = 0; + char buf[CONSOLE_PW_LEN] = {/*zeros*/}; + while (1) { + char ch = 0; + console_read_ctx(ctx, &ch, 1); + if (ch == 10 || ch == 13) { + console_write_ctx(ctx, "\n", 1); + break; + } + if (ch >= 32 && ch <= 126) { + if (pos < CONSOLE_PW_LEN) { + buf[pos++] = ch; + } + console_write_ctx(ctx, "*", 1); + } + } + + if (0 == strncmp(buf, g_Settings.console_pw, CONSOLE_PW_LEN)) { + console_print_ctx(&io->ctx, "Login OK!\n"); + } else { + console_print_ctx(&io->ctx, "Login failed!\n"); + logged_in = false; + if (io->telnet.tcpcli) { + tcpd_kick(io->telnet.tcpcli); + } + } + } + } + } + + if (logged_in) { + console_print_motd(ctx); + console_task(param); + } + + ESP_LOGD(TAG, "Console task ended"); + + // This delay should ensure the TCP client is shut down completely + // before we proceed to free stuff. The delay is deliberately very generous, + // we are in no rush here. + vTaskDelay(pdMS_TO_TICKS(2000)); + + // Deallocate what console allocated inside ctx, ctx is part of ioimpl so it will NOT be freed + ESP_LOGD(TAG, "Clear console context"); + console_ctx_destroy(ctx); + + ESP_LOGD(TAG, "Clear IO context"); + struct console_ioimpl *io = ctx->ioctx; + assert(CONSOLE_IOIMPL_MAGIC == io->__magic); + + // Tear down IO + if (io->kind == CONSOLE_IO_TELNET) { + vRingbufferDelete(io->telnet.console_stdin_ringbuf); + } + + // Free ioimpl + free(io); + + ESP_LOGI(TAG, "Console task shutdown."); + // suicide the task + vTaskDelete(NULL); +} + +static void telnet_shutdown_handler(console_ctx_t *ctx) { + assert(ctx); + assert(CONSOLE_CTX_MAGIC == ctx->__internal_magic); // just make sure it's OK + struct console_ioimpl *io = ctx->ioctx; + assert(io); + assert(CONSOLE_IOIMPL_MAGIC == io->__magic); + + tcpd_kick(io->telnet.tcpcli); +} + +/** + * Start console working with stdin and stdout + */ +static esp_err_t console_start_io(struct console_ioimpl **pIo, TaskHandle_t * hdl, enum console_iokind kind, TcpdClient_t client) { + struct console_ioimpl * io = calloc(1, sizeof(struct console_ioimpl)); + if (io == NULL) { + return ESP_ERR_NO_MEM; + } + if (pIo != NULL) { + *pIo = io; + } + + io->__magic = CONSOLE_IOIMPL_MAGIC; + + io->kind = kind; + + if (kind == CONSOLE_IO_TELNET) { + io->telnet.console_stdin_ringbuf = xRingbufferCreate(CONSOLE_BUFSIZE, RINGBUF_TYPE_BYTEBUF); + if (NULL == io->telnet.console_stdin_ringbuf) { + ESP_LOGE(TAG, "Failed to create RB!"); + free(io); + if (pIo != NULL) { + *pIo = NULL; + } + return ESP_ERR_NO_MEM; + } + io->telnet.tcpcli = client; + + // Store the "io" reference as "cctx" in the TCP client, so we know + // where to write received data in the callback + tcpd_set_client_ctx(client, io); + + } else { + io->files.inf = stdin; + io->files.outf = stdout; + } + + // using "static" allocation - context is part of the io struct + // Here "io" is stored as "ioctx" in "ctx" and then passed to the read/write functions + if (NULL == console_ctx_init(&io->ctx, io)) { + ESP_LOGE(TAG, "Console init failed!"); + goto fail; + } + + if (kind == CONSOLE_IO_FILES) { + io->ctx.exit_allowed = false; + } else { + /* TELNET */ + io->ctx.shutdown_handler = telnet_shutdown_handler; + } + + assert(io->ctx.__internal_magic == CONSOLE_CTX_MAGIC); + + snprintf(io->ctx.prompt, CONSOLE_PROMPT_MAX_LEN, "\x1b[36;1m> \x1b[m"); + + if (pdPASS != xTaskCreate( + my_console_task_freertos, // func + "console", // name + CONSOLE_TASK_STACK, // stack + &io->ctx, // param + CONSOLE_TASK_PRIO, // prio + hdl // handle dest + )) { + ESP_LOGE(TAG, "Err create console task!"); + goto fail; + } + + return ESP_OK; + +fail: + ESP_LOGE(TAG, "console_start_io FAILED, freeing resources!"); + console_ctx_destroy(&io->ctx); + + if (kind == CONSOLE_IO_TELNET) { + vRingbufferDelete(io->telnet.console_stdin_ringbuf); + io->telnet.console_stdin_ringbuf = NULL; + } + + free(io); + if (pIo != NULL) { + *pIo = NULL; + } + return ESP_FAIL; +} + +esp_err_t console_start_stdio(struct console_ioimpl **pIo, TaskHandle_t * hdl) { + ESP_LOGI(TAG, "Start STDIO console task"); + + return console_start_io(pIo, hdl, CONSOLE_IO_FILES, NULL); +} + +esp_err_t console_start_tcp(struct console_ioimpl **pIo, TaskHandle_t * hdl, TcpdClient_t client) { + ESP_LOGI(TAG, "Start TCP console task"); + + return console_start_io(pIo, hdl, CONSOLE_IO_TELNET, client); +} + +/** + * Write to console context. + * + * Return number of characters written, -1 on error. + */ +int console_write_ctx(console_ctx_t *ctx, const char *text, size_t len) { + struct console_ioimpl *io = ctx->ioctx; + assert(CONSOLE_IOIMPL_MAGIC == io->__magic); + + if (io->kind == CONSOLE_IO_TELNET) { + const char *wp = (const char *)text; + const char *sp = (const char *)text; + int towrite = 0; + + // hack to allow using bare \n + for (size_t i = 0; i < len; i++) { + char c = *sp++; + if (c == '\n') { + // LF: print the chunk before it and a CR. + if (towrite > 0) { + if (ESP_OK != tcpd_send(io->telnet.tcpcli, (uint8_t *) wp, towrite)) { + return -1; + } + } + if (ESP_OK != tcpd_send(io->telnet.tcpcli, (uint8_t*) "\r", 1)) { + return -1; + } + // The LF gets rolled into the next chunk. + wp = sp - 1; + towrite = 1; + } else { + // Non-LF character is printed as is + towrite++; + } + } + + // Send the leftovers (chars from last LF or from the start) + if (towrite > 0) { + if (ESP_OK != tcpd_send(io->telnet.tcpcli, (uint8_t*) wp, towrite)) { + return -1; + } + } + + return len; + } else { + // File IO + errno = 0; + // the UART driver takes care of encoding \n as \r\n + size_t n = fwrite(text, 1, len, io->files.outf); + if (n != len) { + if (errno || ferror(io->files.outf)) { + return -1; + } + } + return n; + } +} + +/** + * Read from console context's input stream. + * + * Return number of characters read, -1 on error + */ +int console_read_ctx(console_ctx_t *ctx, char *dest, size_t count) { + if (!console_have_stdin_ctx(ctx)) { + ESP_LOGW(TAG, "Console stream has no stdin!"); + return -1; + } + + struct console_ioimpl *io = ctx->ioctx; + assert(CONSOLE_IOIMPL_MAGIC == io->__magic); + + if (io->kind == CONSOLE_IO_TELNET) { + size_t remain = count; + char *wp = dest; + do { + size_t rcount = 0; + uint8_t *chunk = xRingbufferReceiveUpTo(io->telnet.console_stdin_ringbuf, &rcount, portMAX_DELAY, remain); + + // telnet options negotiation + rcount = telnet_middleware_read(ctx, chunk, rcount); + + if (rcount > 0) { + memcpy(wp, chunk, rcount); + wp += rcount; + remain -= rcount; + } + + vRingbufferReturnItem(io->telnet.console_stdin_ringbuf, chunk); + } while (remain > 0); + + return count; + } else { + // File IO + errno = 0; +// clearerr(io->files.inf); + size_t r = fread(dest, 1, count, io->files.inf); + + if (errno != 0 || feof(io->files.inf) /*|| ferror(io->files.inf)*/) { + ESP_LOGW(TAG, "Console stream EOF or error."); + perror("Read err"); + return -1; + } + + return r; + } +} + +/** + * Check if console input stream has bytes ready. + * + * @return number of queued bytes, 0 if none, -1 on error. + */ +int console_can_read_ctx(console_ctx_t *ctx) { + if (!console_have_stdin_ctx(ctx)) { + ESP_LOGW(TAG, "Console stream has no stdin!"); + return -1; + } + + struct console_ioimpl *io = ctx->ioctx; + assert(CONSOLE_IOIMPL_MAGIC == io->__magic); + + if (io->kind == CONSOLE_IO_TELNET) { + uint32_t nitems = 0; + vRingbufferGetInfo(io->telnet.console_stdin_ringbuf, NULL, NULL, NULL, NULL, &nitems); + return nitems; + } else { + // File IO + + // a hack with select - this is used rarely, we can afford the overhead + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(fileno(io->files.inf), &readfds); + struct timeval timeout = {0, 0}; + int sel_rv = select(1, &readfds, NULL, NULL, &timeout); + if (sel_rv > 0) { + return 1; // at least one + } else if (sel_rv == -1) { + ESP_LOGW(TAG, "Console stream EOF or error."); + return -1; // error + } else { + return 0; // nothing + } + } +} + +/** + * Test if console context is not NULL and has stdin stream available + * + * @return have stdin + */ +bool console_have_stdin_ctx(console_ctx_t *ctx) { + if (!ctx->ioctx) return false; + struct console_ioimpl *io = ctx->ioctx; + assert(CONSOLE_IOIMPL_MAGIC == io->__magic); + + if (io->kind == CONSOLE_IO_TELNET) { + return io->telnet.tcpcli != NULL && + io->telnet.console_stdin_ringbuf != NULL; + } else { + // File IO +// ESP_LOGW(TAG, "%p, %d, %d", io->files.inf, +// feof(io->files.inf), +// ferror(io->files.inf)); + + return io->files.inf != NULL && + !feof(io->files.inf); + } +} diff --git a/main/console/console_ioimpl.h b/main/console/console_ioimpl.h new file mode 100644 index 0000000..32e5fd7 --- /dev/null +++ b/main/console/console_ioimpl.h @@ -0,0 +1,77 @@ +/** + * TODO file description + * + * Created on 2020/04/09. + */ + +#ifndef CSPEMU_CONSOLE_IOIMPL_H +#define CSPEMU_CONSOLE_IOIMPL_H + +#include +#include +#include +#include + +/** Console ring buffer capacity */ +#define CONSOLE_BUFSIZE 512 + +/** Enum for tagging ioimpl kind */ +enum console_iokind { + CONSOLE_IO_FILES, + CONSOLE_IO_TELNET, +}; + +#define CONSOLE_IOIMPL_MAGIC 0x079fbf72 + +struct console_ioimpl { + /** This is a tag for the following union */ + enum console_iokind kind; + union { + /** STDIO variant data */ + struct { + /** STDIN */ + FILE *inf; + /** STDOUT */ + FILE *outf; + } files; + + /** Telnet variant data */ + struct { + /** TCP client handle */ + TcpdClient_t tcpcli; + /** Received data ring buffer */ + RingbufHandle_t console_stdin_ringbuf; + } telnet; + }; + + /** Console context */ + console_ctx_t ctx; + uint32_t __magic; +}; + +/** + * Setup UART IO for console + */ +void console_setup_uart_stdio(void); + +/** + * Start console for stdin/stdout (UART) + * + * @param pIo - pointer where the allocated struct will be stored + * @param hdl - handle to the created task, can be NULL if not used + * @return success + */ +esp_err_t console_start_stdio(struct console_ioimpl **pIo, TaskHandle_t * hdl); + +/** + * Start console for a TCP client + * + * @param pIo - pointer where the allocated struct will be stored + * @param hdl - handle to the created task, can be NULL if not used + * @param client - TCP server client handle + * @return success + */ +esp_err_t console_start_tcp(struct console_ioimpl **pIo, TaskHandle_t * hdl, TcpdClient_t client); + + +#endif //CSPEMU_CONSOLE_IOIMPL_H diff --git a/main/console/console_server.c b/main/console/console_server.c new file mode 100644 index 0000000..61f227e --- /dev/null +++ b/main/console/console_server.c @@ -0,0 +1,155 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include +#include + +#include "socket_server.h" +#include "console_server.h" +#include "application.h" +#include "tasks.h" +#include "telnet_parser.h" +#include "console_ioimpl.h" + +static const char *TAG = "console_srv"; + +Tcpd_t g_telnet_server = NULL; +TcpdClient_t telnetsrv_last_rx_client = NULL; + +/** + * Send a textual message to all the connected peers + * + * @param interface + * @param packet + * @param timeout + * @return + */ +esp_err_t telnetsrv_send(TcpdClient_t client, const char *message, ssize_t len) +{ + if (!g_telnet_server) { + ESP_LOGE(TAG, "server not inited"); + return ESP_FAIL; + } + + if (len < 0) len = strlen(message); + + if (client) { + tcpd_send(client, (const uint8_t *) message, len); + } else { + tcpd_broadcast(g_telnet_server, (const uint8_t *) message, len); + } + + return ESP_OK; +} + +/** + * Handle received bytes + * + * @param client + * @param buf + * @param nbytes + */ +static void telnetsrv_handle(TcpdClient_t client, char *buf, int nbytes) +{ + void *ctx = tcpd_get_client_ctx(client); + struct console_ioimpl *io = ctx; + + if (!ctx) { + ESP_LOGE(TAG, "telnet rx with no ioctx!"); + return; + } + + assert(CONSOLE_IOIMPL_MAGIC == io->__magic); + + int rv = xRingbufferSend(io->telnet.console_stdin_ringbuf, buf, (size_t) nbytes, pdMS_TO_TICKS(100)); + if (rv != pdPASS) { + ESP_LOGE(TAG, "console ringbuf overflow"); + } +} + +/** + * Socket read handler + */ +esp_err_t read_fn(Tcpd_t serv, TcpdClient_t client, int sockfd) +{ + char buf[64]; + int nbytes = read(sockfd, buf, sizeof(buf)); + if (nbytes <= 0) return ESP_FAIL; + +// ESP_LOGI(TAG, "Rx %d bytes", nbytes); + telnetsrv_handle(client, buf, nbytes); + + return ESP_OK; +} + +static void print_motd(console_ctx_t *ctx) +{ + console_printf_ctx(ctx, COLOR_RESET, "\n" + "===================================================\n" + " ESP32 node "APP_NAME" "APP_VERSION " #" GIT_HASH "\n" + " Built " BUILD_TIMESTAMP "\n" + "\n" + " Run `ls` for a list of commands.\n" + "===================================================\n\n" + ); + // show the initial prompt + console_print(ctx->prompt); +} + +/** + * Socket open handler - send a MOTD + */ +static esp_err_t open_fn(Tcpd_t serv, TcpdClient_t client) +{ + vTaskDelay(pdMS_TO_TICKS(100)); + + struct console_ioimpl *io = NULL; + esp_err_t rv = console_start_tcp(&io, NULL, client); + if (rv != ESP_OK) { + return rv; + } + assert(io); + + // set telnet params, unless this is the injected stdin client + if (tcpd_get_client_fd(client) != STDIN_FILENO) { + telnet_send_will(&io->ctx, OPT_SUPPRESS_GO_AHEAD); + telnet_send_will(&io->ctx, OPT_ECHO); + telnet_send_dont(&io->ctx, OPT_ECHO); + } + + return ESP_OK; +} + +void console_print_motd(console_ctx_t *ctx) +{ + print_motd(ctx); +} + +esp_err_t telnetsrv_start(uint16_t port) +{ + tcpd_config_t server_config = TCPD_INIT_DEFAULT(); + server_config.max_clients = 3; + server_config.task_prio = TELNET_TASK_PRIO; + server_config.task_stack = TELNET_TASK_STACK; + server_config.task_name = "TelnetSrv"; + server_config.port = port; + server_config.read_fn = read_fn; + server_config.open_fn = open_fn; + // close fn is not needed, console shuts down if the FD becomes invalid (read fails) + + ESP_ERROR_CHECK(tcpd_init(&server_config, &g_telnet_server)); + + // this deadlocks now... XXX +// cspemu_add_shutdown_handler(telnetsrv_kick_all); + + return ESP_OK; +} + +void telnetsrv_kick_all(void) +{ + ESP_LOGI(TAG, "Kick all telnet clients"); + tcpd_kick_all(g_telnet_server, false); // don't kick injected clients + ESP_LOGI(TAG, "Kicking done!"); +} diff --git a/main/console/console_server.h b/main/console/console_server.h new file mode 100644 index 0000000..06f333f --- /dev/null +++ b/main/console/console_server.h @@ -0,0 +1,29 @@ +/** + * TCP debug server. + * + * Broadcasts TCP debug messages and provides a simple console interface + * to modify settings. + */ + +#ifndef CSPEMU_TCPDBG_SERVER_H +#define CSPEMU_TCPDBG_SERVER_H + +#include +#include +#include "esp_err.h" +#include "socket_server.h" + +extern Tcpd_t g_telnet_server; + +esp_err_t telnetsrv_start(uint16_t port); + +esp_err_t telnetsrv_send(TcpdClient_t client, const char *message, ssize_t len); + +void telnetsrv_kick_all(void); + +/** + * Broadcast the welcome message + */ +void console_print_motd(console_ctx_t *ctx); + +#endif //CSPEMU_TCPDBG_SERVER_H diff --git a/main/console/register_cmds.c b/main/console/register_cmds.c new file mode 100644 index 0000000..77e8e1b --- /dev/null +++ b/main/console/register_cmds.c @@ -0,0 +1,29 @@ +#include "register_cmds.h" +#include + +static const char *TAG = "reg_cmds"; + +extern void register_cmd_wifi(); +extern void register_cmd_tasks(); +extern void register_cmd_version(); +extern void register_cmd_heap(); +extern void register_cmd_dump(); +extern void register_cmd_restart(); +extern void register_cmd_factory_reset(); +extern void register_cmd_ip(); +extern void register_cmd_pw(void); + +void register_console_commands() +{ + console_group_add("wifi", "WiFi control"); + register_cmd_wifi(); + + register_cmd_tasks(); + register_cmd_version(); + register_cmd_heap(); + //register_cmd_dump(); + register_cmd_restart(); + register_cmd_factory_reset(); + register_cmd_ip(); + register_cmd_pw(); +} diff --git a/main/console/register_cmds.h b/main/console/register_cmds.h new file mode 100644 index 0000000..39f7247 --- /dev/null +++ b/main/console/register_cmds.h @@ -0,0 +1,6 @@ +#ifndef REGISTER_CMDS_H +#define REGISTER_CMDS_H + +void register_console_commands(void); + +#endif // REGISTER_CMDS_H diff --git a/main/console/telnet_parser.c b/main/console/telnet_parser.c new file mode 100644 index 0000000..1d621e3 --- /dev/null +++ b/main/console/telnet_parser.c @@ -0,0 +1,539 @@ +//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG + +#include +#include +#include +#include +#include +#include +#include +#include +#include "telnet_parser.h" +#include "console_server.h" + +static const char *TAG = "telnet"; + +// The console is all single-threaded, using a circular buffer, so we can get away +// with a global state here + +enum telnet_code { + CODE_SE = 240, + CODE_NOP = 241, + CODE_DATA_MARK = 242, + CODE_BRK = 243, // break signal - kick all clients (including yourself) + CODE_IP = 244, // interrupt process (reboots ESP) + CODE_AO = 245, // abort output + CODE_AYT = 246, // are you there? (sends an ASCII text reply) + CODE_ERASE_CHAR = 247, // these are obsolete + CODE_ERASE_LINE = 248, + CODE_GO_AHEAD = 249, // not used, always negotiate to SGA + CODE_SB = 250, + CODE_WILL = 251, + CODE_WONT = 252, + CODE_DO = 253, + CODE_DONT = 254, + CODE_IAC = 255 +}; + +enum parser_state { + // next character is expected to be text or IAC + PS_TEXT = 0, + // received IAC, next character should be a verb + PS_IAC, + PS_WILL, + PS_WONT, + PS_DO, + PS_DONT, + PS_SUBNEG, + PS_SUBNEG_BODY, + PS_SUBNEG_BODY_IAC +}; + +struct awaiting_reply_item; + +struct awaiting_reply_item { + enum telnet_code code; + enum telnet_option option; + SLIST_ENTRY(awaiting_reply_item) next; +}; + +SLIST_HEAD(awaiting_reply_list, awaiting_reply_item); + +#define SUBNEG_BUF_LEN 16 +static struct telnet_state { + enum parser_state pstate; + enum telnet_option subneg_option; + struct awaiting_reply_list awaiting_reply; + uint8_t subneg_buf[SUBNEG_BUF_LEN]; + uint8_t subneg_buf_i; + bool mode_rx_binary; + bool mode_tx_binary; +} s_ts = { /* all zeros */ }; + +static void _expect_reply(enum telnet_code code, enum telnet_option option) +{ + ESP_LOGV(TAG, "Awaiting reply to %d:0x%02x", code, option); + struct awaiting_reply_item *item = calloc(sizeof(struct awaiting_reply_item), 1); + item->code = code; + item->option = option; + SLIST_INSERT_HEAD(&s_ts.awaiting_reply, item, next); +} + +static void send_will(console_ctx_t *cctx, enum telnet_option opt, bool is_response) +{ + ESP_LOGD(TAG, "Send WILL 0x%02x", opt); + if (!is_response) _expect_reply(CODE_WILL, opt); + const uint8_t buf[3] = {CODE_IAC, CODE_WILL, opt}; + console_write_ctx(cctx, (const char *) buf, 3); +} + +static void send_wont(console_ctx_t *cctx, enum telnet_option opt, bool is_response) +{ + ESP_LOGD(TAG, "Send WON'T 0x%02x", opt); + if (!is_response) _expect_reply(CODE_WONT, opt); + const uint8_t buf[3] = {CODE_IAC, CODE_WONT, opt}; + console_write_ctx(cctx, (const char *) buf, 3); +} + +static void send_do(console_ctx_t *cctx, enum telnet_option opt, bool is_response) +{ + ESP_LOGD(TAG, "Send DO 0x%02x", opt); + if (!is_response) _expect_reply(CODE_DO, opt); + const uint8_t buf[3] = {CODE_IAC, CODE_DO, opt}; + console_write_ctx(cctx, (const char *) buf, 3); +} + +static void send_dont(console_ctx_t *cctx, enum telnet_option opt, bool is_response) +{ + ESP_LOGD(TAG, "Send DON'T 0x%02x", opt); + if (!is_response) _expect_reply(CODE_DONT, opt); + const uint8_t buf[3] = {CODE_IAC, CODE_DONT, opt}; + console_write_ctx(cctx, (const char *) buf, 3); +} + +static void send_subnegotiate(console_ctx_t *cctx, enum telnet_option opt, const uint8_t *data, size_t len, bool is_response) +{ + if (!is_response) _expect_reply(CODE_SB, opt); + ESP_LOGD(TAG, "Send SUBNEG 0x%02x", opt); + + const uint8_t hdr[] = {CODE_IAC, CODE_SB, opt}; + console_write_ctx(cctx, (const char *) hdr, 3); + + // a heroic attempt at doing the double-IAC encoding without malloc + const uint8_t *last_chunk = data; + const uint8_t a_iac = CODE_IAC; + for (int i = 0; i < len; i++) { + if (data[i] == CODE_IAC) { + if (last_chunk != &data[i]) { + console_write_ctx(cctx, (const char *) last_chunk, &data[i] - last_chunk); + console_write_ctx(cctx, (const char *) &a_iac, 1); + last_chunk = &data[i+1]; + } + } + } + + if (last_chunk != &data[len]) { + console_write_ctx(cctx, (const char *) last_chunk, &data[len] - last_chunk); + } + + const uint8_t foot[] = {CODE_IAC, CODE_SE}; + console_write_ctx(cctx, (const char *) foot, 2); +} + +void telnet_send_subnegotiate(console_ctx_t *cctx, enum telnet_option opt, const uint8_t *data, size_t len) +{ + send_subnegotiate(cctx, opt, data, len, false); +} + +void telnet_send_will(console_ctx_t *cctx, enum telnet_option opt) +{ + send_will(cctx, opt, false); +} + +void telnet_send_wont(console_ctx_t *cctx, enum telnet_option opt) +{ + send_wont(cctx, opt, false); +} + +void telnet_send_do(console_ctx_t *cctx, enum telnet_option opt) +{ + send_do(cctx, opt, false); +} + +void telnet_send_dont(console_ctx_t *cctx, enum telnet_option opt) +{ + send_dont(cctx, opt, false); +} + +/** + * Peer agreed to DO something, + * or announces it WILL do something + */ +static void handle_will(console_ctx_t *cctx, enum telnet_option opt) +{ + struct awaiting_reply_item *it; + SLIST_FOREACH(it, &s_ts.awaiting_reply, next) { + if (it->option == opt) { + if (it->code == CODE_DO) { + ESP_LOGD(TAG, "Peer agreed to DO 0x%02x", opt); + } else if (it->code == CODE_DONT) { + ESP_LOGW(TAG, "Peer refused to NOT DO 0x%02x", opt); + // TODO deal with it + } else { + continue; // leave this entry alone, it's not related + } + SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next); + free(it); + return; + } + } + + // unexpected WILL - peer offers to enable an option + switch (opt) { + case OPT_BINARY: + send_do(cctx, opt, true); + s_ts.mode_rx_binary = true; + break; + case OPT_ECHO: + send_dont(cctx, opt, true); + // that's a bad idea, don't do it + break; + case OPT_SUPPRESS_GO_AHEAD: + send_do(cctx, opt, true); + break; + + default: + ESP_LOGD(TAG, "unknown option for WILL: 0x%02x", opt); + // please don't, we don't understand what it is + send_dont(cctx, opt, true); + } +} + +/** + * Peer refused to DO something, + * or announces it WON'T do something + */ +static void handle_wont(console_ctx_t *cctx, enum telnet_option opt) +{ + struct awaiting_reply_item *it; + SLIST_FOREACH(it, &s_ts.awaiting_reply, next) { + if (it->option == opt) { + if (it->code == CODE_DO) { + ESP_LOGW(TAG, "Peer refused to DO 0x%02x", opt); + // TODO deal with it + } else if (it->code == CODE_DONT) { + ESP_LOGD(TAG, "Peer agreed to NOT DO 0x%02x", opt); + } else { + continue; // leave this entry alone, it's not related + } + SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next); + free(it); + return; + } + } + + // unexpected WON'T - peer offers to disable an option + switch (opt) { + case OPT_BINARY: + send_dont(cctx, opt, true); + s_ts.mode_rx_binary = false; + break; + case OPT_ECHO: + send_dont(cctx, opt, true); + // OK, we don't care + break; + case OPT_SUPPRESS_GO_AHEAD: + // We want SGA, GA is some legacy nonsense nobody uses + send_do(cctx, opt, true); + break; + + // TODO handle supported options + default: + ESP_LOGD(TAG, "unknown option for WON'T: 0x%02x", opt); + // yeah sure, don't do it, whatever + send_dont(cctx, opt, true); + } +} + +/** + * Peer requests we DO something, + * or agrees to something we announced with WILL + */ +static void handle_do(console_ctx_t *cctx, enum telnet_option opt) +{ + struct awaiting_reply_item *it; + SLIST_FOREACH(it, &s_ts.awaiting_reply, next) { + if (it->option == opt) { + if (it->code == CODE_WONT) { + ESP_LOGW(TAG, "Peer rejected that we WON'T 0x%02x", opt); + // TODO deal with it + } else if (it->code == CODE_WILL) { + ESP_LOGD(TAG, "Peer accepted that we WILL 0x%02x", opt); + } else { + continue; // leave this entry alone, it's not related + } + SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next); + free(it); + return; + } + } + + // unexpected DO - peer requests we do something + switch (opt) { + case OPT_BINARY: + send_will(cctx, opt, true); + s_ts.mode_tx_binary = true; + break; + case OPT_ECHO: + // FIXME + send_wont(cctx, opt, true); +// send_will(cctx, opt, true); +// linenoiseSetEchoMode(1); + break; + case OPT_SUPPRESS_GO_AHEAD: + send_will(cctx, opt, true); + break; + case OPT_STATUS: + send_will(cctx, opt, true); + break; + case OPT_TIMING_MARK: + send_will(cctx, opt, true); + break; + + // TODO handle supported options + default: + ESP_LOGD(TAG, "unknown option for DO: 0x%02x", opt); + // we don't know how to do that, so we won't + send_wont(cctx, opt, true); + } +} + +/** + * Peer requests we DON'T do something, + * or agrees we shouldn't do something we announced for with WON'T + */ +static void handle_dont(console_ctx_t *cctx, enum telnet_option opt) +{ + struct awaiting_reply_item *it; + SLIST_FOREACH(it, &s_ts.awaiting_reply, next) { + if (it->option == opt) { + if (it->code == CODE_WILL) { + ESP_LOGW(TAG, "Peer rejected that we WILL 0x%02x", opt); + // TODO deal with it + + switch (opt) { + case OPT_ECHO: + // ok, let's not echo then + // FIXME +// linenoiseSetEchoMode(0); + break; + default: + ESP_LOGW(TAG, "Rejection unhandled"); + } + } else if (it->code == CODE_WONT) { + ESP_LOGD(TAG, "Peer accepted that we WON'T 0x%02x", opt); + } else { + continue; // leave this entry alone, it's not related + } + SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next); + free(it); + return; + } + } + + // unexpected DON'T - peer requests we don't do something + switch (opt) { + case OPT_BINARY: + send_wont(cctx, opt, true); + s_ts.mode_tx_binary = false; + break; + case OPT_ECHO: + send_wont(cctx, opt, true); + // FIXME +// linenoiseSetEchoMode(0); + break; + case OPT_SUPPRESS_GO_AHEAD: + // no, we absolutely won't use GA + send_will(cctx, opt, true); + break; + + // TODO handle supported options + default: + ESP_LOGD(TAG, "unknown option for DON'T: 0x%02x", opt); + // false is probably the default, so we won't do that, ok... + send_wont(cctx, opt, true); + } +} + +/** + * subnegotiation command (ts.subneg_option) was received and is now stored in + * ts.subneg_buf[0 .. ts.subneg_buf_i]. Handle it accordingly + */ +static void handle_subnegotiate(void) +{ + struct awaiting_reply_item *it; + SLIST_FOREACH(it, &s_ts.awaiting_reply, next) { + if (it->code == CODE_SB && it->option == s_ts.subneg_option) { + // this is an expected reply, delete the await token + SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next); + ESP_LOGD(TAG, "got reply to SB 0x%02x", s_ts.subneg_option); + + // TODO now we would handle the received data + free(it); + return; + } + } + + // it was not a reply to our request, i.e. the client is asking for something + + ESP_LOGW(TAG, "recv unsupported subneg request 0x%02x", s_ts.subneg_option); + + // TODO handle some requests.. +} + +size_t telnet_middleware_read(console_ctx_t *cctx, uint8_t *buffer, size_t len) +{ + ESP_LOGV(TAG, "-- Telnet handle %d chars --", len); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, buffer, (uint16_t) len, ESP_LOG_VERBOSE); + + uint8_t *wp = buffer; + for (size_t i = 0; i < len; i++) { + uint8_t c = buffer[i]; + + if (c >= 32 && c < 127) { + ESP_LOGV(TAG, "char %d (%c)", c, c); + } else { + ESP_LOGV(TAG, "char %d", c); + } + + switch (s_ts.pstate) { + case PS_TEXT: + if (c == CODE_IAC) { + ESP_LOGV(TAG, "TEXT->IAC"); + s_ts.pstate = PS_IAC; + } else { + ESP_LOGV(TAG, "TEXT(%c)", c); + *wp++ = c; + } + break; + case PS_IAC: + switch (c) { + case CODE_IAC: + // double IAC means literal IAC (but this shouldn't normally happen) + *wp++ = c; + s_ts.pstate = PS_TEXT; + ESP_LOGV(TAG, "Double IAC in TEXT"); + break; + case CODE_WILL: + ESP_LOGV(TAG, "IAC->WILL"); + s_ts.pstate = PS_WILL; + break; + case CODE_WONT: + ESP_LOGV(TAG, "IAC->WONT"); + s_ts.pstate = PS_WONT; + break; + case CODE_DO: + ESP_LOGV(TAG, "IAC->DO"); + s_ts.pstate = PS_DO; + break; + case CODE_DONT: + ESP_LOGV(TAG, "IAC->DONT"); + s_ts.pstate = PS_DONT; + break; + case CODE_SB: + ESP_LOGV(TAG, "IAC->SUBNEG"); + s_ts.pstate = PS_SUBNEG; + break; + case CODE_AYT: + ESP_LOGV(TAG, "IAC->AYT"); + s_ts.pstate = PS_TEXT; + const char *reply = "\r\nI am here.\r\n"; + console_write_ctx(cctx, reply, strlen(reply)); + break; + case CODE_IP: + ESP_LOGV(TAG, "IAC->IP, reboot"); + esp_restart(); + break; + case CODE_BRK: + ESP_LOGV(TAG, "IAC->BRK, kick clients"); + telnetsrv_kick_all(); + break; + default: + ESP_LOGW(TAG, "Unexpected code IAC+%d, discard!", c); + // Unecpected character, discard and go to base state + s_ts.pstate = PS_TEXT; + } + break; + case PS_WILL: + handle_will(cctx, c); + s_ts.pstate = PS_TEXT; + ESP_LOGV(TAG, "WILL->TEXT"); + break; + case PS_WONT: + handle_wont(cctx, c); + s_ts.pstate = PS_TEXT; + ESP_LOGV(TAG, "WONT->TEXT"); + break; + case PS_DO: + handle_do(cctx, c); + s_ts.pstate = PS_TEXT; + ESP_LOGV(TAG, "DO->TEXT"); + break; + case PS_DONT: + handle_dont(cctx, c); + s_ts.pstate = PS_TEXT; + ESP_LOGV(TAG, "DONT->TEXT"); + break; + case PS_SUBNEG: + s_ts.subneg_option = c; + s_ts.pstate = PS_SUBNEG_BODY; + s_ts.subneg_buf_i = 0; + ESP_LOGV(TAG, "SUBNEG->SUBNEG_BODY, opt %d", c); + break; + case PS_SUBNEG_BODY: + if (c == CODE_IAC) { + ESP_LOGV(TAG, "IAC in subneg body"); + s_ts.pstate = PS_SUBNEG_BODY_IAC; + } else { + if (s_ts.subneg_buf_i < SUBNEG_BUF_LEN) { + ESP_LOGV(TAG, "subneg += %d", c); + s_ts.subneg_buf[s_ts.subneg_buf_i++] = c; + } else { + ESP_LOGV(TAG, "subneg too long!"); + } + } + break; + case PS_SUBNEG_BODY_IAC: + switch (c) { + case CODE_IAC: + ESP_LOGV(TAG, "Literal IAC in subneg"); + // double IAC means literal IAC code + s_ts.subneg_buf[s_ts.subneg_buf_i++] = CODE_IAC; + s_ts.pstate = PS_SUBNEG_BODY; + break; + case CODE_SE: + ESP_LOGV(TAG, "Subneg END"); + // end of subneg block + if (s_ts.subneg_buf_i == SUBNEG_BUF_LEN) { + ESP_LOGW(TAG, "Subneg buf OV, discard command SB %d!", s_ts.subneg_option); + } else { + ESP_LOGV(TAG, "Subneg OK! Handling..."); + handle_subnegotiate(); + } + s_ts.pstate = PS_TEXT; + break; + default: + ESP_LOGV(TAG, "Illegal char in subneg IAC+%d, discard", c); + // illegal character, act like the subneg block was closed, but discard the content + s_ts.pstate = PS_TEXT; + } + break; + } + } + + size_t remains = wp - buffer; + + ESP_LOGV(TAG, "Remain %d cleartext chars", remains); + ESP_LOGV(TAG, "> %*.s", remains, buffer); + return remains; +} diff --git a/main/console/telnet_parser.h b/main/console/telnet_parser.h new file mode 100644 index 0000000..16705f2 --- /dev/null +++ b/main/console/telnet_parser.h @@ -0,0 +1,47 @@ +/** + * Telnet middleware that handles characters read by console and acts on telnet escapes, + * removing them from the stream + * + * Created on 2019/02/17. + */ + +#ifndef CSPEMU_TELNET_PARSER_H +#define CSPEMU_TELNET_PARSER_H + +#include + +/** + * Process a buffer of received characters, handling and removing telnet codes. + * The buffer is modified in place and the number of data characters is returned. + * + * @param[in,out] buffer - buffer to process + * @param len - original number of characters in the buffer + * @return - number of characters left in the buffer, to be passed to the console handler + */ +size_t telnet_middleware_read(console_ctx_t *cctx, uint8_t *buffer, size_t len); + +enum telnet_option { + OPT_BINARY = 0, + OPT_ECHO = 1, + OPT_SUPPRESS_GO_AHEAD = 3, + OPT_STATUS = 5, + OPT_TIMING_MARK = 6, + OPT_TERMINAL_TYPE = 24, + OPT_END_OF_RECORD = 25, + OPT_NEGOTIATE_WINDOW_SIZE = 31, + OPT_NEGOTIATE_TERMINAL_SPEED = 32, + OPT_REMOTE_FLOW_CONTROL = 33, +// OPT_COMPORT_OPTION = 34, - according to RFC, but seems replaced by... + OPT_LINEMODE = 34, + OPT_X_DISPLAY_LOCATION = 35, + OPT_NEW_ENVIRON = 39, + OPT_EXOPL = 255, +}; + +void telnet_send_subnegotiate(console_ctx_t *cctx, enum telnet_option opt, const uint8_t *data, size_t len); +void telnet_send_will(console_ctx_t *cctx, enum telnet_option opt); +void telnet_send_wont(console_ctx_t *cctx, enum telnet_option opt); +void telnet_send_do(console_ctx_t *cctx, enum telnet_option opt); +void telnet_send_dont(console_ctx_t *cctx, enum telnet_option opt); + +#endif //CSPEMU_TELNET_PARSER_H diff --git a/main/gitversion.h.in b/main/gitversion.h.in new file mode 100644 index 0000000..7625d1b --- /dev/null +++ b/main/gitversion.h.in @@ -0,0 +1,9 @@ +/* gitversion.h. Generated by cmake from gitversion.h.in */ +#ifndef _GITVERSION_H +#define _GITVERSION_H + +#define GIT_COUNT "@_revision_number@" +#define GIT_HASH "@_commit_hash@" +#define BUILD_TIMESTAMP "@_build_time_stamp@" + +#endif //_GITVERSION_H diff --git a/main/settings.c b/main/settings.c new file mode 100644 index 0000000..f2496b2 --- /dev/null +++ b/main/settings.c @@ -0,0 +1,160 @@ +// +// Created by MightyPork on 2018/11/18. +// + +#include +#include +#include +#include "settings.h" +#include "utils.h" + +static const char * TAG = "settings.c"; + +static uint16_t app_boot_count = 0; + +#undef X +#define X(pre,name,post,def,gen_nvs,nvs_format,save_prefix) .name = def, +const struct cspemu_settings defaults = { + SETTINGS_XTABLE() +}; + + +struct cspemu_settings g_Settings; +struct cspemu_globals g_State = { + .wifi_inited = false, +}; + +static nvs_handle storage = 0; +extern nvs_handle g_nvs_storage __attribute__((alias("storage"))); + +void settings_init(void) +{ + ESP_ERROR_CHECK(nvs_open("cspemu", NVS_READWRITE, &storage)); +} + +union fu { + float f; + uint32_t u; +}; + +/** Read float from NVS. See `nvs_get_u32` for more info. */ +static esp_err_t nvs_get_f32 (nvs_handle_t handle, const char* key, float* out_value) { + uint32_t u = 0; + esp_err_t rv = nvs_get_u32(handle, key, &u); + if (rv != ESP_OK) { + return rv; + } + + union fu reinterpret = { + .u = u, + }; + + *out_value = reinterpret.f; + return ESP_OK; +} + +/** Write float to NVS. See `nvs_set_u32` for more info. */ +static esp_err_t nvs_set_f32 (nvs_handle_t handle, const char* key, float value) { + union fu reinterpret = { + .f = value, + }; + return nvs_set_u32(handle, key, reinterpret.u); +} + +/** Dummy read from NVS; placeholder for XTABLE entries with manually implemented getters */ +static esp_err_t nvs_get_none(nvs_handle handle, const char* key, void* out_value) { + return ESP_OK; +} + +/** Dummy write to NVS; placeholder for XTABLE entries with manually implemented setters */ +static esp_err_t nvs_set_none(nvs_handle handle, const char* key, void* value) { + return ESP_OK; +} + +void settings_load(void) +{ + esp_err_t rv; + memcpy(&g_Settings, &defaults, sizeof(struct cspemu_settings)); + + // abort on failure other than "not found" +#define NVSCHECK(callback) \ + do { \ + rv = callback; \ + if (rv != ESP_OK && rv != ESP_ERR_NVS_NOT_FOUND) { \ + ESP_LOGE(TAG, "NVS ERR %d", rv); \ + abort(); \ + } \ + } while(0) + + NVSCHECK(nvs_get_u16(storage, "bootcount", &app_boot_count)); + ESP_LOGI(TAG, "NVS restored bootcount %d", app_boot_count); + app_boot_count++; + NVSCHECK(nvs_set_u16(storage, "bootcount", app_boot_count)); + + + // version will be used for settings format upgrades + uint8_t version = 0; + NVSCHECK(nvs_get_u8(storage, "version", &version)); + +#undef X +#define X(pre, name, post, def, gen_nvs, nvs_format, save_prefix) \ + if (gen_nvs) { \ + NVSCHECK(nvs_get_##nvs_format(storage, STR(name), save_prefix g_Settings. name)); \ + } + + SETTINGS_XTABLE() + + // custom values + +#define ensure_str_terminated(str, len) if (strnlen((str), (len)) >= (len)) { (str)[(len)-1] = 0; } + + { + size_t capacity = CONSOLE_PW_LEN; + NVSCHECK(nvs_get_str(storage, "tcpp_pw", g_Settings.console_pw, &capacity)); + ensure_str_terminated(g_Settings.console_pw, CONSOLE_PW_LEN); + } + { + size_t capacity = NTP_SRV_LEN; + NVSCHECK(nvs_get_str(storage, "ntp_srv", g_Settings.ntp_srv, &capacity)); + ensure_str_terminated(g_Settings.ntp_srv, NTP_SRV_LEN); + } +} + +void settings_persist(enum settings_key_enum what) +{ + esp_err_t rv; + char name[24]; + + #undef NVSCHECK + #define NVSCHECK(callback) \ + do { \ + rv = (callback); \ + if (rv != ESP_OK) { \ + ESP_LOGE(TAG, "NVS ERR %d", rv); \ + abort(); \ + } \ + } while(0) + + +#undef X +#define X(pre,name,post,def,gen_nvs,nvs_format,save_prefix) \ + if ((gen_nvs) && ((what)==SETTINGS_ALL || (what)==SETTINGS_## name)) { \ + NVSCHECK(nvs_set_##nvs_format(storage, STR(name), g_Settings. name)); \ + } + + SETTINGS_XTABLE() + + // custom values + if (what==SETTINGS_ALL || what==SETTINGS_console_pw) { + NVSCHECK(nvs_set_str(storage, "tcpp_pw", g_Settings.console_pw)); + } + + if (what==SETTINGS_ALL || what==SETTINGS_ntp_srv) { + NVSCHECK(nvs_set_str(storage, "ntp_srv", g_Settings.ntp_srv)); + } +} + +uint16_t app_get_bootcount() +{ + return app_boot_count; +} diff --git a/main/settings.h b/main/settings.h new file mode 100644 index 0000000..45626ff --- /dev/null +++ b/main/settings.h @@ -0,0 +1,97 @@ +// +// Created by MightyPork on 2018/11/18. +// + +#ifndef CSPEMU_SETTINGS_H +#define CSPEMU_SETTINGS_H + +#include +#include +#include +#include +#include + +#define CONSOLE_TELNET_PORT CONFIG_CONSOLE_TELNET_PORT +#define CONSOLE_PW_LEN CONFIG_CONSOLE_PW_LEN +#define USE_CAPTIVE_PORTAL 0 + +extern nvs_handle g_nvs_storage; + +#define NTP_SRV_LEN 32 +#define DEF_NTP_SRV "pool.ntp.org" + +/* type , name , suffix , defval , generate load/save funcs + * , , save fn - use 'none' if unused to avoid build errors + * , , , save fn var prefix */ +#define SETTINGS_XTABLE() \ + X(bool , wifi_enabled , , 1 , true , bool , &) /* ssid and pass are stored in flash by SDK funcs */ \ + X(bool , sta_enabled , , 1 , true , bool , &) \ + X(bool , ap_enabled , , 0 , true , bool , &) \ + X(bool , console_echo , , 1 , true , bool , &) /* Console modes */ \ + X(bool , console_ansi , , 1 , true , bool , &) \ + X(char , console_pw, [CONSOLE_PW_LEN], "" , false , none , ) \ + X(bool , ntp_enable , , 1 , true , bool , &) \ + X(char , ntp_srv ,[NTP_SRV_LEN], DEF_NTP_SRV , false, none , ) \ + X(bool , dhcp_wd_enable , , 1 , true , bool , &) \ + X(bool , dhcp_enable , , 1 , true , bool , &) \ + X(uint32_t , static_ip , , 0 , true , u32 , &) \ + X(uint32_t , static_ip_mask , , 0x00ffffff , true , u32 , &) /* 255.255.255.0 */ \ + X(uint32_t , static_ip_gw , , 0x0100a8c0 , true , u32 , &) /* 192.168.0.1 */ \ + X(uint32_t , ap_ip , , 0x0100a8c0 , true , u32 , &) /* 192.168.0.1 */ \ + X(uint32_t , static_dns , , 0x08080808 , true , u32 , &) /* 8.8.8.8 */ + +enum settings_key_enum { +#undef X +#define X(pre,name,post,def,gen_nvs,nvs_format,save_prefix) \ + SETTINGS_##name, + + SETTINGS_ALL, + SETTINGS_XTABLE() // unfortunately the last bit is lowercase +}; + +/** + * Init the NVS namespace + */ +void settings_init(void); + +/** + * Load settings from the NVS + */ +void settings_load(void); + +/** + * Save the settings object to NVS + */ +void settings_persist(enum settings_key_enum what); + +/** + * Load default rtable into the settings struct + * (called when the saved config is rejected by CSP) + */ +void settings_load_default_rtable(void); + +/** + * Save the current CSP rtable into the settings object (can then be persisted) + */ +void cspemu_settings_store_rtable(void); + +extern struct cspemu_settings g_Settings; +extern struct cspemu_globals g_State; + + +struct cspemu_globals { + bool wifi_inited; +}; + +struct cspemu_settings { +#undef X + +#define X(pre,name,post,def,gen_nvs,nvs_format,save_prefix) \ + pre name post; + + SETTINGS_XTABLE() +}; + +uint16_t app_get_bootcount(); + +#endif //CSPEMU_SETTINGS_H diff --git a/main/shutdown_handlers.c b/main/shutdown_handlers.c new file mode 100644 index 0000000..ae8ae17 --- /dev/null +++ b/main/shutdown_handlers.c @@ -0,0 +1,31 @@ +#include "esp_log.h" +#include "shutdown_handlers.h" + +static const char *TAG = "shutdown_hdl"; + +#define MAX_SHUTDOWN_HANDLERS 10 +static int shutdown_handlers_next = 0; +static shutdown_handler_t shutdown_handlers[MAX_SHUTDOWN_HANDLERS]; + +// --------------------------- + +esp_err_t cspemu_add_shutdown_handler(shutdown_handler_t handler) +{ + ESP_LOGI(TAG, "+ new shutdown handler %p", handler); + if (shutdown_handlers_next < MAX_SHUTDOWN_HANDLERS) { + shutdown_handlers[shutdown_handlers_next++] = handler; + return ESP_OK; + } else { + return ESP_FAIL; + } +} + +void cspemu_run_shutdown_handlers(void) +{ + ESP_LOGI(TAG, "Running shutdown handlers"); + for (int i = 0; i < shutdown_handlers_next; i++) { + shutdown_handlers[i](); + } + // prevent them being run multiple times + shutdown_handlers_next = 0; +} diff --git a/main/shutdown_handlers.h b/main/shutdown_handlers.h new file mode 100644 index 0000000..6b2c37c --- /dev/null +++ b/main/shutdown_handlers.h @@ -0,0 +1,17 @@ +/** + * TODO file description + * + * Created on 2020/04/02. + */ + +#ifndef CSPEMU_SHUTDOWN_HANDLERS_H +#define CSPEMU_SHUTDOWN_HANDLERS_H + +#include "esp_err.h" +#include "esp_system.h" + +esp_err_t cspemu_add_shutdown_handler(shutdown_handler_t handler); + +void cspemu_run_shutdown_handlers(void); + +#endif //CSPEMU_SHUTDOWN_HANDLERS_H diff --git a/main/sntp_cli.c b/main/sntp_cli.c new file mode 100644 index 0000000..ffd5cb6 --- /dev/null +++ b/main/sntp_cli.c @@ -0,0 +1,63 @@ +#include "sntp_cli.h" +#include +#include +#include "esp_log.h" +#include "tasks.h" +#include "utils.h" +#include "lwip/err.h" +#include "lwip/apps/sntp.h" +#include "settings.h" + +static const char * TAG = "sntp_cli"; + +static volatile bool sntp_running = false; + +static void vTaskSNTPclient(void * pvParameters) +{ + (void)pvParameters; //UN-USED + ESP_LOGI(TAG, "Starting SNTP client, server %s", g_Settings.ntp_srv); + vTaskDelay(configTICK_RATE_HZ); + + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, g_Settings.ntp_srv); + sntp_init(); + vTaskDelay(2*configTICK_RATE_HZ); + + time_t now; + struct tm timeinfo; + int retry = 0; + const int retry_count = 10; + + time(&now); + localtime_r(&now, &timeinfo); + + while(timeinfo.tm_year < (2016 - 1900) && ++retry < retry_count) { + ESP_LOGD(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count); + vTaskDelay(3*configTICK_RATE_HZ); + time(&now); + localtime_r(&now, &timeinfo); + } + + if(timeinfo.tm_year < (2016 - 1900)) { + ESP_LOGW(TAG, "SNTP client failed to get time!"); + } else { + char strftime_buf[64]; + strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo); + ESP_LOGI(TAG, "The current UTC date/time is: %s, SNTP client done", strftime_buf); + } + + sntp_stop(); + sntp_running = false; + vTaskDelete(NULL); +} + +bool sntp_cli_start() +{ + if (sntp_running) { + return false; + } + + sntp_running = true; + RTOS_ERROR_CHECK(xTaskCreate(vTaskSNTPclient, "SNTPc", 4096, NULL, PRIO_LOW, NULL)); + return true; +} diff --git a/main/sntp_cli.h b/main/sntp_cli.h new file mode 100644 index 0000000..3cc99da --- /dev/null +++ b/main/sntp_cli.h @@ -0,0 +1,9 @@ +#ifndef SNTP_CLI_H +#define SNTP_CLI_H + +#include + +/** Start the SNTP client. Returns true if started, false if already running. */ +bool sntp_cli_start(); + +#endif // SNTP_CLI_H diff --git a/main/tasks.h b/main/tasks.h new file mode 100644 index 0000000..1021c05 --- /dev/null +++ b/main/tasks.h @@ -0,0 +1,18 @@ +// +// Created by MightyPork on 2018/12/16. +// + +#ifndef CSPEMU_PRIORITIES_H +#define CSPEMU_PRIORITIES_H + +#define PRIO_NORMAL 4 +#define PRIO_LOW 2 +#define PRIO_HIGH 6 + +#define CONSOLE_TASK_STACK 4096 +#define CONSOLE_TASK_PRIO PRIO_NORMAL + +#define TELNET_TASK_STACK 4096 +#define TELNET_TASK_PRIO PRIO_LOW + +#endif //CSPEMU_PRIORITIES_H diff --git a/main/utils.c b/main/utils.c new file mode 100644 index 0000000..45e1df0 --- /dev/null +++ b/main/utils.c @@ -0,0 +1,71 @@ +#include +#include +#include "esp_log.h" +#include "esp_wifi.h" +#include "utils.h" +#include "application.h" + +static const char * TAG = "utils.c"; + +static void recon_internal(void); + +/** + * Attempt to reconnect to AP if we have SSID stored + */ +void try_reconn_if_have_wifi_creds(void) +{ + recon_internal(); +} + +static volatile bool recurse = false; + +static void recon_internal(void) +{ + if (recurse) { + ESP_LOGE(TAG, "Stopping recursion!"); + } + recurse = true; + + wifi_config_t wificonf; + + // try to connect if we have a saved config + ESP_ERROR_CHECK(esp_wifi_get_config(WIFI_IF_STA, &wificonf)); + + if (wificonf.sta.ssid[0]) { + ESP_LOGI(TAG, "(Re)connecting using saved STA creds"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } else { + ESP_LOGI(TAG, "No WiFi creds, no (re)conn"); + } + + recurse = false; +} + + +esp_err_t nvs_get_bool(nvs_handle handle, const char* key, bool* out_value) +{ + uint8_t x = (uint8_t) *out_value; + esp_err_t rv = nvs_get_u8(handle, key, &x); + if (rv == ESP_OK) { + *out_value = (bool)x; + } + return rv; +} + +esp_err_t nvs_set_bool(nvs_handle handle, const char* key, bool value) +{ + return nvs_set_u8(handle, key, (uint8_t) value); +} + +void feed_all_dogs(void) +{ + TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; + TIMERG0.wdt_feed=1; + TIMERG0.wdt_wprotect=0; + + TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; + TIMERG1.wdt_config2=CONFIG_INT_WDT_TIMEOUT_MS*2; //Set timeout before interrupt + TIMERG1.wdt_config3=CONFIG_INT_WDT_TIMEOUT_MS*4; //Set timeout before reset + TIMERG1.wdt_feed=1; + TIMERG1.wdt_wprotect=0; +} diff --git a/main/utils.h b/main/utils.h new file mode 100644 index 0000000..d0d52fa --- /dev/null +++ b/main/utils.h @@ -0,0 +1,76 @@ +/** + * Utilities and helpers + */ + +#ifndef CSPEMU_UTILS_H +#define CSPEMU_UTILS_H + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include +#include +#include + +/** Error check macro for FreeRTOS status codes */ +#define RTOS_ERROR_CHECK(code) if (pdPASS != (code)) ESP_ERROR_CHECK(ESP_FAIL); + +/** Error check macro for NULL return value */ +#define NULL_CHECK(code) if (NULL == (code)) ESP_ERROR_CHECK(ESP_FAIL); + +/** Error check macro for CSP status codes */ +#define CSP_ERROR_CHECK(code) if (CSP_ERR_NONE != (code)) ESP_ERROR_CHECK(ESP_FAIL); + +/** + * Reconnect to WiFi if there are saved credentials. + * Called from the main event group handler. + */ +void try_reconn_if_have_wifi_creds(void); + +/** + * Helper to retrieve a bool value from the NVS. Internally uses 'u8' + * + * @param handle - NVS storage + * @param key + * @param out_value + * @return success + */ +esp_err_t nvs_get_bool(nvs_handle handle, const char* key, bool* out_value); + +/** + * Helper to store a bool into the NVS. Internally uses 'u8' + * + * @param handle - NVS storage + * @param key + * @param value + * @return success + */ +esp_err_t nvs_set_bool(nvs_handle handle, const char* key, bool value); + +/** + * Feed watchdogs (inside a busy wait loop) + */ +void feed_all_dogs(void); + +/** + * @brief malloc() and snprintf() combined + * @attention DO NOT use in if/for/do etc without braces. + * + * The caller is responsible for disposing of the allocated string afterwards. + */ +#define malloc_sprintf(var, n, format, ...) \ + char *var = calloc(n, 1); \ + if (!var) assert(0); \ + snprintf(var, n, format, ##__VA_ARGS__); + +/** + * Generate externs for an embedded file. + * Variables {varname} and {varname}_end will be produced. + */ +#define efile(varname, filename) \ + extern const char varname[] asm("_binary_"filename"_start"); \ + extern const char varname##_end[] asm("_binary_"filename"_end"); + +/** Get embedded file size (must be declared with efile() first) */ +#define efsize(varname) ((varname##_end) - (varname)) + +#endif //CSPEMU_UTILS_H diff --git a/main/web/websrv.c b/main/web/websrv.c new file mode 100644 index 0000000..b875853 --- /dev/null +++ b/main/web/websrv.c @@ -0,0 +1,165 @@ +#include +#include + +#include +#include +#include + +#include "websrv.h" +#include "esp_http_server.h" +#include "utils.h" + +#include "www_files_enum.h" + +static const char *TAG = "websrv"; + +static httpd_handle_t s_hServer = NULL; + +// Embedded files (must also be listed in CMakeLists.txt as COMPONENT_EMBED_TXTFILES) +efile(index_file, "index_html"); + +static struct tpl_kv_list build_index_replacements_kv(void) { + //char name[TPL_KV_KEY_LEN]; + struct tpl_kv_list kv = tpl_kv_init(); + tpl_kv_add(&kv, "version", APP_VERSION); + return kv; +} + +/* Main page */ +static esp_err_t handler_index(httpd_req_t *req) { + struct tpl_kv_list kv = build_index_replacements_kv(); + + esp_err_t suc = httpd_send_template_file(req, FILE_INDEX_HTML, tpl_kv_replacer, &kv, 0); + tpl_kv_free(&kv); + return suc; +} + +/* Update XHR for new index page data */ +static esp_err_t handler_update(httpd_req_t *req) { + struct tpl_kv_list kv = build_index_replacements_kv(); + + esp_err_t suc = tpl_kv_send_as_ascii_map(req, &kv); + tpl_kv_free(&kv); + return suc; +} + +/* Set a param */ +static esp_err_t handler_set(httpd_req_t *req) { + char buf[64]; + int n = httpd_req_recv(req, buf, 63); + if (n < 0) { + ESP_LOGW(TAG, "rx er"); + goto err; + } + buf[n] = 0; + + char keybuf[20]; + char valbuf[20]; + if (ESP_OK != httpd_query_key_value(buf, "key", keybuf, 20)) goto err; + if (ESP_OK != httpd_query_key_value(buf, "value", valbuf, 20)) goto err; + + // TODO + ESP_LOGW(TAG, "bad key %s", keybuf); + goto err; + + return httpd_resp_send(req, NULL, 0); + + err: + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL); +} + +/* Request emulator reboot */ +static esp_err_t handler_reboot(httpd_req_t *req) { + httpd_resp_send(req, "" + "" + "" + "" + "Reboot requested. Reloading in 10 seconds.
" + "Try now.", -1); + + ESP_LOGI(TAG, "Restarting ESP..."); + esp_restart(); +} + +/* An HTTP GET handler */ +static esp_err_t handler_staticfiles(httpd_req_t *r) { + const struct embedded_file_info *file; + const char *fname = r->user_ctx; + enum file_access_level access = FILE_ACCESS_PROTECTED; + + // wildcard files must be public + if (fname == NULL) { + fname = r->uri + 1; // URI always starts with slash, but we dont want a slash in the file name + access = FILE_ACCESS_PUBLIC; + } + +#if USE_CAPTIVE_PORTAL + // First check if this is a phone taken here by the captive portal + esp_err_t rv = httpd_captive_redirect(r); + if (rv != ESP_ERR_NOT_FOUND) return rv; +#endif + + if (ESP_OK != www_get_static_file(fname, access, &file)) { + ESP_LOGW(TAG, "File not found: %s", fname); + return httpd_resp_send_404(r); + } else { + if (streq(file->mime, "text/html")) { + // using the template func to allow includes + return httpd_send_template_file_struct(r, file, /*replacer*/NULL, /*ctx*/NULL, /*opts*/0); + } else { + return httpd_send_static_file_struct(r, file, TPL_ESCAPE_NONE, 0); + } + } +} + +static const httpd_uri_t routes[] = { + { + .uri = "/", + .method = HTTP_GET, + .handler = handler_index, + }, + { + .uri = "/data", + .method = HTTP_GET, + .handler = handler_update, + }, + { + .uri = "/set", + .method = HTTP_POST, + .handler = handler_set, + }, + { + .uri = "/reboot", + .method = HTTP_GET, + .handler = handler_reboot, + }, + { + .uri = "*", // any file except protected (e.g. not HTML, PEM etc) + .method = HTTP_GET, + .handler = handler_staticfiles, + }, +}; + +esp_err_t websrv_init(void) { + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.max_open_sockets = 3; + + config.uri_match_fn = httpd_uri_match_wildcard; + config.lru_purge_enable = true; + + ESP_LOGI(TAG, "Starting HTTP server on port: '%d'", config.server_port); + + esp_err_t suc = httpd_start(&s_hServer, &config); + if (suc == ESP_OK) { + ESP_LOGI(TAG, "HTTP server started"); + + for (int i = 0; i < sizeof(routes) / sizeof(httpd_uri_t); i++) { + httpd_register_uri_handler(s_hServer, &routes[i]); + } + + return ESP_OK; + } + + ESP_LOGE(TAG, "Error starting server!"); + return suc; +} diff --git a/main/web/websrv.h b/main/web/websrv.h new file mode 100644 index 0000000..3bc9679 --- /dev/null +++ b/main/web/websrv.h @@ -0,0 +1,14 @@ +/** + * Integrated webserver + * + * Created on 2019/07/13. + */ + +#ifndef CSPEMU_WEBSRV_H +#define CSPEMU_WEBSRV_H + +#include + +esp_err_t websrv_init(void); + +#endif //CSPEMU_WEBSRV_H diff --git a/main/wifi_conn.c b/main/wifi_conn.c new file mode 100644 index 0000000..4e60385 --- /dev/null +++ b/main/wifi_conn.c @@ -0,0 +1,190 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "wifi_conn.h" +#include "esp_wifi.h" +#include "esp_wifi_types.h" +#include "esp_event.h" +#include "esp_log.h" +#include "sntp_cli.h" + +#include "application.h" +#include "dhcp_wd.h" + +#define WIFI_MAX_RETRY 5 + +/* FreeRTOS event group to signal when we are connected*/ +EventGroupHandle_t g_wifi_event_group; + +static const char *TAG = "wifi station"; + +static int s_retry_num = 0; +static dhcp_wd_handle_t dhcp_wd = NULL; + +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + ESP_LOGI(TAG, "STA disconnected"); + xEventGroupClearBits(g_wifi_event_group, WIFI_CONNECTED_BIT); + + if (dhcp_wd) { + dhcp_watchdog_notify(dhcp_wd, DHCP_WD_NOTIFY_DISCONNECTED); + } + + if (s_retry_num < WIFI_MAX_RETRY) { + esp_wifi_connect(); + s_retry_num++; + ESP_LOGI(TAG, "Retry to connect (try %d)", s_retry_num+1); + } else { + xEventGroupSetBits(g_wifi_event_group, WIFI_FAIL_BIT); + } + } else if (event_base == IP_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { + if (dhcp_wd) { + dhcp_watchdog_notify(dhcp_wd, DHCP_WD_NOTIFY_CONNECTED); + } + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + s_retry_num = 0; + xEventGroupSetBits(g_wifi_event_group, WIFI_CONNECTED_BIT); + + if (g_Settings.ntp_enable) { + sntp_cli_start(); + } + + if (dhcp_wd) { + dhcp_watchdog_notify(dhcp_wd, DHCP_WD_NOTIFY_GOT_IP); + } + } +} + +bool wifi_wait_connect() +{ + s_retry_num = 0; + + /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum + * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */ + EventBits_t bits = xEventGroupWaitBits(g_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY); + + /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually + * happened. */ + if (bits & WIFI_CONNECTED_BIT) { + ESP_LOGI(TAG, "Connected to AP!"); + return true; + } else if (bits & WIFI_FAIL_BIT) { + ESP_LOGI(TAG, "Failed to connect."); + return false; + } else { + ESP_LOGE(TAG, "UNEXPECTED EVENT"); + return false; + } +} + +void initialise_wifi(void) +{ + esp_err_t rv = ESP_OK; + + #define ERROR_CHECK(x) do { if (ESP_OK != (rv = (x))) goto fail; } while (0) + + assert(g_Settings.sta_enabled || g_Settings.ap_enabled); + + g_wifi_event_group = xEventGroupCreate(); + + ESP_LOGD(TAG, "create wifi netif"); + esp_netif_t *netif_sta = NULL; + if (g_Settings.sta_enabled) { + netif_sta = esp_netif_create_default_wifi_sta(); + } + if (g_Settings.ap_enabled) { + esp_netif_create_default_wifi_ap(); + } + + ESP_LOGD(TAG, "initing wifi"); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_LOGD(TAG, "set storage, set sta"); + ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_FLASH)); + + wifi_mode_t mode; + if (g_Settings.sta_enabled && g_Settings.ap_enabled) { + mode = WIFI_MODE_APSTA; + } else if (g_Settings.sta_enabled) { + mode = WIFI_MODE_STA; + } else { + // This function is never called if both STA and AP are disabled! + mode = WIFI_MODE_AP; + } + + ESP_LOGD(TAG, "set wifi mode"); + ERROR_CHECK(esp_wifi_set_mode(mode)); + + if (g_Settings.ap_enabled) { + ESP_LOGD(TAG, "set AP IP config"); + + tcpip_adapter_ip_info_t ip_info; + ERROR_CHECK(tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP)); + + memset(&ip_info, 0, sizeof(ip_info)); + ip_info.ip.addr = g_Settings.ap_ip; + ip_info.gw.addr = g_Settings.ap_ip; + ip_info.netmask.addr = 0x00ffffff; + + ERROR_CHECK(tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &ip_info)); + ERROR_CHECK(tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP)); + } + + const bool use_dhcp = (g_Settings.dhcp_enable || g_Settings.static_ip == 0) + && g_Settings.sta_enabled; + + if (g_Settings.sta_enabled) { + if (!use_dhcp) { + ESP_LOGD(TAG, "set STA static IP"); + + tcpip_adapter_ip_info_t ip_info; + ERROR_CHECK(tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA)); + + memset(&ip_info, 0, sizeof(ip_info)); + ip_info.ip.addr = g_Settings.static_ip; + ip_info.gw.addr = g_Settings.static_ip_gw; + ip_info.netmask.addr = g_Settings.static_ip_mask; + ERROR_CHECK(tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info)); + + tcpip_adapter_dns_info_t dnsinfo = { + // special esp version of ip address because why not I guess. + // ipv4 has union tag 0, so it needn't be set + .ip.u_addr.ip4.addr = g_Settings.static_dns, + }; + + ERROR_CHECK(tcpip_adapter_set_dns_info(TCPIP_ADAPTER_IF_STA, ESP_NETIF_DNS_MAIN, &dnsinfo)); + } + + ESP_LOGD(TAG, "register WiFi events"); + ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); + ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL)); + } + + ESP_LOGD(TAG, "wifi start"); + ERROR_CHECK(esp_wifi_start() ); + + ESP_LOGI(TAG, "WiFi config done"); + + if (g_Settings.dhcp_wd_enable && use_dhcp) { + ESP_LOGD(TAG, "Start gateway ping watchdog"); + ERROR_CHECK(dhcp_watchdog_start(netif_sta, true, &dhcp_wd)); + } + + return; + +fail: + ESP_LOGE(TAG, "Error setting up WiFi: %d - %s", rv, esp_err_to_name(rv)); +} diff --git a/main/wifi_conn.h b/main/wifi_conn.h new file mode 100644 index 0000000..45df540 --- /dev/null +++ b/main/wifi_conn.h @@ -0,0 +1,13 @@ +/** + * Handle WiFi connection + * + * Created on 2020/04/02. + */ + +#ifndef CSPEMU_WIFI_CONN_H +#define CSPEMU_WIFI_CONN_H + +void initialise_wifi(void); +bool wifi_wait_connect(); + +#endif //CSPEMU_WIFI_CONN_H diff --git a/sdkconfig b/sdkconfig new file mode 100644 index 0000000..90972fc --- /dev/null +++ b/sdkconfig @@ -0,0 +1,1319 @@ +# +# Automatically generated file. DO NOT EDIT. +# Espressif IoT Development Framework (ESP-IDF) Project Configuration +# +CONFIG_IDF_CMAKE=y +CONFIG_IDF_TARGET_ARCH_XTENSA=y +CONFIG_IDF_TARGET="esp32" +CONFIG_IDF_TARGET_ESP32=y +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0000 + +# +# SDK tool configuration +# +CONFIG_SDK_TOOLPREFIX="xtensa-esp32-elf-" +# CONFIG_SDK_TOOLCHAIN_SUPPORTS_TIME_WIDE_64_BITS is not set +# end of SDK tool configuration + +# +# Build type +# +CONFIG_APP_BUILD_TYPE_APP_2NDBOOT=y +# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set +CONFIG_APP_BUILD_GENERATE_BINARIES=y +CONFIG_APP_BUILD_BOOTLOADER=y +CONFIG_APP_BUILD_USE_FLASH_SECTIONS=y +# end of Build type + +# +# Application manager +# +CONFIG_APP_COMPILE_TIME_DATE=y +# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set +# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set +# CONFIG_APP_PROJECT_VER_FROM_CONFIG is not set +CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16 +# end of Application manager + +# +# Bootloader config +# +CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x1000 +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_NONE is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_NONE is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_ERROR is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_WARN is not set +CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y +# CONFIG_BOOTLOADER_LOG_LEVEL_DEBUG is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_VERBOSE is not set +CONFIG_BOOTLOADER_LOG_LEVEL=3 +# CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_8V is not set +CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y +# CONFIG_BOOTLOADER_FACTORY_RESET is not set +# CONFIG_BOOTLOADER_APP_TEST is not set +CONFIG_BOOTLOADER_WDT_ENABLE=y +# CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set +CONFIG_BOOTLOADER_WDT_TIME_MS=9000 +# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set +CONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0 +# CONFIG_BOOTLOADER_CUSTOM_RESERVE_RTC is not set +# end of Bootloader config + +# +# Security features +# +# CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT is not set +# CONFIG_SECURE_BOOT is not set +# CONFIG_SECURE_FLASH_ENC_ENABLED is not set +# end of Security features + +# +# Serial flasher config +# +CONFIG_ESPTOOLPY_BAUD_OTHER_VAL=115200 +# CONFIG_ESPTOOLPY_NO_STUB is not set +# CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set +# CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set +CONFIG_ESPTOOLPY_FLASHMODE_DIO=y +# CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set +CONFIG_ESPTOOLPY_FLASHMODE="dio" +# CONFIG_ESPTOOLPY_FLASHFREQ_80M is not set +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +# CONFIG_ESPTOOLPY_FLASHFREQ_26M is not set +# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set +CONFIG_ESPTOOLPY_FLASHFREQ="40m" +# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y +# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE="2MB" +CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=y +CONFIG_ESPTOOLPY_BEFORE_RESET=y +# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set +CONFIG_ESPTOOLPY_BEFORE="default_reset" +CONFIG_ESPTOOLPY_AFTER_RESET=y +# CONFIG_ESPTOOLPY_AFTER_NORESET is not set +CONFIG_ESPTOOLPY_AFTER="hard_reset" +# CONFIG_ESPTOOLPY_MONITOR_BAUD_CONSOLE is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_9600B is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_57600B is not set +CONFIG_ESPTOOLPY_MONITOR_BAUD_115200B=y +# CONFIG_ESPTOOLPY_MONITOR_BAUD_230400B is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_921600B is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_2MB is not set +# CONFIG_ESPTOOLPY_MONITOR_BAUD_OTHER is not set +CONFIG_ESPTOOLPY_MONITOR_BAUD_OTHER_VAL=115200 +CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 +# end of Serial flasher config + +# +# Partition Table +# +CONFIG_PARTITION_TABLE_SINGLE_APP=y +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +# CONFIG_PARTITION_TABLE_CUSTOM is not set +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp.csv" +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +CONFIG_PARTITION_TABLE_MD5=y +# end of Partition Table + +# +# IRBLASTER Configuration +# + +# +# Pin mapping +# +CONFIG_PIN_I2C_SDA0=18 +CONFIG_PIN_I2C_SCL0=19 +# end of Pin mapping + +# +# Console +# +CONFIG_CONSOLE_TELNET_PORT=23 +CONFIG_CONSOLE_PW_LEN=32 +# end of Console +# end of IRBLASTER Configuration + +# +# Compiler options +# +CONFIG_COMPILER_OPTIMIZATION_DEFAULT=y +# CONFIG_COMPILER_OPTIMIZATION_SIZE is not set +# CONFIG_COMPILER_OPTIMIZATION_PERF is not set +# CONFIG_COMPILER_OPTIMIZATION_NONE is not set +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set +# CONFIG_COMPILER_CXX_EXCEPTIONS is not set +# CONFIG_COMPILER_CXX_RTTI is not set +CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y +# CONFIG_COMPILER_STACK_CHECK_MODE_NORM is not set +# CONFIG_COMPILER_STACK_CHECK_MODE_STRONG is not set +# CONFIG_COMPILER_STACK_CHECK_MODE_ALL is not set +# CONFIG_COMPILER_WARN_WRITE_STRINGS is not set +# CONFIG_COMPILER_DISABLE_GCC8_WARNINGS is not set +# CONFIG_COMPILER_DUMP_RTL_FILES is not set +# end of Compiler options + +# +# Component config +# + +# +# Application Level Tracing +# +# CONFIG_APPTRACE_DEST_TRAX is not set +CONFIG_APPTRACE_DEST_NONE=y +CONFIG_APPTRACE_LOCK_ENABLE=y +# end of Application Level Tracing + +# +# ESP-ASIO +# +# CONFIG_ASIO_SSL_SUPPORT is not set +# end of ESP-ASIO + +# +# Bluetooth +# +# CONFIG_BT_ENABLED is not set +CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_EFF=0 +CONFIG_BTDM_CTRL_PCM_ROLE_EFF=0 +CONFIG_BTDM_CTRL_PCM_POLAR_EFF=0 +CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF=0 +CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN_EFF=0 +CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN_EFF=0 +CONFIG_BTDM_CTRL_PINNED_TO_CORE=0 +CONFIG_BTDM_BLE_SLEEP_CLOCK_ACCURACY_INDEX_EFF=1 +CONFIG_BT_CTRL_MODE_EFF=1 +CONFIG_BT_CTRL_BLE_MAX_ACT=10 +CONFIG_BT_CTRL_BLE_MAX_ACT_EFF=10 +CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=0 +CONFIG_BT_CTRL_PINNED_TO_CORE=0 +CONFIG_BT_CTRL_HCI_TL=1 +CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=30 +CONFIG_BT_CTRL_HW_CCA_EFF=0 +CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF=0 +CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y +CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100 +CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 +CONFIG_BT_CTRL_BLE_SCAN_DUPL=y +CONFIG_BT_CTRL_SCAN_DUPL_TYPE=0 +CONFIG_BT_CTRL_SCAN_DUPL_CACHE_SIZE=100 +CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_EFF=0 +CONFIG_BT_CTRL_SLEEP_MODE_EFF=0 +CONFIG_BT_CTRL_SLEEP_CLOCK_EFF=0 +CONFIG_BT_CTRL_HCI_TL_EFF=1 +CONFIG_BT_RESERVE_DRAM=0 +CONFIG_BT_NIMBLE_USE_ESP_TIMER=y +# end of Bluetooth + +# +# CoAP Configuration +# +CONFIG_COAP_MBEDTLS_PSK=y +# CONFIG_COAP_MBEDTLS_PKI is not set +# CONFIG_COAP_MBEDTLS_DEBUG is not set +CONFIG_COAP_LOG_DEFAULT_LEVEL=0 +# end of CoAP Configuration + +# +# Driver configurations +# + +# +# ADC configuration +# +# CONFIG_ADC_FORCE_XPD_FSM is not set +CONFIG_ADC_DISABLE_DAC=y +# end of ADC configuration + +# +# SPI configuration +# +# CONFIG_SPI_MASTER_IN_IRAM is not set +CONFIG_SPI_MASTER_ISR_IN_IRAM=y +# CONFIG_SPI_SLAVE_IN_IRAM is not set +CONFIG_SPI_SLAVE_ISR_IN_IRAM=y +# end of SPI configuration + +# +# TWAI configuration +# +# CONFIG_TWAI_ISR_IN_IRAM is not set +# CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC is not set +# CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST is not set +# CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID is not set +# CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT is not set +# end of TWAI configuration + +# +# UART configuration +# +# CONFIG_UART_ISR_IN_IRAM is not set +# end of UART configuration + +# +# RTCIO configuration +# +# CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC is not set +# end of RTCIO configuration + +# +# GPIO Configuration +# +# CONFIG_GPIO_ESP32_SUPPORT_SWITCH_SLP_PULL is not set +# end of GPIO Configuration +# end of Driver configurations + +# +# eFuse Bit Manager +# +# CONFIG_EFUSE_CUSTOM_TABLE is not set +# CONFIG_EFUSE_VIRTUAL is not set +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_NONE is not set +CONFIG_EFUSE_CODE_SCHEME_COMPAT_3_4=y +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_REPEAT is not set +CONFIG_EFUSE_MAX_BLK_LEN=192 +# end of eFuse Bit Manager + +# +# ESP-TLS +# +CONFIG_ESP_TLS_USING_MBEDTLS=y +# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set +# CONFIG_ESP_TLS_SERVER is not set +# CONFIG_ESP_TLS_PSK_VERIFICATION is not set +# CONFIG_ESP_TLS_INSECURE is not set +# end of ESP-TLS + +# +# ESP32-specific +# +CONFIG_ESP32_REV_MIN_0=y +# CONFIG_ESP32_REV_MIN_1 is not set +# CONFIG_ESP32_REV_MIN_2 is not set +# CONFIG_ESP32_REV_MIN_3 is not set +CONFIG_ESP32_REV_MIN=0 +CONFIG_ESP32_DPORT_WORKAROUND=y +# CONFIG_ESP32_DEFAULT_CPU_FREQ_80 is not set +CONFIG_ESP32_DEFAULT_CPU_FREQ_160=y +# CONFIG_ESP32_DEFAULT_CPU_FREQ_240 is not set +CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ=160 +# CONFIG_ESP32_SPIRAM_SUPPORT is not set +# CONFIG_ESP32_TRAX is not set +CONFIG_ESP32_TRACEMEM_RESERVE_DRAM=0x0 +# CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_TWO is not set +CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_FOUR=y +CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES=4 +# CONFIG_ESP32_ULP_COPROC_ENABLED is not set +CONFIG_ESP32_ULP_COPROC_RESERVE_MEM=0 +CONFIG_ESP32_DEBUG_OCDAWARE=y +CONFIG_ESP32_BROWNOUT_DET=y +CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_0=y +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_7 is not set +CONFIG_ESP32_BROWNOUT_DET_LVL=0 +CONFIG_ESP32_REDUCE_PHY_TX_POWER=y +CONFIG_ESP32_TIME_SYSCALL_USE_RTC_FRC1=y +# CONFIG_ESP32_TIME_SYSCALL_USE_RTC is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_FRC1 is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_NONE is not set +CONFIG_ESP32_RTC_CLK_SRC_INT_RC=y +# CONFIG_ESP32_RTC_CLK_SRC_EXT_CRYS is not set +# CONFIG_ESP32_RTC_CLK_SRC_EXT_OSC is not set +# CONFIG_ESP32_RTC_CLK_SRC_INT_8MD256 is not set +CONFIG_ESP32_RTC_CLK_CAL_CYCLES=1024 +CONFIG_ESP32_DEEP_SLEEP_WAKEUP_DELAY=2000 +CONFIG_ESP32_XTAL_FREQ_40=y +# CONFIG_ESP32_XTAL_FREQ_26 is not set +# CONFIG_ESP32_XTAL_FREQ_AUTO is not set +CONFIG_ESP32_XTAL_FREQ=40 +# CONFIG_ESP32_DISABLE_BASIC_ROM_CONSOLE is not set +# CONFIG_ESP32_NO_BLOBS is not set +# CONFIG_ESP32_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +# CONFIG_ESP32_COMPATIBLE_PRE_V3_1_BOOTLOADERS is not set +# CONFIG_ESP32_USE_FIXED_STATIC_RAM_SIZE is not set +CONFIG_ESP32_DPORT_DIS_INTERRUPT_LVL=5 +# end of ESP32-specific + +# +# ADC-Calibration +# +CONFIG_ADC_CAL_EFUSE_TP_ENABLE=y +CONFIG_ADC_CAL_EFUSE_VREF_ENABLE=y +CONFIG_ADC_CAL_LUT_ENABLE=y +# end of ADC-Calibration + +# +# Common ESP-related +# +CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584 +CONFIG_ESP_IPC_TASK_STACK_SIZE=1024 +CONFIG_ESP_IPC_USES_CALLERS_PRIORITY=y +CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048 +CONFIG_ESP_CONSOLE_UART_DEFAULT=y +# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set +# CONFIG_ESP_CONSOLE_NONE is not set +CONFIG_ESP_CONSOLE_UART=y +CONFIG_ESP_CONSOLE_MULTIPLE_UART=y +CONFIG_ESP_CONSOLE_UART_NUM=0 +CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 +CONFIG_ESP_INT_WDT=y +CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 +CONFIG_ESP_INT_WDT_CHECK_CPU1=y +CONFIG_ESP_TASK_WDT=y +# CONFIG_ESP_TASK_WDT_PANIC is not set +CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y +# CONFIG_ESP_PANIC_HANDLER_IRAM is not set +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_STA=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_AP=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_BT=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_ETH=y +# end of Common ESP-related + +# +# Ethernet +# +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_INTERFACE_RMII=y +# CONFIG_ETH_PHY_INTERFACE_MII is not set +CONFIG_ETH_RMII_CLK_INPUT=y +# CONFIG_ETH_RMII_CLK_OUTPUT is not set +CONFIG_ETH_RMII_CLK_IN_GPIO=0 +CONFIG_ETH_DMA_BUFFER_SIZE=512 +CONFIG_ETH_DMA_RX_BUFFER_NUM=10 +CONFIG_ETH_DMA_TX_BUFFER_NUM=10 +CONFIG_ETH_USE_SPI_ETHERNET=y +# CONFIG_ETH_SPI_ETHERNET_DM9051 is not set +# CONFIG_ETH_SPI_ETHERNET_W5500 is not set +# CONFIG_ETH_USE_OPENETH is not set +# end of Ethernet + +# +# Event Loop Library +# +# CONFIG_ESP_EVENT_LOOP_PROFILING is not set +CONFIG_ESP_EVENT_POST_FROM_ISR=y +CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR=y +# end of Event Loop Library + +# +# GDB Stub +# +# end of GDB Stub + +# +# ESP HTTP client +# +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +# CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH is not set +# end of ESP HTTP client + +# +# HTTP Server +# +CONFIG_HTTPD_MAX_REQ_HDR_LEN=512 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +# CONFIG_HTTPD_LOG_PURGE_DATA is not set +# CONFIG_HTTPD_WS_SUPPORT is not set +# end of HTTP Server + +# +# ESP HTTPS OTA +# +# CONFIG_OTA_ALLOW_HTTP is not set +# end of ESP HTTPS OTA + +# +# ESP HTTPS server +# +# CONFIG_ESP_HTTPS_SERVER_ENABLE is not set +# end of ESP HTTPS server + +# +# ESP NETIF Adapter +# +CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 +CONFIG_ESP_NETIF_TCPIP_LWIP=y +# CONFIG_ESP_NETIF_LOOPBACK is not set +CONFIG_ESP_NETIF_TCPIP_ADAPTER_COMPATIBLE_LAYER=y +# end of ESP NETIF Adapter + +# +# Power Management +# +# CONFIG_PM_ENABLE is not set +# end of Power Management + +# +# ESP System Settings +# +# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set +CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y +# CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set +CONFIG_ESP_SYSTEM_PD_FLASH=y + +# +# Memory protection +# +# end of Memory protection +# end of ESP System Settings + +# +# High resolution timer (esp_timer) +# +# CONFIG_ESP_TIMER_PROFILING is not set +CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y +CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y +CONFIG_ESP_TIMER_TASK_STACK_SIZE=3584 +# CONFIG_ESP_TIMER_IMPL_FRC2 is not set +CONFIG_ESP_TIMER_IMPL_TG0_LAC=y +# end of High resolution timer (esp_timer) + +# +# Wi-Fi +# +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 +CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_CSI_ENABLED is not set +CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP32_WIFI_TX_BA_WIN=6 +CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP32_WIFI_RX_BA_WIN=6 +CONFIG_ESP32_WIFI_NVS_ENABLED=y +CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y +# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set +CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 +# CONFIG_WIFI_LOG_DEFAULT_LEVEL_NONE is not set +# CONFIG_WIFI_LOG_DEFAULT_LEVEL_ERROR is not set +# CONFIG_WIFI_LOG_DEFAULT_LEVEL_WARN is not set +CONFIG_WIFI_LOG_DEFAULT_LEVEL_INFO=y +# CONFIG_WIFI_LOG_DEFAULT_LEVEL_DEBUG is not set +# CONFIG_WIFI_LOG_DEFAULT_LEVEL_VERBOSE is not set +CONFIG_ESP32_WIFI_IRAM_OPT=y +CONFIG_ESP32_WIFI_RX_IRAM_OPT=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y +# CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set +# CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE is not set +# end of Wi-Fi + +# +# PHY +# +CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP32_PHY_MAX_TX_POWER=20 +# end of PHY + +# +# Core dump +# +# CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH is not set +# CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set +CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y +# end of Core dump + +# +# FAT Filesystem support +# +# CONFIG_FATFS_CODEPAGE_DYNAMIC is not set +CONFIG_FATFS_CODEPAGE_437=y +# CONFIG_FATFS_CODEPAGE_720 is not set +# CONFIG_FATFS_CODEPAGE_737 is not set +# CONFIG_FATFS_CODEPAGE_771 is not set +# CONFIG_FATFS_CODEPAGE_775 is not set +# CONFIG_FATFS_CODEPAGE_850 is not set +# CONFIG_FATFS_CODEPAGE_852 is not set +# CONFIG_FATFS_CODEPAGE_855 is not set +# CONFIG_FATFS_CODEPAGE_857 is not set +# CONFIG_FATFS_CODEPAGE_860 is not set +# CONFIG_FATFS_CODEPAGE_861 is not set +# CONFIG_FATFS_CODEPAGE_862 is not set +# CONFIG_FATFS_CODEPAGE_863 is not set +# CONFIG_FATFS_CODEPAGE_864 is not set +# CONFIG_FATFS_CODEPAGE_865 is not set +# CONFIG_FATFS_CODEPAGE_866 is not set +# CONFIG_FATFS_CODEPAGE_869 is not set +# CONFIG_FATFS_CODEPAGE_932 is not set +# CONFIG_FATFS_CODEPAGE_936 is not set +# CONFIG_FATFS_CODEPAGE_949 is not set +# CONFIG_FATFS_CODEPAGE_950 is not set +CONFIG_FATFS_CODEPAGE=437 +CONFIG_FATFS_LFN_NONE=y +# CONFIG_FATFS_LFN_HEAP is not set +# CONFIG_FATFS_LFN_STACK is not set +CONFIG_FATFS_FS_LOCK=0 +CONFIG_FATFS_TIMEOUT_MS=10000 +CONFIG_FATFS_PER_FILE_CACHE=y +# CONFIG_FATFS_USE_FASTSEEK is not set +# end of FAT Filesystem support + +# +# Modbus configuration +# +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=502 +CONFIG_FMB_TCP_PORT_MAX_CONN=5 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=150 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=200 +CONFIG_FMB_QUEUE_LENGTH=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_SERIAL_BUF_SIZE=256 +CONFIG_FMB_SERIAL_ASCII_BITS_PER_SYMB=8 +CONFIG_FMB_SERIAL_ASCII_TIMEOUT_RESPOND_MS=1000 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT=y +CONFIG_FMB_CONTROLLER_SLAVE_ID=0x00112233 +CONFIG_FMB_CONTROLLER_NOTIFY_TIMEOUT=20 +CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE=20 +CONFIG_FMB_CONTROLLER_STACK_SIZE=4096 +CONFIG_FMB_EVENT_QUEUE_TIMEOUT=20 +CONFIG_FMB_TIMER_PORT_ENABLED=y +CONFIG_FMB_TIMER_GROUP=0 +CONFIG_FMB_TIMER_INDEX=0 +# CONFIG_FMB_TIMER_ISR_IN_IRAM is not set +# end of Modbus configuration + +# +# FreeRTOS +# +# CONFIG_FREERTOS_UNICORE is not set +CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF +CONFIG_FREERTOS_CORETIMER_0=y +# CONFIG_FREERTOS_CORETIMER_1 is not set +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION=y +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set +CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y +# CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is not set +CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y +CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1 +CONFIG_FREERTOS_ASSERT_FAIL_ABORT=y +# CONFIG_FREERTOS_ASSERT_FAIL_PRINT_CONTINUE is not set +# CONFIG_FREERTOS_ASSERT_DISABLE is not set +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=2304 +CONFIG_FREERTOS_ISR_STACKSIZE=1536 +# CONFIG_FREERTOS_LEGACY_HOOKS is not set +CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16 +CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y +# CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP is not set +CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10 +CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 +CONFIG_FREERTOS_USE_TRACE_FACILITY=y +CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y +CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y +# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set +CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=y +CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=y +# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set +# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set +CONFIG_FREERTOS_DEBUG_OCDAWARE=y +# CONFIG_FREERTOS_FPU_IN_ISR is not set +# end of FreeRTOS + +# +# Heap memory debugging +# +CONFIG_HEAP_POISONING_DISABLED=y +# CONFIG_HEAP_POISONING_LIGHT is not set +# CONFIG_HEAP_POISONING_COMPREHENSIVE is not set +CONFIG_HEAP_TRACING_OFF=y +# CONFIG_HEAP_TRACING_STANDALONE is not set +# CONFIG_HEAP_TRACING_TOHOST is not set +# CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS is not set +# end of Heap memory debugging + +# +# jsmn +# +# CONFIG_JSMN_PARENT_LINKS is not set +# CONFIG_JSMN_STRICT is not set +# end of jsmn + +# +# libsodium +# +# end of libsodium + +# +# Log output +# +# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set +# CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set +# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set +# CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LOG_COLORS=y +CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y +# CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set +# end of Log output + +# +# LWIP +# +CONFIG_LWIP_LOCAL_HOSTNAME="espressif" +CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y +# CONFIG_LWIP_L2_TO_L3_COPY is not set +# CONFIG_LWIP_IRAM_OPTIMIZATION is not set +CONFIG_LWIP_TIMERS_ONDEMAND=y +CONFIG_LWIP_MAX_SOCKETS=10 +# CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set +# CONFIG_LWIP_SO_LINGER is not set +CONFIG_LWIP_SO_REUSE=y +CONFIG_LWIP_SO_REUSE_RXTOALL=y +# CONFIG_LWIP_SO_RCVBUF is not set +# CONFIG_LWIP_NETBUF_RECVINFO is not set +CONFIG_LWIP_IP4_FRAG=y +CONFIG_LWIP_IP6_FRAG=y +# CONFIG_LWIP_IP4_REASSEMBLY is not set +# CONFIG_LWIP_IP6_REASSEMBLY is not set +# CONFIG_LWIP_IP_FORWARD is not set +# CONFIG_LWIP_STATS is not set +# CONFIG_LWIP_ETHARP_TRUST_IP_MAC is not set +CONFIG_LWIP_ESP_GRATUITOUS_ARP=y +CONFIG_LWIP_GARP_TMR_INTERVAL=60 +CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 +CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y +# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set +# CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set + +# +# DHCP server +# +CONFIG_LWIP_DHCPS=y +CONFIG_LWIP_DHCPS_LEASE_UNIT=60 +CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8 +# end of DHCP server + +# CONFIG_LWIP_AUTOIP is not set +CONFIG_LWIP_IPV6=y +# CONFIG_LWIP_IPV6_AUTOCONFIG is not set +CONFIG_LWIP_NETIF_LOOPBACK=y +CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 + +# +# TCP +# +CONFIG_LWIP_MAX_ACTIVE_TCP=16 +CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y +CONFIG_LWIP_TCP_MAXRTX=12 +CONFIG_LWIP_TCP_SYNMAXRTX=12 +CONFIG_LWIP_TCP_MSS=1440 +CONFIG_LWIP_TCP_TMR_INTERVAL=250 +CONFIG_LWIP_TCP_MSL=60000 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 +CONFIG_LWIP_TCP_WND_DEFAULT=5744 +CONFIG_LWIP_TCP_RECVMBOX_SIZE=6 +CONFIG_LWIP_TCP_QUEUE_OOSEQ=y +# CONFIG_LWIP_TCP_SACK_OUT is not set +# CONFIG_LWIP_TCP_KEEP_CONNECTION_WHEN_IP_CHANGES is not set +CONFIG_LWIP_TCP_OVERSIZE_MSS=y +# CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set +CONFIG_LWIP_TCP_RTO_TIME=1500 +# end of TCP + +# +# UDP +# +CONFIG_LWIP_MAX_UDP_PCBS=16 +CONFIG_LWIP_UDP_RECVMBOX_SIZE=6 +# end of UDP + +# +# Checksums +# +# CONFIG_LWIP_CHECKSUM_CHECK_IP is not set +# CONFIG_LWIP_CHECKSUM_CHECK_UDP is not set +CONFIG_LWIP_CHECKSUM_CHECK_ICMP=y +# end of Checksums + +CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=3072 +CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0 is not set +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x7FFFFFFF +# CONFIG_LWIP_PPP_SUPPORT is not set +CONFIG_LWIP_IPV6_MEMP_NUM_ND6_QUEUE=3 +CONFIG_LWIP_IPV6_ND6_NUM_NEIGHBORS=5 +# CONFIG_LWIP_SLIP_SUPPORT is not set + +# +# ICMP +# +CONFIG_LWIP_ICMP=y +# CONFIG_LWIP_MULTICAST_PING is not set +# CONFIG_LWIP_BROADCAST_PING is not set +# end of ICMP + +# +# LWIP RAW API +# +CONFIG_LWIP_MAX_RAW_PCBS=16 +# end of LWIP RAW API + +# +# SNTP +# +CONFIG_LWIP_DHCP_MAX_NTP_SERVERS=1 +CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 +# end of SNTP + +CONFIG_LWIP_ESP_LWIP_ASSERT=y + +# +# Hooks +# +# CONFIG_LWIP_HOOK_TCP_ISN_NONE is not set +CONFIG_LWIP_HOOK_TCP_ISN_DEFAULT=y +# CONFIG_LWIP_HOOK_TCP_ISN_CUSTOM is not set +CONFIG_LWIP_HOOK_IP6_ROUTE_NONE=y +# CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT is not set +# CONFIG_LWIP_HOOK_IP6_ROUTE_CUSTOM is not set +CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set +# end of Hooks + +# CONFIG_LWIP_DEBUG is not set +# end of LWIP + +# +# mbedTLS +# +CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y +# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set +# CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set +CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y +CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 +CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 +# CONFIG_MBEDTLS_DYNAMIC_BUFFER is not set +# CONFIG_MBEDTLS_DEBUG is not set + +# +# Certificate Bundle +# +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN is not set +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE is not set +# CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE is not set +# end of Certificate Bundle + +# CONFIG_MBEDTLS_ECP_RESTARTABLE is not set +# CONFIG_MBEDTLS_CMAC_C is not set +CONFIG_MBEDTLS_HARDWARE_AES=y +CONFIG_MBEDTLS_HARDWARE_MPI=y +CONFIG_MBEDTLS_HARDWARE_SHA=y +CONFIG_MBEDTLS_ROM_MD5=y +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN is not set +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY is not set +CONFIG_MBEDTLS_HAVE_TIME=y +# CONFIG_MBEDTLS_HAVE_TIME_DATE is not set +CONFIG_MBEDTLS_ECDSA_DETERMINISTIC=y +CONFIG_MBEDTLS_SHA512_C=y +CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y +# CONFIG_MBEDTLS_TLS_SERVER_ONLY is not set +# CONFIG_MBEDTLS_TLS_CLIENT_ONLY is not set +# CONFIG_MBEDTLS_TLS_DISABLED is not set +CONFIG_MBEDTLS_TLS_SERVER=y +CONFIG_MBEDTLS_TLS_CLIENT=y +CONFIG_MBEDTLS_TLS_ENABLED=y + +# +# TLS Key Exchange Methods +# +# CONFIG_MBEDTLS_PSK_MODES is not set +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_DHE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=y +# end of TLS Key Exchange Methods + +CONFIG_MBEDTLS_SSL_RENEGOTIATION=y +# CONFIG_MBEDTLS_SSL_PROTO_SSL3 is not set +CONFIG_MBEDTLS_SSL_PROTO_TLS1=y +CONFIG_MBEDTLS_SSL_PROTO_TLS1_1=y +CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y +# CONFIG_MBEDTLS_SSL_PROTO_DTLS is not set +CONFIG_MBEDTLS_SSL_ALPN=y +CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS=y +CONFIG_MBEDTLS_X509_CHECK_KEY_USAGE=y +CONFIG_MBEDTLS_X509_CHECK_EXTENDED_KEY_USAGE=y +CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS=y + +# +# Symmetric Ciphers +# +CONFIG_MBEDTLS_AES_C=y +# CONFIG_MBEDTLS_CAMELLIA_C is not set +# CONFIG_MBEDTLS_DES_C is not set +CONFIG_MBEDTLS_RC4_DISABLED=y +# CONFIG_MBEDTLS_RC4_ENABLED_NO_DEFAULT is not set +# CONFIG_MBEDTLS_RC4_ENABLED is not set +# CONFIG_MBEDTLS_BLOWFISH_C is not set +# CONFIG_MBEDTLS_XTEA_C is not set +CONFIG_MBEDTLS_CCM_C=y +CONFIG_MBEDTLS_GCM_C=y +# CONFIG_MBEDTLS_NIST_KW_C is not set +# end of Symmetric Ciphers + +# CONFIG_MBEDTLS_RIPEMD160_C is not set + +# +# Certificates +# +CONFIG_MBEDTLS_PEM_PARSE_C=y +CONFIG_MBEDTLS_PEM_WRITE_C=y +CONFIG_MBEDTLS_X509_CRL_PARSE_C=y +CONFIG_MBEDTLS_X509_CSR_PARSE_C=y +# end of Certificates + +CONFIG_MBEDTLS_ECP_C=y +CONFIG_MBEDTLS_ECDH_C=y +CONFIG_MBEDTLS_ECDSA_C=y +# CONFIG_MBEDTLS_ECJPAKE_C is not set +CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y +CONFIG_MBEDTLS_ECP_NIST_OPTIM=y +# CONFIG_MBEDTLS_POLY1305_C is not set +# CONFIG_MBEDTLS_CHACHA20_C is not set +# CONFIG_MBEDTLS_HKDF_C is not set +# CONFIG_MBEDTLS_THREADING_C is not set +# CONFIG_MBEDTLS_LARGE_KEY_SOFTWARE_MPI is not set +# CONFIG_MBEDTLS_SECURITY_RISKS is not set +# end of mbedTLS + +# +# mDNS +# +CONFIG_MDNS_MAX_SERVICES=10 +CONFIG_MDNS_TASK_PRIORITY=1 +CONFIG_MDNS_TASK_STACK_SIZE=4096 +# CONFIG_MDNS_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_MDNS_TASK_AFFINITY_CPU0=y +# CONFIG_MDNS_TASK_AFFINITY_CPU1 is not set +CONFIG_MDNS_TASK_AFFINITY=0x0 +CONFIG_MDNS_SERVICE_ADD_TIMEOUT_MS=2000 +# CONFIG_MDNS_STRICT_MODE is not set +CONFIG_MDNS_TIMER_PERIOD_MS=100 +# end of mDNS + +# +# ESP-MQTT Configurations +# +CONFIG_MQTT_PROTOCOL_311=y +CONFIG_MQTT_TRANSPORT_SSL=y +CONFIG_MQTT_TRANSPORT_WEBSOCKET=y +CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y +# CONFIG_MQTT_MSG_ID_INCREMENTAL is not set +# CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set +# CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set +# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set +# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set +# CONFIG_MQTT_CUSTOM_OUTBOX is not set +# end of ESP-MQTT Configurations + +# +# Newlib +# +CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set +CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y +# CONFIG_NEWLIB_NANO_FORMAT is not set +# end of Newlib + +# +# NVS +# +# end of NVS + +# +# OpenSSL +# +# CONFIG_OPENSSL_DEBUG is not set +CONFIG_OPENSSL_ERROR_STACK=y +# CONFIG_OPENSSL_ASSERT_DO_NOTHING is not set +CONFIG_OPENSSL_ASSERT_EXIT=y +# end of OpenSSL + +# +# PThreads +# +CONFIG_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_PTHREAD_STACK_MIN=768 +CONFIG_PTHREAD_DEFAULT_CORE_NO_AFFINITY=y +# CONFIG_PTHREAD_DEFAULT_CORE_0 is not set +# CONFIG_PTHREAD_DEFAULT_CORE_1 is not set +CONFIG_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_PTHREAD_TASK_NAME_DEFAULT="pthread" +# end of PThreads + +# +# SPI Flash driver +# +# CONFIG_SPI_FLASH_VERIFY_WRITE is not set +# CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set +CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y +CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS=y +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS is not set +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED is not set +# CONFIG_SPI_FLASH_USE_LEGACY_IMPL is not set +# CONFIG_SPI_FLASH_SHARE_SPI1_BUS is not set +# CONFIG_SPI_FLASH_BYPASS_BLOCK_ERASE is not set +CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y +CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=20 +CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=1 +CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=8192 +# CONFIG_SPI_FLASH_SIZE_OVERRIDE is not set +# CONFIG_SPI_FLASH_CHECK_ERASE_TIMEOUT_DISABLED is not set + +# +# Auto-detect flash chips +# +CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_GD_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP=y +# end of Auto-detect flash chips + +CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE=y +# end of SPI Flash driver + +# +# SPIFFS Configuration +# +CONFIG_SPIFFS_MAX_PARTITIONS=3 + +# +# SPIFFS Cache Configuration +# +CONFIG_SPIFFS_CACHE=y +CONFIG_SPIFFS_CACHE_WR=y +# CONFIG_SPIFFS_CACHE_STATS is not set +# end of SPIFFS Cache Configuration + +CONFIG_SPIFFS_PAGE_CHECK=y +CONFIG_SPIFFS_GC_MAX_RUNS=10 +# CONFIG_SPIFFS_GC_STATS is not set +CONFIG_SPIFFS_PAGE_SIZE=256 +CONFIG_SPIFFS_OBJ_NAME_LEN=32 +# CONFIG_SPIFFS_FOLLOW_SYMLINKS is not set +CONFIG_SPIFFS_USE_MAGIC=y +CONFIG_SPIFFS_USE_MAGIC_LENGTH=y +CONFIG_SPIFFS_META_LENGTH=4 +CONFIG_SPIFFS_USE_MTIME=y + +# +# Debug Configuration +# +# CONFIG_SPIFFS_DBG is not set +# CONFIG_SPIFFS_API_DBG is not set +# CONFIG_SPIFFS_GC_DBG is not set +# CONFIG_SPIFFS_CACHE_DBG is not set +# CONFIG_SPIFFS_CHECK_DBG is not set +# CONFIG_SPIFFS_TEST_VISUALISATION is not set +# end of Debug Configuration +# end of SPIFFS Configuration + +# +# TCP Transport +# + +# +# Websocket +# +CONFIG_WS_TRANSPORT=y +CONFIG_WS_BUFFER_SIZE=1024 +# end of Websocket +# end of TCP Transport + +# +# TinyUSB +# +# end of TinyUSB + +# +# Unity unit testing library +# +CONFIG_UNITY_ENABLE_FLOAT=y +CONFIG_UNITY_ENABLE_DOUBLE=y +# CONFIG_UNITY_ENABLE_COLOR is not set +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y +# CONFIG_UNITY_ENABLE_FIXTURE is not set +# CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL is not set +# end of Unity unit testing library + +# +# Virtual file system +# +CONFIG_VFS_SUPPORT_IO=y +CONFIG_VFS_SUPPORT_DIR=y +CONFIG_VFS_SUPPORT_SELECT=y +CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y +CONFIG_VFS_SUPPORT_TERMIOS=y + +# +# Host File System I/O (Semihosting) +# +CONFIG_VFS_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +CONFIG_VFS_SEMIHOSTFS_HOST_PATH_MAX_LEN=128 +# end of Host File System I/O (Semihosting) +# end of Virtual file system + +# +# Wear Levelling +# +# CONFIG_WL_SECTOR_SIZE_512 is not set +CONFIG_WL_SECTOR_SIZE_4096=y +CONFIG_WL_SECTOR_SIZE=4096 +# end of Wear Levelling + +# +# Wi-Fi Provisioning Manager +# +CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 +CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 +# end of Wi-Fi Provisioning Manager + +# +# Supplicant +# +CONFIG_WPA_MBEDTLS_CRYPTO=y +# CONFIG_WPA_WAPI_PSK is not set +# CONFIG_WPA_DEBUG_PRINT is not set +# CONFIG_WPA_TESTING_OPTIONS is not set +# CONFIG_WPA_WPS_STRICT is not set +# CONFIG_WPA_11KV_SUPPORT is not set +# end of Supplicant + +# +# DHCP watchdog +# +CONFIG_DHCPWD_PERIOD_GW_PING_S=60 +CONFIG_DHCPWD_GETIP_TIMEOUT_S=10 +CONFIG_DHCPWD_TASK_STACK_SIZE=4096 +CONFIG_DHCPWD_TASK_PRIORITY=3 +# end of DHCP watchdog +# end of Component config + +# +# Compatibility options +# +# CONFIG_LEGACY_INCLUDE_COMMON_HEADERS is not set +# end of Compatibility options + +# Deprecated options for backward compatibility +CONFIG_TOOLPREFIX="xtensa-esp32-elf-" +# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set +CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y +# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set +CONFIG_LOG_BOOTLOADER_LEVEL=3 +# CONFIG_APP_ROLLBACK_ENABLE is not set +# CONFIG_FLASH_ENCRYPTION_ENABLED is not set +# CONFIG_FLASHMODE_QIO is not set +# CONFIG_FLASHMODE_QOUT is not set +CONFIG_FLASHMODE_DIO=y +# CONFIG_FLASHMODE_DOUT is not set +# CONFIG_MONITOR_BAUD_9600B is not set +# CONFIG_MONITOR_BAUD_57600B is not set +CONFIG_MONITOR_BAUD_115200B=y +# CONFIG_MONITOR_BAUD_230400B is not set +# CONFIG_MONITOR_BAUD_921600B is not set +# CONFIG_MONITOR_BAUD_2MB is not set +# CONFIG_MONITOR_BAUD_OTHER is not set +CONFIG_MONITOR_BAUD_OTHER_VAL=115200 +CONFIG_MONITOR_BAUD=115200 +CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y +# CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set +CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y +# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set +# CONFIG_CXX_EXCEPTIONS is not set +CONFIG_STACK_CHECK_NONE=y +# CONFIG_STACK_CHECK_NORM is not set +# CONFIG_STACK_CHECK_STRONG is not set +# CONFIG_STACK_CHECK_ALL is not set +# CONFIG_WARN_WRITE_STRINGS is not set +# CONFIG_DISABLE_GCC8_WARNINGS is not set +# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set +CONFIG_ESP32_APPTRACE_DEST_NONE=y +CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y +CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN_EFF=0 +CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN_EFF=0 +CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN_EFF=0 +CONFIG_BTDM_CONTROLLER_PINNED_TO_CORE=0 +CONFIG_ADC2_DISABLE_DAC=y +# CONFIG_SPIRAM_SUPPORT is not set +CONFIG_TRACEMEM_RESERVE_DRAM=0x0 +# CONFIG_TWO_UNIVERSAL_MAC_ADDRESS is not set +CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y +CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4 +# CONFIG_ULP_COPROC_ENABLED is not set +CONFIG_ULP_COPROC_RESERVE_MEM=0 +CONFIG_BROWNOUT_DET=y +CONFIG_BROWNOUT_DET_LVL_SEL_0=y +# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_7 is not set +CONFIG_BROWNOUT_DET_LVL=0 +CONFIG_REDUCE_PHY_TX_POWER=y +CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_RC=y +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_CRYSTAL is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_OSC is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_8MD256 is not set +# CONFIG_DISABLE_BASIC_ROM_CONSOLE is not set +# CONFIG_NO_BLOBS is not set +# CONFIG_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_MAIN_TASK_STACK_SIZE=3584 +CONFIG_IPC_TASK_STACK_SIZE=1024 +CONFIG_CONSOLE_UART_DEFAULT=y +# CONFIG_CONSOLE_UART_CUSTOM is not set +# CONFIG_ESP_CONSOLE_UART_NONE is not set +CONFIG_CONSOLE_UART=y +CONFIG_CONSOLE_UART_NUM=0 +CONFIG_CONSOLE_UART_BAUDRATE=115200 +CONFIG_INT_WDT=y +CONFIG_INT_WDT_TIMEOUT_MS=300 +CONFIG_INT_WDT_CHECK_CPU1=y +CONFIG_TASK_WDT=y +# CONFIG_TASK_WDT_PANIC is not set +CONFIG_TASK_WDT_TIMEOUT_S=5 +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y +# CONFIG_EVENT_LOOP_PROFILING is not set +CONFIG_POST_EVENTS_FROM_ISR=y +CONFIG_POST_EVENTS_FROM_IRAM_ISR=y +# CONFIG_ESP32S2_PANIC_PRINT_HALT is not set +CONFIG_ESP32S2_PANIC_PRINT_REBOOT=y +# CONFIG_ESP32S2_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP32S2_PANIC_GDBSTUB is not set +CONFIG_TIMER_TASK_STACK_SIZE=3584 +# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set +CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y +CONFIG_MB_MASTER_TIMEOUT_MS_RESPOND=150 +CONFIG_MB_MASTER_DELAY_MS_CONVERT=200 +CONFIG_MB_QUEUE_LENGTH=20 +CONFIG_MB_SERIAL_TASK_STACK_SIZE=4096 +CONFIG_MB_SERIAL_BUF_SIZE=256 +CONFIG_MB_SERIAL_TASK_PRIO=10 +CONFIG_MB_CONTROLLER_SLAVE_ID_SUPPORT=y +CONFIG_MB_CONTROLLER_SLAVE_ID=0x00112233 +CONFIG_MB_CONTROLLER_NOTIFY_TIMEOUT=20 +CONFIG_MB_CONTROLLER_NOTIFY_QUEUE_SIZE=20 +CONFIG_MB_CONTROLLER_STACK_SIZE=4096 +CONFIG_MB_EVENT_QUEUE_TIMEOUT=20 +CONFIG_MB_TIMER_PORT_ENABLED=y +CONFIG_MB_TIMER_GROUP=0 +CONFIG_MB_TIMER_INDEX=0 +# CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set +CONFIG_TIMER_TASK_PRIORITY=1 +CONFIG_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_TIMER_QUEUE_LENGTH=10 +# CONFIG_L2_TO_L3_COPY is not set +# CONFIG_USE_ONLY_LWIP_SELECT is not set +CONFIG_ESP_GRATUITOUS_ARP=y +CONFIG_GARP_TMR_INTERVAL=60 +CONFIG_TCPIP_RECVMBOX_SIZE=32 +CONFIG_TCP_MAXRTX=12 +CONFIG_TCP_SYNMAXRTX=12 +CONFIG_TCP_MSS=1440 +CONFIG_TCP_MSL=60000 +CONFIG_TCP_SND_BUF_DEFAULT=5744 +CONFIG_TCP_WND_DEFAULT=5744 +CONFIG_TCP_RECVMBOX_SIZE=6 +CONFIG_TCP_QUEUE_OOSEQ=y +# CONFIG_ESP_TCP_KEEP_CONNECTION_WHEN_IP_CHANGES is not set +CONFIG_TCP_OVERSIZE_MSS=y +# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_TCP_OVERSIZE_DISABLE is not set +CONFIG_UDP_RECVMBOX_SIZE=6 +CONFIG_TCPIP_TASK_STACK_SIZE=3072 +CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set +# CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF +# CONFIG_PPP_SUPPORT is not set +CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_ESP32_PTHREAD_STACK_MIN=768 +CONFIG_ESP32_DEFAULT_PTHREAD_CORE_NO_AFFINITY=y +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_0 is not set +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_1 is not set +CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" +CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set +CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y +CONFIG_SUPPORT_TERMIOS=y +CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +CONFIG_SEMIHOSTFS_HOST_PATH_MAX_LEN=128 +# End of deprecated options