Initial template version based of cspemu

modbus
Ondřej Hruška 2 years ago
commit c2e87940f2
  1. 4
      .gitignore
  2. 6
      CMakeLists.txt
  3. 8
      Makefile
  4. 8
      components/common_utils/CMakeLists.txt
  5. 2
      components/common_utils/README.txt
  6. 3
      components/common_utils/component.mk
  7. 75
      components/common_utils/include/common_utils/base16.h
  8. 131
      components/common_utils/include/common_utils/datetime.h
  9. 19
      components/common_utils/include/common_utils/hexdump.h
  10. 80
      components/common_utils/include/common_utils/utils.h
  11. 62
      components/common_utils/src/base16.c
  12. 52
      components/common_utils/src/common_utils.c
  13. 110
      components/common_utils/src/datetime.c
  14. 72
      components/common_utils/src/hexdump.c
  15. 9
      components/dhcp_wd/CMakeLists.txt
  16. 27
      components/dhcp_wd/Kconfig
  17. 5
      components/dhcp_wd/README.txt
  18. 3
      components/dhcp_wd/component.mk
  19. 82
      components/dhcp_wd/include/dhcp_wd.h
  20. 277
      components/dhcp_wd/src/dhcp_wd.c
  21. 16
      components/fileserver/CMakeLists.txt
  22. 2
      components/fileserver/README.txt
  23. 3
      components/fileserver/component.mk
  24. BIN
      components/fileserver/files/embed/favicon.ico
  25. 106
      components/fileserver/files/embed/index.html
  26. 163
      components/fileserver/files/rebuild_file_tables.php
  27. 15
      components/fileserver/files/www_files_enum.c
  28. 13
      components/fileserver/files/www_files_enum.h
  29. 51
      components/fileserver/include/fileserver/embedded_files.h
  30. 216
      components/fileserver/include/fileserver/token_subs.h
  31. 29
      components/fileserver/readme/README.md
  32. 170
      components/fileserver/readme/rebuild_file_tables.php
  33. 22
      components/fileserver/src/embedded_files.c
  34. 580
      components/fileserver/src/token_subs.c
  35. 9
      components/httpd_utils/CMakeLists.txt
  36. 4
      components/httpd_utils/README.txt
  37. 3
      components/httpd_utils/component.mk
  38. 32
      components/httpd_utils/include/httpd_utils/captive.h
  39. 13
      components/httpd_utils/include/httpd_utils/fd_to_ipv4.h
  40. 16
      components/httpd_utils/include/httpd_utils/redirect.h
  41. 55
      components/httpd_utils/include/httpd_utils/session.h
  42. 77
      components/httpd_utils/include/httpd_utils/session_kvmap.h
  43. 100
      components/httpd_utils/include/httpd_utils/session_store.h
  44. 106
      components/httpd_utils/src/captive.c
  45. 42
      components/httpd_utils/src/fd_to_ipv4.c
  46. 20
      components/httpd_utils/src/redirect.c
  47. 181
      components/httpd_utils/src/session_kvmap.c
  48. 220
      components/httpd_utils/src/session_store.c
  49. 41
      components/httpd_utils/src/session_utils.c
  50. 4
      components/ping/CMakeLists.txt
  51. 1
      components/ping/README.txt
  52. 3
      components/ping/component.mk
  53. 58
      components/ping/include/ping.h
  54. 262
      components/ping/src/ping.c
  55. 4
      components/socket_server/CMakeLists.txt
  56. 1
      components/socket_server/README.txt
  57. 3
      components/socket_server/component.mk
  58. 282
      components/socket_server/include/socket_server.h
  59. 1015
      components/socket_server/src/socket_server.c
  60. 18
      components/vconsole/CMakeLists.txt
  61. 54
      components/vconsole/libconsole/.gitignore
  62. 73
      components/vconsole/libconsole/CMakeLists.txt
  63. 1
      components/vconsole/libconsole/LICENSE.txt
  64. 24
      components/vconsole/libconsole/include/console/cmddef.h
  65. 22
      components/vconsole/libconsole/include/console/config.h.in
  66. 362
      components/vconsole/libconsole/include/console/console.h
  67. 196
      components/vconsole/libconsole/include/console/console_io.h
  68. 94
      components/vconsole/libconsole/include/console/prefix_match.h
  69. 213
      components/vconsole/libconsole/include/console/utils.h
  70. 9
      components/vconsole/libconsole/lib/argtable3/CMakeLists.txt
  71. 3
      components/vconsole/libconsole/lib/argtable3/README.txt
  72. 4969
      components/vconsole/libconsole/lib/argtable3/argtable3.c
  73. 306
      components/vconsole/libconsole/lib/argtable3/argtable3.h
  74. 1824
      components/vconsole/libconsole/src/console.c
  75. 42
      components/vconsole/libconsole/src/console_filecap.c
  76. 194
      components/vconsole/libconsole/src/console_io.c
  77. 1129
      components/vconsole/libconsole/src/console_linenoise.c
  78. 248
      components/vconsole/libconsole/src/console_linenoise.h
  79. 196
      components/vconsole/libconsole/src/console_prefix_match.c
  80. 120
      components/vconsole/libconsole/src/console_split_argv.c
  81. 36
      components/vconsole/libconsole/src/console_split_argv.h
  82. 204
      components/vconsole/libconsole/src/console_utils.c
  83. 645
      components/vconsole/libconsole/src/queue.h
  84. 46
      main/CMakeLists.txt
  85. 23
      main/Kconfig.projbuild
  86. 62
      main/app_main.c
  87. 26
      main/application.h
  88. 4
      main/component.mk
  89. 48
      main/console/cmd_common.h
  90. 34
      main/console/commands/cmd_dump.c
  91. 50
      main/console/commands/cmd_factory_reset.c
  92. 38
      main/console/commands/cmd_heap.c
  93. 341
      main/console/commands/cmd_ip.c
  94. 52
      main/console/commands/cmd_pw.c
  95. 43
      main/console/commands/cmd_restart.c
  96. 51
      main/console/commands/cmd_tasks.c
  97. 52
      main/console/commands/cmd_version.c
  98. 360
      main/console/commands/cmd_wifi.c
  99. 418
      main/console/console_ioimpl.c
  100. 77
      main/console/console_ioimpl.h
  101. Some files were not shown because too many files have changed in this diff Show More

4
.gitignore vendored

@ -0,0 +1,4 @@
.idea/
build
cmake-build-*
*.old

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

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

@ -0,0 +1,8 @@
set(COMPONENT_ADD_INCLUDEDIRS include)
set(COMPONENT_SRCDIRS
"src")
#set(COMPONENT_REQUIRES)
register_component()

@ -0,0 +1,2 @@
General purpose, mostly platofrm-idependent utilities
that may be used by other components.

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

@ -0,0 +1,75 @@
/*
* Copyright (C) 2010 Michael Brown <mbrown@fensystems.co.uk>.
*
* 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 <stdint.h>
#include <string.h>
/**
* 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_ */

@ -0,0 +1,131 @@
/**
* TODO file description
*
* Created on 2019/09/13.
*/
#ifndef CSPEMU_DATETIME_H
#define CSPEMU_DATETIME_H
#include <stdbool.h>
#include <stdint.h>
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

@ -0,0 +1,19 @@
/**
* @file
* @brief A simple way of dumping memory to a hex output
*
* \addtogroup Hexdump
*
* @{
*/
#include <stdio.h>
#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);
/**
* }@
*/

@ -0,0 +1,80 @@
/**
* General purpose, platform agnostic, reusable utils
*/
#ifndef COMMON_UTILS_UTILS_H
#define COMMON_UTILS_UTILS_H
#include <stdbool.h>
#include <stdint.h>
#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

@ -0,0 +1,62 @@
/*
* Copyright (C) 2010 Michael Brown <mbrown@fensystems.co.uk>.
*
* 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 <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <esp_log.h>
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);
}

@ -0,0 +1,52 @@
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <fcntl.h>
#include <errno.h>
#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;
}

@ -0,0 +1,110 @@
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#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;
}

@ -0,0 +1,72 @@
/*
* util.c
*
* Created on: Aug 12, 2009
* Author: johan
*/
// adapted from libgomspace
#include <string.h>
#include <stdio.h>
#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<len; i++) {
j++;
fprintf(fp, "%02X ", ((volatile unsigned char *)src)[i]);
if(j == 8)
fputc(' ', fp);
if(j == 16) {
j = 0;
memcpy(text, &((char *)src)[i-15], 16);
for(k=0; k<16; k++) {
if((text[k] < 32) || (text[k] > 126)) {
text[k] = '.';
}
}
fprintf(fp, " |%s|\n\r", text);
if(i<len-1) {
fprintf(fp, "%p : ", src+i+1);
}
}
}
if (i % 16)
fprintf(fp, "\r\n");
}
void hex_dump_buff_line(FILE *fp, int addr_size, unsigned pos, char *line, unsigned len)
{
unsigned i;
fprintf(fp, "%0*x", addr_size, pos);
for (i = 0; i < HEX_DUMP_LINE_BUFF_SIZ; i++)
{
if (!(i % 8))
fputc(' ', fp);
if (i < len)
fprintf(fp, " %02x", (unsigned char)line[i]);
else
fputs(" ", fp);
}
fputs(" |", fp);
for (i = 0; i < HEX_DUMP_LINE_BUFF_SIZ && i < len; i++)
{
if (line[i] >= 32 && line[i] <= 126)
fprintf(fp, "%c", (unsigned char)line[i]);
else
fputc('.', fp);
}
fputs("|\r\n", fp);
}

@ -0,0 +1,9 @@
set(COMPONENT_ADD_INCLUDEDIRS
"include")
set(COMPONENT_SRCDIRS
"src")
set(COMPONENT_REQUIRES ping tcpip_adapter)
register_component()

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

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

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

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

@ -0,0 +1,277 @@
#include <string.h>
#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;
}
}

@ -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()

@ -0,0 +1,2 @@
File and template serving support for the http_server bundled with ESP-IDF.

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ESP node</title>
<style>
html { font-family:sans-serif }
table { border-collapse:collapse; }
td,th { padding: 4px 10px; min-width: 65px; }
td { min-width: 70px; }
th { text-align:left; }
tbody th { text-align:center; }
tbody th {border-right: 1px solid black;}
thead th { text-align:center;padding-top:0; }
td { text-align:right; }
td[c],[c] td { text-align:center; }
td[l],[l] td { text-align:left; }
td[r],[r] td { text-align:right; }
td[ed] {cursor:pointer;}
td[ed]:hover {text-decoration:underline;}
table[thl] tbody th {text-align:left;}
figure {
border-top:3px solid black;
border-bottom:3px solid black;
display: inline-block;
padding: 5px 0;
margin: 0 0 15px 15px;
}
thead tr { border-bottom: 2px solid black; }
</style>
</head>
<body>
<h1>ESP node {version}</h1>
<a href="/reboot">Restart node</a>
<script>
var kv_re = /^(-?[0-9.-]+)\s+(.+)$/;
var Qi = function (x) { return document.getElementById(x) };
var data = {};
function update(data) {
if (data) {
var rows = data.split('\x1e');
rows.forEach(function (v) {
var kv = v.split('\x1f');
var el = Qi(kv[0]);
if (!el) return;
var suf = '';
var res = kv_re.exec(el.textContent);
if (res) suf = ' ' + res[2];
el.textContent = kv[1] + suf;
data[kv[0]] = kv[1];
});
} else {
var xhr=new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState===4){
if (xhr.status===200) {
update(xhr.responseText);
}
setTimeout(update, 1000);
}
};
xhr.onerror = function () {
setTimeout(update, 1000);
};
xhr.open('GET', '/data');
xhr.send();
}
}
function mkXhr() {
var xhr=new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState===4 && xhr.status!==200) {
alert(xhr.responseText || xhr.statusText || 'Request failed');
}
};
return xhr;
}
function editOpt() {
var k = this.id;
var v = prompt(k+' =', data[k]||'');
if (v !== null) {
var xhr = mkXhr();
xhr.open('POST', '/set');
xhr.send("key="+k+'&value='+encodeURIComponent(v));
}
}
function toggleOpt() {
var xhr = mkXhr();
xhr.open('POST', '/toggle');
xhr.send("x="+this.id);
}
/*
setTimeout(update, 500);
for(var i=1;i<8;i++) {
if(i<7) Qi('i'+i+'_max').addEventListener('click', editOpt);
Qi('ch'+i+'_state').addEventListener('click', toggleOpt);
}
*/
</script>

@ -0,0 +1,163 @@
#!/usr/bin/env php
<?php
// This script rebuilds the static files enum, extern symbols pointing to the embedded byte buffers,
// and the look-up structs table. To add more files, simply add them in the 'files' directory.
// Note that all files will be accessible by the webserver, unless you filter them in embedded_files.c.
// List all files
$files = scandir(__DIR__.'/embed');
$files = array_filter(array_map(function ($f) {
if (!is_file(__DIR__.'/embed/'.$f)) return null;
if (preg_match('/^\.|\.kate-swp|\.bak$|~$|\.sh$/', $f)) return null;
echo "Found: $f\n";
return $f;
}, $files));
sort($files);
$formatted = array_filter(array_map(function ($f) {
return "\"files/embed/$f\"";
}, $files));
$cmake = file_get_contents(__DIR__.'/../CMakeLists.txt');
$cmake = preg_replace('/#begin staticfiles\n.*#end staticfiles/s',
"#begin staticfiles\n".
"# generated by rebuild_file_tables\n".
"set(COMPONENT_EMBED_FILES\n ".
implode("\n ", $formatted) . ")\n".
"#end staticfiles",
$cmake);
file_put_contents(__DIR__.'/../CMakeLists.txt', $cmake);
// Generate a list of files
$num = 0;
$enum_keys = array_map(function ($f) use(&$num) {
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
return 'FILE_'. $a.' = '.($num++);
}, $files);
$keylist = implode(",\n ", $enum_keys);
$struct_array = [];
$externs = array_map(function ($f) use (&$struct_array) {
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
$start = '_binary_'. strtolower($a).'_start';
$end = '_binary_'. strtolower($a).'_end';
static $mimes = array(
'txt' => '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', <<<FILE
// Generated by 'rebuild_file_tables'
#ifndef _EMBEDDED_FILES_ENUM_H
#define _EMBEDDED_FILES_ENUM_H
#include "fileserver/embedded_files.h"
enum embedded_file_id {
$keylist
};
#endif // _EMBEDDED_FILES_ENUM_H
FILE
);
$files_count = count($struct_array);
file_put_contents("www_files_enum.c", <<<FILE
// Generated by 'rebuild_file_tables'
#include <stdint.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
);

@ -0,0 +1,15 @@
// Generated by 'rebuild_file_tables'
#include <stdint.h>
#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;

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

@ -0,0 +1,51 @@
//
// Created on 2018/10/17 by Ondrej Hruska <ondra@ondrovo.com>
//
#ifndef FBNODE_EMBEDDED_FILES_H
#define FBNODE_EMBEDDED_FILES_H
#include <stdint.h>
#include <esp_err.h>
#include <stdbool.h>
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

@ -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}";
// <input value="{h:old_value}">
// {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.
//
// <input value="{h:old_value?}">
//
// 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 <ondra@ondrovo.com>
//
#ifndef FBNODE_TOKEN_SUBS_H
#define FBNODE_TOKEN_SUBS_H
#include "embedded_files.h"
#include <sys/queue.h>
#include <esp_err.h>
#include <esp_http_server.h>
/** 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

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

@ -0,0 +1,170 @@
#!/usr/bin/env php
<?php
// This script rebuilds the static files enum, extern symbols pointing to the embedded byte buffers,
// and the look-up structs table. To add more files, simply add them in the 'files' directory.
// Note that all files will be accessible by the webserver, unless you filter them in embedded_files.c.
// List all files
$files = scandir(__DIR__.'/embed');
$files = array_filter(array_map(function ($f) {
if (!is_file(__DIR__.'/embed/'.$f)) return null;
if (preg_match('/^\.|\.kate-swp|\.bak$|~$|\.sh$/', $f)) return null;
echo "Found: $f\n";
return $f;
}, $files));
sort($files);
$formatted = array_filter(array_map(function ($f) {
return "\"files/embed/$f\"";
}, $files));
$cmake = file_get_contents(__DIR__.'/../CMakeLists.txt');
$cmake = preg_replace('/#begin staticfiles\n.*#end staticfiles/s',
"#begin staticfiles\n".
"set(COMPONENT_EMBED_FILES\n ".
implode("\n ", $formatted) . ")\n".
"#end staticfiles",
$cmake);
file_put_contents(__DIR__.'/../CMakeLists.txt', $cmake);
// Generate a list of files
$num = 0;
$enum_keys = array_map(function ($f) use(&$num) {
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
return 'FILE_'. $a.' = '.($num++);
}, $files);
$keylist = implode(",\n ", $enum_keys);
$struct_array = [];
$externs = array_map(function ($f) use (&$struct_array) {
$a = preg_replace("/[^A-Z0-9_]+/", "_", strtoupper($f));
$start = '_binary_'. strtolower($a).'_start';
$end = '_binary_'. strtolower($a).'_end';
static $mimes = array(
'txt' => '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', <<<FILE
// Do not change, auto-generated by gen_staticfiles.php
#ifndef _EMBEDDED_FILES_ENUM_H
#define _EMBEDDED_FILES_ENUM_H
#include <stddef.h>
#include <stdint.h>
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", <<<FILE
// Do not change, auto-generated by gen_staticfiles.php
#include <stdint.h>
#include "files_enum.h"
const struct embedded_file_info EMBEDDED_FILE_LOOKUP[] = {
$structlist
};
FILE
);

@ -0,0 +1,22 @@
#include <esp_err.h>
#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;
}

@ -0,0 +1,580 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <esp_log.h>
#include <esp_http_server.h>
#include <sys/queue.h>
#include <lwip/ip4_addr.h>
#include <sys/param.h>
#include <common_utils/utils.h>
#include <fileserver/token_subs.h>
#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, "&#34;", 5));
else if (c == '\'') ESP_TRY(httpd_resp_send_chunk(r, "&#39;", 5));
else if (c == '<') ESP_TRY(httpd_resp_send_chunk(r, "&lt;", 4));
else if (c == '>') ESP_TRY(httpd_resp_send_chunk(r, "&gt;", 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;
}

@ -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()

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

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

@ -0,0 +1,32 @@
#ifndef HTTPD_UTILS_CAPTIVE_H
#define HTTPD_UTILS_CAPTIVE_H
#include <esp_http_server.h>
#include <esp_err.h>
/**
* 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

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

@ -0,0 +1,16 @@
#ifndef HTTPD_UTILS_REDIRECT_H
#define HTTPD_UTILS_REDIRECT_H
#include <esp_http_server.h>
#include <esp_err.h>
/**
* 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

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

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

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

@ -0,0 +1,106 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/param.h>
#include "httpd_utils/captive.h"
#include "httpd_utils/fd_to_ipv4.h"
#include "httpd_utils/redirect.h"
#include <common_utils/utils.h>
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;
}

@ -0,0 +1,42 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#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;
}

@ -0,0 +1,20 @@
#include <esp_wifi_types.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <fcntl.h>
#include <sys/socket.h>
#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);
}

@ -0,0 +1,181 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <sys/queue.h>
#include <malloc.h>
#include <assert.h>
#include <esp_log.h>
#include <esp_err.h>
#include <string.h>
#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;
}

@ -0,0 +1,220 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <malloc.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <esp_http_server.h>
#include <sys/queue.h>
#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);
}

@ -0,0 +1,41 @@
/**
* TODO file description
*
* Created on 2019/07/13.
*/
#ifndef SESSION_UTILS_C_H
#define SESSION_UTILS_C_H
#include <esp_err.h>
#include <esp_http_server.h>
#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

@ -0,0 +1,4 @@
set(COMPONENT_ADD_INCLUDEDIRS include)
set(COMPONENT_SRCS "src/ping.c")
register_component()

@ -0,0 +1 @@
ICMP ping implementation for connectivity testing

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

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

@ -0,0 +1,262 @@
// based on https://github.com/pbecchi/ESP32_ping/blob/master/Ping.cpp
#include <string.h>
#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;
}

@ -0,0 +1,4 @@
set(COMPONENT_ADD_INCLUDEDIRS include)
set(COMPONENT_SRCS "src/socket_server.c")
register_component()

@ -0,0 +1 @@
Generic TCP socket server that can be used e.g. for telnet

@ -0,0 +1,3 @@
COMPONENT_SRCDIRS := src
COMPONENT_ADD_INCLUDEDIRS := include

@ -0,0 +1,282 @@
/**
* Generic implementation of a TCP socket server.
*/
#ifndef _SOCKET_SERVER_H_
#define _SOCKET_SERVER_H_
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <sys/types.h>
#include <sys/socket.h>
#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 <esp_err.h>
#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_

File diff suppressed because it is too large Load Diff

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

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

@ -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})

@ -0,0 +1 @@
Proprietary code (c) VZLU 2019-2020

@ -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 <stdint.h>
#include <argtable3.h>
#include "console/console.h"
#if CONSOLE_HAVE_CSP
#include <csp/csp.h>
#endif
#include "console/utils.h"
#endif //LIBCONSOLE_CMDDEF_H

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

@ -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 <stdio.h>
#include <console/config.h>
#include <stdint.h>
#include <stdbool.h>
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 <pthread.h>
typedef pthread_mutex_t console_mutex_t;
#endif
#if CONSOLE_USE_TERMIOS
#include <termios.h>
#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

@ -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 <stdarg.h>
// ------ 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

@ -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 <stdbool.h>
#include <stddef.h>
/** 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

@ -0,0 +1,213 @@
/**
* Utilities for console commands
*
* Created on 2020/03/11.
*/
#ifndef LIBCONSOLE_UTILS_H
#define LIBCONSOLE_UTILS_H
#include <stdio.h>
#include <console/console.h>
#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, "<node>", EXPENDABLE_STRING("node ID"))
/**
* Define a mandatory CSP node argument
*/
#define arg_cspaddr1() arg_int1(NULL, NULL, "<node>", 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", "<ms>", 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", "<ms>", 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

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

@ -0,0 +1,3 @@
Copy of upstream argtable3 from github
It is released under the 3-clause BSD license.

File diff suppressed because it is too large Load Diff

@ -0,0 +1,306 @@
/*******************************************************************************
* This file is part of the argtable3 library.
*
* Copyright (C) 1998-2001,2003-2011,2013 Stewart Heitmann
* <sheitmann@users.sourceforge.net>
* 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 <stdio.h> /* FILE */
#include <time.h> /* 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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,42 @@
#include <stdio.h>
#include <console/console.h>
#include <console/utils.h>
#include <malloc.h>
#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

@ -0,0 +1,194 @@
// enable "vasprintf" from stdio.h
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "console/console.h"
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
// 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 <unistd.h>
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 <termio.h>
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);
}

File diff suppressed because it is too large Load Diff

@ -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 <antirez at gmail dot com>
* Copyright (c) 2010-2013, Pieter Noordhuis <pcnoordhuis at gmail dot com>
*
* 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 <stddef.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#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 <tab>. 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 */

@ -0,0 +1,196 @@
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#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;
}

@ -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 <stdio.h>
#include <ctype.h>
#include <string.h>
#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;
}

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

@ -0,0 +1,204 @@
//
// Created by MightyPork on 2020/03/11.
//
#include <string.h>
#include <stdint.h>
#include "console/console.h"
#include "console/utils.h"
#if (CONSOLE_HAVE_CSP && !CSP_POSIX) || CONSOLE_TESTING_ALLOC_FUNCS
#include <csp/arch/csp_malloc.h>
#else
#include <malloc.h>
#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
}

@ -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 <sys/cdefs.h>
#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_ */

@ -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")

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

@ -0,0 +1,62 @@
#include <sdkconfig.h>
#include <console/console.h>
#include <console/console_ioimpl.h>
#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());
}

@ -0,0 +1,26 @@
/**
* Globals
*/
#ifndef _APPLICATION_H
#define _APPLICATION_H
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#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

@ -0,0 +1,4 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)

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

@ -0,0 +1,34 @@
//
// Created by MightyPork on 2018/12/08.
//
#include "settings.h"
#include "console/cmd_common.h"
#include <console/cmddef.h>
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");
}

@ -0,0 +1,50 @@
//
// Created by MightyPork on 2018/12/08.
//
#include <string.h>
#include <linenoise/linenoise.h>
#include <settings.h>
#include <utils.h>
#include <nvs_flash.h>
#include "console/cmd_common.h"
#include <console/cmddef.h>
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>", "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");
}

@ -0,0 +1,38 @@
//
// Created by MightyPork on 2018/12/08.
//
#include <esp_heap_caps.h>
#include <esp_system.h>
#include "console/cmd_common.h"
#include <console/cmddef.h>
/* '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");
}

@ -0,0 +1,341 @@
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <esp_wifi.h>
#include <settings.h>
#include <lwip/inet.h>
#include "console/cmd_common.h"
#include <console/cmddef.h>
#include <application.h>
#include <common_utils/utils.h>
#include <sntp_cli.h>
#include <ping.h>
#include <lwip/netdb.h>
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, "<cmd>", "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, "<IP>", "Target IP");
args.num = arg_int0("n", NULL, "<num>", "Ping count (def 1)");
args.timeout = arg_int0("t", NULL, "<ms>", "Timeout (def 3000)");
args.interval = arg_int0("i", NULL, "<ms>", "Interval (def 1000)");
args.size = arg_int0("s", NULL, "<bytes>", "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, "<IP>", "Set IP address");
args.gw = arg_str0("g", NULL, "<GW>", "Set gateway address");
args.mask = arg_str0("n", NULL, "<MASK>", "Set netmask");
args.dns = arg_str0("d", NULL, "<DNS>", "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, "<cmd>", "Enable or disable autostart. Omit to check current state. start = start now");
cmd_args.addr = arg_str0("s", NULL, "<server>", "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");
}

@ -0,0 +1,52 @@
//
// Set telnet pw
//
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <esp_wifi.h>
#include <settings.h>
#include "console/cmd_common.h"
#include <console/cmddef.h>
#include <application.h>
#include <console/prefix_match.h>
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, "<password>", 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");
}

@ -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 <console/cmddef.h>
/** '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");
}

@ -0,0 +1,51 @@
//
// Created by MightyPork on 2018/12/08.
//
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "console/cmd_common.h"
#include <console/cmddef.h>
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");
}

@ -0,0 +1,52 @@
//
// Created by MightyPork on 2018/12/08.
//
#include "console/cmd_common.h"
#include <esp_spi_flash.h>
#include <esp_system.h>
#include "application.h"
#include <console/cmddef.h>
/** '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");
}

@ -0,0 +1,360 @@
//
// Created by MightyPork on 2018/12/08.
//
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <esp_wifi.h>
#include <settings.h>
#include "console/cmd_common.h"
#include <console/cmddef.h>
#include <application.h>
#include <console/prefix_match.h>
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, "<SSID>", EXPENDABLE_STRING("Set AP SSID"));
args.pw = arg_str0("p", NULL, "<PWD>", EXPENDABLE_STRING("Set AP WPA2 password. Empty for open."));
args.ip = arg_str0("a", NULL, "<IP>", "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", "<t>", "Connection timeout, ms");
cmd_args.ssid = arg_str1(NULL, NULL, "<ssid>", "SSID of AP");
cmd_args.password = arg_str0(NULL, NULL, "<pass>", "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");
}

@ -0,0 +1,418 @@
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <utils.h>
#include <stdlib.h>
#include <esp_log.h>
#include <driver/uart.h>
#include <errno.h>
#include <esp_vfs_dev.h>
#include <fcntl.h>
#include <settings.h>
#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);
}
}

@ -0,0 +1,77 @@
/**
* TODO file description
*
* Created on 2020/04/09.
*/
#ifndef CSPEMU_CONSOLE_IOIMPL_H
#define CSPEMU_CONSOLE_IOIMPL_H
#include <freertos/FreeRTOS.h>
#include <freertos/ringbuf.h>
#include <socket_server.h>
#include <console/console.h>
/** 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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save