including tftp based shell and sockets from different project
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "."
|
||||
INCLUDE_DIRS "."
|
||||
SRC_DIRS "src"
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file countDown.hpp
|
||||
* @author Gustice
|
||||
* @brief Classes for typical index manipulation
|
||||
*
|
||||
* @copyright Copyright (c) 2020
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class CountDown {
|
||||
public:
|
||||
CountDown(unsigned max) : Max(max) {}
|
||||
|
||||
void Tick() {
|
||||
_cnt++;
|
||||
if (_cnt >= Max) {
|
||||
_cnt = 0;
|
||||
_pFlag = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool GetVolatileFlag() {
|
||||
if (!_pFlag)
|
||||
return false;
|
||||
|
||||
// Reset _pFlag in any case, so it only be consumed once
|
||||
// hence volatile
|
||||
_pFlag = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
const unsigned Max;
|
||||
unsigned _cnt;
|
||||
bool _pFlag;
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @file rotatingIndex.hpp
|
||||
* @author Gustice
|
||||
* @brief Classes for typical index manipulation
|
||||
*
|
||||
* @copyright Copyright (c) 2020
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* @brief Index with automatic revolution back to start position
|
||||
* @details Useful for Buffer-Pointer, or similar tasks
|
||||
*/
|
||||
class RotatingIndex {
|
||||
public:
|
||||
/// @brief Value at witch the index rotates back to beginning
|
||||
const uint16_t MaxIndex;
|
||||
|
||||
/// @brief Constructor for rotating index
|
||||
/// @brief output index will be set to 0 if incremented value reaches Maximum
|
||||
/// @param max Maximum index for overflow-condition
|
||||
RotatingIndex(uint16_t max) : MaxIndex(max) { _index = 0; }
|
||||
|
||||
/// @brief Get the current index and inkrement index afterwards
|
||||
/// @return uint16_t current index
|
||||
uint16_t getIndexAndIncrement(void) {
|
||||
auto retVal = _index;
|
||||
_index++;
|
||||
if (_index >= MaxIndex) {
|
||||
_index = 0;
|
||||
}
|
||||
return retVal;
|
||||
};
|
||||
|
||||
/// @brief Get the current index without incrementing the index
|
||||
/// @return uint16_t current index
|
||||
uint16_t getIndex(void) { return _index; }
|
||||
|
||||
/// @brief Check if next get will lead to overflow condition
|
||||
bool peekRevolution(void) {
|
||||
if (MaxIndex == 0)
|
||||
return true;
|
||||
|
||||
if (_index >= MaxIndex - 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// @brief Reset Index to 0
|
||||
void resetIndex(void) { _index = 0; }
|
||||
|
||||
private:
|
||||
/// Current Index
|
||||
uint16_t _index;
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @file utils.hpp
|
||||
* @author Gustice
|
||||
* @brief Template
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* @brief Simple exception
|
||||
*/
|
||||
class CoreException : public std::exception {
|
||||
char const *Text;
|
||||
|
||||
public:
|
||||
/// @brief Constructor with simple pointer to text
|
||||
CoreException(char const *text) : Text(text) {
|
||||
}
|
||||
|
||||
/// @brief error message getter
|
||||
char const *what() const throw() override { return Text; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Common interface for character streams
|
||||
*/
|
||||
class CharStream {
|
||||
public:
|
||||
/// @brief Read method
|
||||
/// @return string
|
||||
virtual std::string read() = 0;
|
||||
|
||||
/// @brief Write character
|
||||
/// @param c character to write
|
||||
virtual void write(char c) = 0;
|
||||
|
||||
/// @brief Write string
|
||||
/// @param str string to write
|
||||
virtual void write(std::string str) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Common interface
|
||||
*/
|
||||
class ByteStream {
|
||||
/// @brief Read method
|
||||
/// @return series of bytes
|
||||
virtual std::vector<uint8_t> read() = 0;
|
||||
|
||||
/// @brief write single byte
|
||||
/// @param b byte to write
|
||||
virtual void write(uint8_t b) = 0;
|
||||
|
||||
/// @brief write series of bytes
|
||||
/// @param data series to write
|
||||
virtual void write(std::vector<uint8_t> data) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Base for constructs that allow only one concurrently active instance
|
||||
*
|
||||
* @tparam T
|
||||
* @note needs a `T * T::SingleActive<T>::active{};` definition in source
|
||||
*/
|
||||
template <typename T>
|
||||
class SingleActive {
|
||||
static bool active;
|
||||
|
||||
public:
|
||||
/// @brief Constructor, memorizes if already on instance was successfully build
|
||||
SingleActive() {
|
||||
if (active) {
|
||||
throw "Instance already active";
|
||||
}
|
||||
assert(active == false && "Constructor my only be run if no instance active");
|
||||
active = true;
|
||||
}
|
||||
|
||||
/// @brief Destructor, clears memory of existing instance
|
||||
~SingleActive() {
|
||||
assert(active == true && "Destructor my only be run on active instance");
|
||||
active = false;
|
||||
}
|
||||
|
||||
// Disable move and assignment
|
||||
SingleActive(SingleActive &other) = delete;
|
||||
void operator=(const SingleActive &) = delete;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @file utils.cpp
|
||||
* @author Gustice
|
||||
* @brief Implementation of utilities
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
|
||||
#include "utils.hpp"
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "src"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_netif lwip project_bases freeRtosApi
|
||||
)
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* @file etherCommon.hpp
|
||||
* @author Gustice
|
||||
* @brief Helper Functions for Ethernet usage
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "lwip/err.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/sys.h"
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <lwip/netdb.h>
|
||||
#include <string>
|
||||
|
||||
#if defined(CONFIG_LWIP_NETBUF_RECVINFO)
|
||||
constexpr bool NETBUF_RECVINFO = true;
|
||||
#else
|
||||
constexpr bool NETBUF_RECVINFO = false;
|
||||
#endif
|
||||
#if defined(CONFIG_EXAMPLE_IPV4)
|
||||
constexpr bool USE_IPV4 = true;
|
||||
#else
|
||||
constexpr bool USE_IPV4 = false;
|
||||
#endif
|
||||
#if defined(CONFIG_EXAMPLE_IPV6)
|
||||
constexpr bool USE_IPV6 = true;
|
||||
#else
|
||||
constexpr bool USE_IPV6 = false;
|
||||
#endif
|
||||
|
||||
class Ip {
|
||||
public:
|
||||
Ip(uint32_t ipV4) {
|
||||
_address.word = ipV4;
|
||||
}
|
||||
Ip(esp_ip4_addr_t ipV4) {
|
||||
_address.word = ipV4.addr;
|
||||
}
|
||||
|
||||
Ip(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) {
|
||||
_address.bytes[0] = b0;
|
||||
_address.bytes[1] = b1;
|
||||
_address.bytes[2] = b2;
|
||||
_address.bytes[3] = b3;
|
||||
}
|
||||
|
||||
Ip(std::string ipStr) {
|
||||
_address.word = ipaddr_addr(ipStr.c_str());
|
||||
}
|
||||
|
||||
uint8_t operator[](int index) const { return _address.bytes[index]; };
|
||||
uint32_t asWord() const { return _address.word; }
|
||||
std::string asString() const { return inet_ntoa(_address.word); }
|
||||
|
||||
private:
|
||||
union {
|
||||
uint8_t bytes[4]; // IPv4 address
|
||||
uint32_t word;
|
||||
} _address;
|
||||
};
|
||||
|
||||
class Mac {
|
||||
public:
|
||||
Mac(uint8_t val[NETIF_MAX_HWADDR_LEN]) {
|
||||
std::memcpy(bytes.begin(), val, NETIF_MAX_HWADDR_LEN);
|
||||
}
|
||||
|
||||
uint8_t operator[](int index) const {
|
||||
return bytes[index];
|
||||
};
|
||||
|
||||
std::string asString() const {
|
||||
char buffer[64];
|
||||
sprintf(buffer, "%02x:%02x:%02x:%02x:%02x:%02x", bytes[0], bytes[1],
|
||||
bytes[2], bytes[3], bytes[4], bytes[5]);
|
||||
return {buffer};
|
||||
}
|
||||
|
||||
const std::array<uint8_t, NETIF_MAX_HWADDR_LEN> &asArray() const {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<uint8_t, NETIF_MAX_HWADDR_LEN> bytes; // IPv4 address
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief IP-set class
|
||||
*/
|
||||
struct IpInfo {
|
||||
/// @brief IP address
|
||||
Ip address{0u};
|
||||
/// @brief IP subnet
|
||||
Ip subnet{0u};
|
||||
/// @brief Gateway IP
|
||||
Ip gateway{0u};
|
||||
/// @brief MAC-Address
|
||||
Mac macAddress;
|
||||
|
||||
/// @brief Get broadcast address from IP and subnet
|
||||
/// @return
|
||||
Ip getBradcastAddress() const {
|
||||
auto sn = address.asWord() | ~subnet.asWord();
|
||||
return Ip(sn);
|
||||
}
|
||||
};
|
||||
|
||||
struct IpAdr {
|
||||
public:
|
||||
IpAdr(std::string ipStr, uint16_t port);
|
||||
IpAdr(uint32_t ip, uint16_t port);
|
||||
IpAdr(uint16_t port);
|
||||
IpAdr(sockaddr_in ipV4data);
|
||||
|
||||
static constexpr int IpProtocol = IPPROTO_IP; // IPPROTO_IPV6 for version 6
|
||||
static constexpr int ProtocolFamily = AF_INET; // AF_INET6 for version 6
|
||||
/// @brief StackFamily e.g. AF_INET or AF_INET6
|
||||
sa_family_t getProtocolFamily() const { return addrStore.ss_family; }
|
||||
|
||||
std::string getIp() {
|
||||
struct sockaddr_in *ipV4 = (struct sockaddr_in *)&addrStore;
|
||||
return inet_ntoa(ipV4->sin_addr.s_addr);
|
||||
}
|
||||
uint16_t getPort() const {
|
||||
struct sockaddr_in *ipV4 = (struct sockaddr_in *)&addrStore;
|
||||
return ntohs(ipV4->sin_port);
|
||||
};
|
||||
struct sockaddr *asSockAddrPtr() const { return (struct sockaddr *)&addrStore; }
|
||||
|
||||
static constexpr int Size = sizeof(sockaddr_storage);
|
||||
private:
|
||||
struct sockaddr_storage addrStore;
|
||||
};
|
||||
|
||||
struct IpAdrV6 {
|
||||
public:
|
||||
IpAdrV6(std::string ipStr, uint16_t port);
|
||||
IpAdrV6(in6_addr ip, uint16_t port);
|
||||
IpAdrV6(uint16_t port);
|
||||
IpAdrV6(sockaddr_in6 ipV6data);
|
||||
|
||||
static constexpr int IpProtocol = IPPROTO_IPV6;
|
||||
static constexpr int ProtocolFamily = AF_INET6;
|
||||
/// @brief StackFamily e.g. AF_INET or AF_INET6
|
||||
sa_family_t getProtocolFamily() const { return addrStore.ss_family; }
|
||||
|
||||
std::string getIp() {
|
||||
struct sockaddr_in6 *ipV6 = (struct sockaddr_in6 *)&addrStore;
|
||||
return inet6_ntoa(ipV6->sin6_addr);
|
||||
|
||||
}
|
||||
uint16_t getPort() const {
|
||||
struct sockaddr_in6 *ipV6 = (struct sockaddr_in6 *)&addrStore;
|
||||
return ntohs(ipV6->sin6_port);
|
||||
};
|
||||
struct sockaddr *asSockAddrPtr() const { return (struct sockaddr *)&addrStore; }
|
||||
|
||||
static constexpr int Size = sizeof(sockaddr_storage);
|
||||
private:
|
||||
struct sockaddr_storage addrStore;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Simplified abstraction for sockets
|
||||
*/
|
||||
class SocketBase {
|
||||
public:
|
||||
/// @brief Get if stream is active
|
||||
/// @return true if active
|
||||
virtual bool isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
IpInfo getIpInfo() {
|
||||
auto *netIf = esp_netif_get_default_netif();
|
||||
esp_netif_ip_info_t ip;
|
||||
uint8_t mac[NETIF_MAX_HWADDR_LEN];
|
||||
ESP_ERROR_CHECK(esp_netif_get_ip_info(netIf, &ip));
|
||||
ESP_ERROR_CHECK(esp_netif_get_mac(netIf, mac));
|
||||
|
||||
IpInfo ips{
|
||||
.address = Ip{ip.ip},
|
||||
.subnet = Ip(ip.netmask),
|
||||
.gateway = Ip(ip.netmask),
|
||||
.macAddress = Mac(mac)};
|
||||
return ips;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool active;
|
||||
};
|
||||
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* @file tcpSockets.hpp
|
||||
* @author Gustice
|
||||
* @brief Function wrapper for ESP-IDF socket implementation
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "etherCommon.hpp"
|
||||
#include "utils.hpp"
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
|
||||
/**
|
||||
* @brief TCP-Socket wrapper for ESP-IDF socket implementation
|
||||
*/
|
||||
class TcpSocket {
|
||||
public:
|
||||
/// @brief Constructor
|
||||
/// @note Socket not active until call of attach method
|
||||
/// @param addr Server/Client address
|
||||
TcpSocket(const IpAdr &addr)
|
||||
: Local(addr) {
|
||||
_listenSockId = socket(addr.ProtocolFamily, SOCK_STREAM, addr.IpProtocol);
|
||||
if (_listenSockId < 0) {
|
||||
ESP_LOGE("TCP", "Unable to create socket: errno %d", errno);
|
||||
throw CoreException("Unable to create socket");
|
||||
}
|
||||
|
||||
if (addr.ProtocolFamily == IPPROTO_IP) {
|
||||
int opt = 1;
|
||||
setsockopt(_listenSockId, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
}
|
||||
|
||||
if constexpr (USE_IPV4 && USE_IPV6) {
|
||||
int opt = 1;
|
||||
// Note that by default IPV6 binds to both protocols, it is must be disabled
|
||||
// if both protocols used at the same time (used in CI)
|
||||
setsockopt(_listenSockId, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
|
||||
}
|
||||
|
||||
ESP_LOGI("TCP", "Socket created");
|
||||
}
|
||||
|
||||
/// @brief Destructor
|
||||
~TcpSocket() {
|
||||
close(_listenSockId);
|
||||
}
|
||||
|
||||
/// @brief Attach to configured socket
|
||||
/// @note Blocks until successful or failed
|
||||
/// @return true if successful
|
||||
bool bindTo() {
|
||||
int err = bind(_listenSockId, Local.asSockAddrPtr(), sizeof(sockaddr_storage));
|
||||
if (err != 0) {
|
||||
ESP_LOGE("TCP", "Socket unable to bind: errno %d", errno);
|
||||
ESP_LOGE("TCP", "IPPROTO: %d", Local.ProtocolFamily);
|
||||
throw CoreException("Socket unable to bind");
|
||||
}
|
||||
ESP_LOGI("TCP", "Socket bound, port %d", Local.getPort());
|
||||
|
||||
err = listen(_listenSockId, 1);
|
||||
if (err != 0) {
|
||||
ESP_LOGE("TCP", "Error occurred during listen: errno %d", errno);
|
||||
throw CoreException("Error occurred during listen");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @brief Get socket id for stream-operations
|
||||
/// @return Id of socket
|
||||
int getSocketId() {
|
||||
return _listenSockId;
|
||||
}
|
||||
|
||||
/// @brief Address of this station (server perspective)
|
||||
const IpAdr &Local;
|
||||
|
||||
private:
|
||||
int _listenSockId;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief TCP session
|
||||
*/
|
||||
class TcpSession : public SocketBase, public CharStream {
|
||||
public:
|
||||
/**
|
||||
* @brief Configuration structure for Session
|
||||
*/
|
||||
struct Config {
|
||||
int keepAlive;
|
||||
int keepIdle;
|
||||
int keepInterval;
|
||||
int keepCount;
|
||||
};
|
||||
|
||||
/// @brief Constructor
|
||||
/// @note Constructor blocks
|
||||
/// @param config Configuration for session
|
||||
/// @param listener reference to socket
|
||||
TcpSession(Config &config, TcpSocket &listener) : _other(IPADDR_ANY, 0) {
|
||||
socklen_t addr_len = _other.Size;
|
||||
_socket = accept(listener.getSocketId(), _other.asSockAddrPtr(), &addr_len);
|
||||
if (_socket < 0) {
|
||||
ESP_LOGE("TCP", "Unable to accept connection: errno %d", errno);
|
||||
active = false;
|
||||
throw CoreException("Unable to accept connection");
|
||||
}
|
||||
|
||||
// Set tcp keepalive option
|
||||
setsockopt(_socket, SOL_SOCKET, SO_KEEPALIVE, &config.keepAlive, sizeof(int));
|
||||
setsockopt(_socket, IPPROTO_TCP, TCP_KEEPIDLE, &config.keepIdle, sizeof(int));
|
||||
setsockopt(_socket, IPPROTO_TCP, TCP_KEEPINTVL, &config.keepInterval, sizeof(int));
|
||||
setsockopt(_socket, IPPROTO_TCP, TCP_KEEPCNT, &config.keepCount, sizeof(int));
|
||||
|
||||
std::string addrStr;
|
||||
if (_other.ProtocolFamily == listener.Local.ProtocolFamily) {
|
||||
if (_other.getProtocolFamily() == PF_INET) {
|
||||
addrStr = _other.getIp();
|
||||
// } else if (_other.addrStore.ss_family == PF_INET6) {
|
||||
// inet6_ntoa_r(((struct sockaddr_in6 *)&_other.addrStore)->sin6_addr, addrStr, sizeof(addrStr) - 1);
|
||||
} else {
|
||||
throw CoreException("Unknown case");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW("TCP", "Wrong IP-version request");
|
||||
active = false;
|
||||
}
|
||||
|
||||
ESP_LOGI("TCP", "Socket accepted ip address: %s", addrStr.c_str());
|
||||
|
||||
active = true;
|
||||
}
|
||||
|
||||
/// @brief Destructor
|
||||
~TcpSession() {
|
||||
if (_socket < 0)
|
||||
return;
|
||||
|
||||
ESP_LOGD("TCP", "Tearing down socket");
|
||||
shutdown(_socket, 0);
|
||||
}
|
||||
|
||||
/// @brief Get IP of remote
|
||||
/// @return
|
||||
IpAdr getSource() {
|
||||
return _other;
|
||||
}
|
||||
|
||||
/// @brief Read string
|
||||
/// @note This call blocks until data is received
|
||||
/// @return read string
|
||||
std::string read() override {
|
||||
auto data = readRaw();
|
||||
if (data.size() == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string ret((const char *)&data.data()[0]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// @brief Write character to socket
|
||||
/// @param b character to write
|
||||
void write(char c) override {
|
||||
if (!isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int written = send(_socket, &c, 1, 0);
|
||||
if (written < 0) {
|
||||
ESP_LOGE("TCP", "Error occurred during sending: errno %d", errno);
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Write string to specific socket
|
||||
/// @param message payload
|
||||
/// @param dest Destination for message
|
||||
void write(std::string message) override {
|
||||
if (!isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int written = send(_socket, message.c_str(), message.size(), 0);
|
||||
if (written < 0) {
|
||||
ESP_LOGE("TCP", "Error occurred during sending: errno %d", errno);
|
||||
}
|
||||
}
|
||||
|
||||
IpAdr getOther() {
|
||||
return _other;
|
||||
}
|
||||
|
||||
protected:
|
||||
int _socket = 0;
|
||||
IpAdr _other;
|
||||
|
||||
std::vector<uint8_t> readRaw() {
|
||||
uint8_t data[128];
|
||||
int len = recv(_socket, data, sizeof(data) - 1, 0);
|
||||
|
||||
if (len < 0) {
|
||||
ESP_LOGE("TCP", "Error occurred during receiving: errno %d", errno);
|
||||
active = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
data[len] = 0;
|
||||
if (len == 0) {
|
||||
ESP_LOGW("TCP", "Connection closed");
|
||||
active = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
ESP_LOGD("TCP", "Received %d bytes: %s", len, data);
|
||||
std::vector<uint8_t> vec(data, data + len + 1);
|
||||
return vec;
|
||||
}
|
||||
};
|
||||
|
||||
constexpr TcpSession::Config DefaultTcpConfig{
|
||||
.keepAlive = 1,
|
||||
.keepIdle = 5,
|
||||
.keepInterval = 5,
|
||||
.keepCount = 3,
|
||||
};
|
||||
|
||||
constexpr uint16_t TelnetPort = 23;
|
||||
|
||||
/**
|
||||
* @brief TCP-Socket for Telnet-Applications
|
||||
*/
|
||||
class TelnetServer {
|
||||
|
||||
/**
|
||||
* @brief Specialization of TCP-Session for Telnet
|
||||
*/
|
||||
class TelnetSession : public TcpSession {
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
TelnetSession(Config &config, TcpSocket &listener) : TcpSession(config, listener) {
|
||||
}
|
||||
|
||||
/// @brief Read string
|
||||
/// @note This call blocks until data is received
|
||||
/// @return read string
|
||||
std::string read() override {
|
||||
auto data = readRaw();
|
||||
if (data.size() == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
data.pop_back(); // 0-termination is not needed here
|
||||
|
||||
int i = 0;
|
||||
while (data[i] == 0xFF) { // while telnet command
|
||||
i += 3; // skip this command
|
||||
}
|
||||
|
||||
if (i == data.size()) { // only configuration
|
||||
send(_socket, data.data(), data.size(), 0);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string ret((const char *)&data[i]);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
/// @brief Constructor
|
||||
/// @param addrFamily IPV4 or IPV6
|
||||
TelnetServer(int addrFamily)
|
||||
: Local(addrFamily == AF_INET ? IpAdr(INADDR_ANY, TelnetPort) : IpAdr(TelnetPort)),
|
||||
Config(DefaultTcpConfig) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start server
|
||||
*
|
||||
* @param initCall Callback for shell init
|
||||
* @param cyclicCall Callback for cyclic polling
|
||||
*/
|
||||
void run(std::function<void(TcpSession &)> initCall, std::function<void(TcpSession &)> cyclicCall) {
|
||||
TcpSocket socket(Local);
|
||||
socket.bindTo();
|
||||
while (true) {
|
||||
// Run telnet-session as long socket is active else restart socket
|
||||
ESP_LOGI("Telnet", "Socket waiting for connect");
|
||||
TelnetSession session(Config, socket);
|
||||
|
||||
initCall(session);
|
||||
while (session.isActive()) {
|
||||
cyclicCall(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
IpAdr Local;
|
||||
TcpSession::Config Config;
|
||||
};
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* @file udpSockets.hpp
|
||||
* @author Gustice
|
||||
* @brief UDP-Socket wrapper for ESP-IDF socket implementation
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "Task.hpp"
|
||||
#include "esp_log.h"
|
||||
#include "etherCommon.hpp"
|
||||
#include "utils.hpp"
|
||||
#include <cstdarg>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
constexpr bool UDP_WITH_TIMEOUT = false;
|
||||
|
||||
/**
|
||||
* @brief UDP-Socket wrapper for ESP-IDF socket implementation
|
||||
*/
|
||||
class UdpSocket : public ByteStream, public SocketBase {
|
||||
public:
|
||||
/// @brief Constructor for UDP-Socket-Server
|
||||
/// @note Socket not active until call of attach method
|
||||
/// @param addr Server/Client address
|
||||
UdpSocket(const IpAdr &addr)
|
||||
: Local(addr), _other(INADDR_ANY, 0) {
|
||||
_listenSockId = socket(addr.ProtocolFamily, SOCK_DGRAM, addr.IpProtocol);
|
||||
if (_listenSockId < 0) {
|
||||
ESP_LOGE("UDP", "Unable to create socket: errno %d", errno);
|
||||
throw CoreException("Unable to create socket");
|
||||
}
|
||||
ESP_LOGD("UDP", "Socket created");
|
||||
|
||||
if constexpr (NETBUF_RECVINFO && !USE_IPV6) {
|
||||
int enable = 1;
|
||||
lwip_setsockopt(_listenSockId, IPPROTO_IP, IP_PKTINFO, &enable, sizeof(enable));
|
||||
}
|
||||
|
||||
if constexpr (USE_IPV4 && USE_IPV6) {
|
||||
if (addr.ProtocolFamily == AF_INET6) {
|
||||
// Note that by default IPV6 binds to both protocols, it is must be disabled
|
||||
// if both protocols used at the same time (used in CI)
|
||||
int opt = 1;
|
||||
setsockopt(_listenSockId, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
setsockopt(_listenSockId, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (UDP_WITH_TIMEOUT) { // Set timeout if desired
|
||||
struct timeval timeout {
|
||||
.tv_sec = 10,
|
||||
.tv_usec = 0,
|
||||
};
|
||||
setsockopt(_listenSockId, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Destructor
|
||||
~UdpSocket() {
|
||||
active = false;
|
||||
ESP_LOGD("UDP", "Shutting down socket and restarting...");
|
||||
shutdown(_listenSockId, 0);
|
||||
close(_listenSockId);
|
||||
}
|
||||
|
||||
/// @brief Attach to configured socket
|
||||
/// @note Blocks until successful or failed
|
||||
/// @return true if successful
|
||||
bool bindTo() {
|
||||
active = false;
|
||||
int err = bind(_listenSockId, Local.asSockAddrPtr(), sizeof(sockaddr_storage));
|
||||
if (err < 0) {
|
||||
ESP_LOGE("UDP", "Socket unable to bind: errno %d", errno);
|
||||
throw CoreException("Socket unable to bind");
|
||||
}
|
||||
ESP_LOGI("UDP", "Socket bound, port %d", Local.getPort());
|
||||
|
||||
if constexpr (NETBUF_RECVINFO && !USE_IPV6) {
|
||||
_iov.iov_base = _rxBuffer;
|
||||
_iov.iov_len = sizeof(_rxBuffer);
|
||||
_msg.msg_control = _cmsg_buf;
|
||||
_msg.msg_controllen = sizeof(_cmsg_buf);
|
||||
_msg.msg_flags = 0;
|
||||
_msg.msg_iov = &_iov;
|
||||
_msg.msg_iovlen = 1;
|
||||
_msg.msg_name = _other.asSockAddrPtr();
|
||||
_msg.msg_namelen = _other.Size;
|
||||
}
|
||||
|
||||
active = true;
|
||||
return active;
|
||||
}
|
||||
|
||||
/// @brief Read from socket
|
||||
/// @note This call blocks until data is received
|
||||
/// @return read bytes
|
||||
std::vector<uint8_t> read() override {
|
||||
char buffer[256];
|
||||
int len = 0;
|
||||
if constexpr (NETBUF_RECVINFO && !USE_IPV6) {
|
||||
len = recvmsg(_listenSockId, &_msg, 0);
|
||||
} else {
|
||||
socklen_t socklen = _other.Size;
|
||||
len = recvfrom(_listenSockId, buffer, sizeof(buffer) - 1, 0, _other.asSockAddrPtr(), &socklen);
|
||||
}
|
||||
if (len < 0) {
|
||||
ESP_LOGE("UDP", "recvfrom failed: errno %d", errno);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string addrStr;
|
||||
// Get the sender's ip address as string
|
||||
if (Local.ProtocolFamily == PF_INET) {
|
||||
addrStr = _other.getIp();
|
||||
|
||||
if constexpr (NETBUF_RECVINFO && !USE_IPV6) {
|
||||
struct cmsghdr *cmsgtmp;
|
||||
for (cmsgtmp = CMSG_FIRSTHDR(&_msg); cmsgtmp != NULL; cmsgtmp = CMSG_NXTHDR(&_msg, cmsgtmp)) {
|
||||
if (cmsgtmp->cmsg_level == IPPROTO_IP && cmsgtmp->cmsg_type == IP_PKTINFO) {
|
||||
struct in_pktinfo *pktinfo;
|
||||
pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsgtmp);
|
||||
ESP_LOGI("UDP", "dest ip: %s", inet_ntoa(pktinfo->ipi_addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
} // else if (Local.ProtocolFamily == PF_INET6) {
|
||||
// inet6_ntoa_r(((struct sockaddr_in6 *)&_other.addrStore)->sin6_addr, addrStr, sizeof(addrStr) - 1);
|
||||
// }
|
||||
|
||||
ESP_LOGD("UDP", "Received %d bytes from %s:", len, addrStr.c_str());
|
||||
std::vector<uint8_t> ret(buffer, buffer + len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// @brief Write character to socket
|
||||
/// @param b character to write
|
||||
void write(uint8_t b) override {}
|
||||
|
||||
/// @brief Write bytes to socket
|
||||
/// @param data payload as series of bytes
|
||||
void write(std::vector<uint8_t> data) override {
|
||||
write(&data[0], data.size());
|
||||
}
|
||||
|
||||
/// @brief Write string to specific socket
|
||||
/// @param message payload
|
||||
/// @param dest Destination for message
|
||||
void write(std::string &message, IpAdr *dest = nullptr) {
|
||||
auto data = message.c_str();
|
||||
write((uint8_t *)data, message.size() + 1, dest);
|
||||
}
|
||||
|
||||
/// @brief Write string to specific socket or to last source
|
||||
/// @param data payload
|
||||
/// @param size size of data
|
||||
/// @param addr Destination for message (if nullptr it will be send to last source)
|
||||
void write(uint8_t *data, uint size, IpAdr *dest = nullptr) {
|
||||
int err;
|
||||
if (dest == nullptr) {
|
||||
err = sendto(_listenSockId, data, size, 0, _other.asSockAddrPtr(), sizeof(sockaddr_storage));
|
||||
} else {
|
||||
struct sockaddr_in *ipV4 = (struct sockaddr_in *)dest->asSockAddrPtr();
|
||||
// ESP_LOGI("UDP", "EthIP: %s", inet_ntoa(ipV4->sin_addr.s_addr));
|
||||
// ESP_LOGI("UDP", "PORT: %i", ntohs(ipV4->sin_port));
|
||||
err = sendto(_listenSockId, data, size, 0, dest->asSockAddrPtr(), sizeof(sockaddr_storage));
|
||||
}
|
||||
|
||||
if (err < 0) {
|
||||
ESP_LOGE("UDP", "Error occurred during sending: errno %d", errno);
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Standard destination address
|
||||
const IpAdr &Local;
|
||||
|
||||
/// @brief query if stream is still active
|
||||
/// @return true if active
|
||||
bool isValid() {
|
||||
return _listenSockId >= 0;
|
||||
}
|
||||
|
||||
IpAdr getOther() {
|
||||
return _other;
|
||||
}
|
||||
|
||||
private:
|
||||
int _listenSockId; // os handle for socket
|
||||
IpAdr _other; // Remote Station
|
||||
char _rxBuffer[128];
|
||||
|
||||
// Only used if NETBUF_RECVINFO is active
|
||||
iovec _iov;
|
||||
msghdr _msg;
|
||||
u8_t _cmsg_buf[CMSG_SPACE(sizeof(struct in_pktinfo))];
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* @details according to RFC-5424
|
||||
* https://datatracker.ietf.org/doc/html/rfc5424
|
||||
*/
|
||||
class Syslog {
|
||||
public:
|
||||
static constexpr uint16_t SyslogPort = 514;
|
||||
static constexpr std::string_view NilValue = "-";
|
||||
|
||||
enum class Severity {
|
||||
Emergency = 0,
|
||||
Alert = 1,
|
||||
Critical = 2,
|
||||
Error = 3,
|
||||
Warning = 4,
|
||||
Notice = 5,
|
||||
Informational = 6,
|
||||
Debug = 7,
|
||||
};
|
||||
static constexpr uint8_t SeverityMask = 0x000F;
|
||||
|
||||
enum class Facility {
|
||||
kern = 0 << 3,
|
||||
User = 1 << 3,
|
||||
Mail = 2 << 3,
|
||||
Daemon = 3 << 3,
|
||||
Auth = 4 << 3,
|
||||
Syslog = 5 << 3,
|
||||
Lpr = 6 << 3,
|
||||
News = 7 << 3,
|
||||
Uucp = 8 << 3,
|
||||
Cron = 9 << 3,
|
||||
Authpriv = 10 << 3,
|
||||
Ftp = 11 << 3,
|
||||
Ntp = 12 << 3,
|
||||
LogAudit = 13 << 3,
|
||||
LogAlert = 14 << 3,
|
||||
Clock = 15 << 3,
|
||||
Local0 = 16 << 3,
|
||||
Local1 = 17 << 3,
|
||||
Local2 = 18 << 3,
|
||||
Local3 = 19 << 3,
|
||||
Local4 = 20 << 3,
|
||||
Local5 = 21 << 3,
|
||||
Local6 = 22 << 3,
|
||||
Local7 = 23 << 3,
|
||||
};
|
||||
|
||||
static void log(Severity severity, std::string message) {
|
||||
if (_Socket == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
// V T H A P M D Msg
|
||||
// alternatively with BOM flag UTF8 \xEF\xBB\xBF
|
||||
auto pos = sprintf(buffer, "<%i>1 - %s %s - - - %s",
|
||||
static_cast<int>(_Facility) | static_cast<int>(severity),
|
||||
_HostName.c_str(),
|
||||
_AppName.c_str(),
|
||||
message.c_str());
|
||||
_Socket->write((uint8_t *)buffer, pos, &_Target);
|
||||
}
|
||||
|
||||
template <typename...Params>
|
||||
static void logv(Severity severity, const char *fmt, Params&&... args) {
|
||||
if (_Socket == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
// V T H A P M D Msg
|
||||
// alternatively with BOM flag UTF8 \xEF\xBB\xBF
|
||||
auto pos = sprintf(buffer, "<%i>1 - %s %s - - - ",
|
||||
static_cast<int>(_Facility) | static_cast<int>(severity),
|
||||
_HostName.c_str(),
|
||||
_AppName.c_str());
|
||||
pos += sprintf(&buffer[pos], fmt, std::forward<Params> (args)...);
|
||||
|
||||
_Socket->write((uint8_t *)buffer, pos, &_Target);
|
||||
}
|
||||
|
||||
static void setupTask(IpAdr target) {
|
||||
if (_Socket) {
|
||||
return;
|
||||
}
|
||||
_Facility = Facility::Syslog;
|
||||
_HostName = "ESP";
|
||||
_AppName = "App";
|
||||
_Target = IpAdr{target.getIp(), SyslogPort};
|
||||
|
||||
static fos::Task syslogTask(SyslogTask, "SysLog", 4096);
|
||||
}
|
||||
|
||||
private:
|
||||
static void SyslogTask();
|
||||
static Facility _Facility;
|
||||
static std::string _HostName;
|
||||
static std::string _AppName;
|
||||
static UdpSocket *_Socket;
|
||||
static IpAdr _Target;
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @file etherCommon.cpp
|
||||
* @author Gustice
|
||||
* @brief Helper Functions for Ethernet usage
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
|
||||
#include "etherCommon.hpp"
|
||||
#include <cassert>
|
||||
|
||||
IpAdr::IpAdr(std::string ipStr, uint16_t port)
|
||||
: IpAdr(ipaddr_addr(ipStr.c_str()), port) {
|
||||
}
|
||||
|
||||
IpAdr::IpAdr(uint32_t ip, uint16_t port) {
|
||||
struct sockaddr_in *ipV4 = (struct sockaddr_in *)&addrStore;
|
||||
ipV4->sin_len = sizeof(sockaddr_in);
|
||||
ipV4->sin_addr.s_addr = ip;
|
||||
ipV4->sin_family = AF_INET;
|
||||
ipV4->sin_port = htons(port);
|
||||
}
|
||||
|
||||
IpAdr::IpAdr(uint16_t port) {
|
||||
struct sockaddr_in *ipV4 = (struct sockaddr_in *)&addrStore;
|
||||
ipV4->sin_len = sizeof(sockaddr_in);
|
||||
ipV4->sin_addr.s_addr = INADDR_ANY;
|
||||
ipV4->sin_family = AF_INET;
|
||||
ipV4->sin_port = htons(port);
|
||||
}
|
||||
|
||||
IpAdr::IpAdr(sockaddr_in ipV4data) {
|
||||
struct sockaddr_storage *store = (struct sockaddr_storage *)&ipV4data;
|
||||
assert(ipV4data.sin_family == AF_INET);
|
||||
addrStore = *store;
|
||||
}
|
||||
|
||||
|
||||
IpAdrV6::IpAdrV6(std::string ipStr, uint16_t port) {
|
||||
struct sockaddr_in6 *ipV6 = (struct sockaddr_in6 *)&addrStore;
|
||||
ipV6->sin6_len = sizeof(sockaddr_in6);
|
||||
inet6_aton(ipStr.c_str(), &ipV6->sin6_addr);
|
||||
ipV6->sin6_family = AF_INET6;
|
||||
ipV6->sin6_port = htons(port);
|
||||
}
|
||||
|
||||
IpAdrV6::IpAdrV6(in6_addr ip, uint16_t port) {
|
||||
struct sockaddr_in6 *ipV6 = (struct sockaddr_in6 *)&addrStore;
|
||||
ipV6->sin6_len = sizeof(sockaddr_in6);
|
||||
memcpy(&ip, &ipV6->sin6_addr, sizeof(in6_addr));
|
||||
ipV6->sin6_family = AF_INET6;
|
||||
ipV6->sin6_port = htons(port);
|
||||
}
|
||||
|
||||
IpAdrV6::IpAdrV6(uint16_t port) {
|
||||
struct sockaddr_in6 *ipV6 = (struct sockaddr_in6 *)&addrStore;
|
||||
ipV6->sin6_len = sizeof(sockaddr_in6);
|
||||
// ip6_addr_set_any()
|
||||
bzero(&ipV6->sin6_addr.un, sizeof(ipV6->sin6_addr.un));
|
||||
ipV6->sin6_family = AF_INET6;
|
||||
ipV6->sin6_port = htons(port);
|
||||
}
|
||||
|
||||
IpAdrV6::IpAdrV6(sockaddr_in6 ipV6data) {
|
||||
struct sockaddr_storage *store = (struct sockaddr_storage *)&ipV6data;
|
||||
assert(ipV6data.sin6_family == AF_INET);
|
||||
addrStore = *store;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @file tcpSockets.cpp
|
||||
* @author Gustice
|
||||
* @brief Function frames for handling sockets
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
|
||||
#include "tcpSockets.hpp"
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @file udpSockets.cpp
|
||||
* @author Gustice
|
||||
* @brief Function frames for handling sockets
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
|
||||
#include "udpSockets.hpp"
|
||||
|
||||
UdpSocket * Syslog::_Socket;
|
||||
Syslog::Facility Syslog::_Facility;
|
||||
std::string Syslog::_HostName;
|
||||
std::string Syslog::_AppName;
|
||||
IpAdr Syslog::_Target(INADDR_ANY, 0);
|
||||
|
||||
void Syslog::SyslogTask() {
|
||||
ESP_LOGI("SysLog", "Starting Syslog Task");
|
||||
try {
|
||||
while (true) {
|
||||
IpAdr ip(INADDR_ANY, SyslogPort);
|
||||
UdpSocket socket(ip);
|
||||
_Socket = &socket;
|
||||
socket.bindTo();
|
||||
|
||||
while (socket.isActive()) {
|
||||
(void)socket.read();
|
||||
// ignore all incomming data
|
||||
}
|
||||
}
|
||||
_Socket = nullptr;
|
||||
} catch (const std::exception &e) {
|
||||
ESP_LOGE("SysLog", "%s", e.what());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "extern"
|
||||
INCLUDE_DIRS "include" "extern"
|
||||
)
|
||||
+1156
File diff suppressed because it is too large
Load Diff
+312
@@ -0,0 +1,312 @@
|
||||
#ifndef EMBEDDED_CLI_H
|
||||
#define EMBEDDED_CLI_H
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
extern "C" {
|
||||
#else
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#endif
|
||||
|
||||
// cstdint is available only since C++11, so use C header
|
||||
#include <stdint.h>
|
||||
|
||||
// used for proper alignment of cli buffer
|
||||
#if UINTPTR_MAX == 0xFFFF
|
||||
#define CLI_UINT uint16_t
|
||||
#elif UINTPTR_MAX == 0xFFFFFFFF
|
||||
#define CLI_UINT uint32_t
|
||||
#elif UINTPTR_MAX == 0xFFFFFFFFFFFFFFFFu
|
||||
#define CLI_UINT uint64_t
|
||||
#else
|
||||
#error unsupported pointer size
|
||||
#endif
|
||||
|
||||
#define CLI_UINT_SIZE (sizeof(CLI_UINT))
|
||||
// convert size in bytes to size in terms of CLI_UINTs (rounded up
|
||||
// if bytes is not divisible by size of single CLI_UINT)
|
||||
#define BYTES_TO_CLI_UINTS(bytes) \
|
||||
(((bytes) + CLI_UINT_SIZE - 1)/CLI_UINT_SIZE)
|
||||
|
||||
typedef struct CliCommand CliCommand;
|
||||
typedef struct CliCommandBinding CliCommandBinding;
|
||||
typedef struct EmbeddedCli EmbeddedCli;
|
||||
typedef struct EmbeddedCliConfig EmbeddedCliConfig;
|
||||
|
||||
|
||||
struct CliCommand {
|
||||
/**
|
||||
* Name of the command.
|
||||
* In command "set led 1 1" "set" is name
|
||||
*/
|
||||
const char *name;
|
||||
|
||||
/**
|
||||
* String of arguments of the command.
|
||||
* In command "set led 1 1" "led 1 1" is string of arguments
|
||||
* Is ended with double 0x00 char
|
||||
* Use tokenize functions to easily get individual tokens
|
||||
*/
|
||||
char *args;
|
||||
};
|
||||
|
||||
/**
|
||||
* Struct to describe binding of command to function and
|
||||
*/
|
||||
struct CliCommandBinding {
|
||||
/**
|
||||
* Name of command to bind. Should not be NULL.
|
||||
*/
|
||||
const char *name;
|
||||
|
||||
/**
|
||||
* Help string that will be displayed when "help <cmd>" is executed.
|
||||
* Can have multiple lines separated with "\r\n"
|
||||
* Can be NULL if no help is provided.
|
||||
*/
|
||||
const char *help;
|
||||
|
||||
/**
|
||||
* Flag to perform tokenization before calling binding function.
|
||||
*/
|
||||
bool tokenizeArgs;
|
||||
|
||||
/**
|
||||
* Pointer to any specific app context that is required for this binding.
|
||||
* It will be provided in binding callback.
|
||||
*/
|
||||
void *context;
|
||||
|
||||
/**
|
||||
* Binding function for when command is received.
|
||||
* If null, default callback (onCommand) will be called.
|
||||
* @param cli - pointer to cli that is calling this binding
|
||||
* @param args - string of args (if tokenizeArgs is false) or tokens otherwise
|
||||
* @param context
|
||||
*/
|
||||
void (*binding)(EmbeddedCli *cli, char *args, void *context);
|
||||
};
|
||||
|
||||
struct EmbeddedCli {
|
||||
/**
|
||||
* Should write char to connection
|
||||
* @param cli - pointer to cli that executed this function
|
||||
* @param c - actual character to write
|
||||
*/
|
||||
void (*writeChar)(EmbeddedCli *cli, char c);
|
||||
|
||||
/**
|
||||
* Called when command is received and command not found in list of
|
||||
* command bindings (or binding function is null).
|
||||
* @param cli - pointer to cli that executed this function
|
||||
* @param command - pointer to received command
|
||||
*/
|
||||
void (*onCommand)(EmbeddedCli *cli, CliCommand *command);
|
||||
|
||||
/**
|
||||
* Can be used for any application context
|
||||
*/
|
||||
void *appContext;
|
||||
|
||||
/**
|
||||
* Pointer to actual implementation, do not use.
|
||||
*/
|
||||
void *_impl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration to create CLI
|
||||
*/
|
||||
struct EmbeddedCliConfig {
|
||||
/**
|
||||
* Invitation string. Is printed at the beginning of each line with user
|
||||
* input
|
||||
*/
|
||||
const char *invitation;
|
||||
|
||||
/**
|
||||
* Size of buffer that is used to store characters until they're processed
|
||||
*/
|
||||
uint16_t rxBufferSize;
|
||||
|
||||
/**
|
||||
* Size of buffer that is used to store current input that is not yet
|
||||
* sended as command (return not pressed yet)
|
||||
*/
|
||||
uint16_t cmdBufferSize;
|
||||
|
||||
/**
|
||||
* Size of buffer that is used to store previously entered commands
|
||||
* Only unique commands are stored in buffer. If buffer is smaller than
|
||||
* entered command (including arguments), command is discarded from history
|
||||
*/
|
||||
uint16_t historyBufferSize;
|
||||
|
||||
/**
|
||||
* Maximum amount of bindings that can be added via addBinding function.
|
||||
* Cli increases takes extra bindings for internal commands:
|
||||
* - help
|
||||
*/
|
||||
uint16_t maxBindingCount;
|
||||
|
||||
/**
|
||||
* Buffer to use for cli and all internal structures. If NULL, memory will
|
||||
* be allocated dynamically. Otherwise this buffer is used and no
|
||||
* allocations are made
|
||||
*/
|
||||
CLI_UINT *cliBuffer;
|
||||
|
||||
/**
|
||||
* Size of buffer for cli and internal structures (in bytes).
|
||||
*/
|
||||
uint16_t cliBufferSize;
|
||||
|
||||
/**
|
||||
* Whether autocompletion should be enabled.
|
||||
* If false, autocompletion is disabled but you still can use 'tab' to
|
||||
* complete current command manually.
|
||||
*/
|
||||
bool enableAutoComplete;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns pointer to default configuration for cli creation. It is safe to
|
||||
* modify it and then send to embeddedCliNew().
|
||||
* Returned structure is always the same so do not free and try to use it
|
||||
* immediately.
|
||||
* Default values:
|
||||
* <ul>
|
||||
* <li>rxBufferSize = 64</li>
|
||||
* <li>cmdBufferSize = 64</li>
|
||||
* <li>historyBufferSize = 128</li>
|
||||
* <li>cliBuffer = NULL (use dynamic allocation)</li>
|
||||
* <li>cliBufferSize = 0</li>
|
||||
* <li>maxBindingCount = 8</li>
|
||||
* <li>enableAutoComplete = true</li>
|
||||
* </ul>
|
||||
* @return configuration for cli creation
|
||||
*/
|
||||
EmbeddedCliConfig *embeddedCliDefaultConfig(void);
|
||||
|
||||
/**
|
||||
* Returns how many space in config buffer is required for cli creation
|
||||
* If you provide buffer with less space, embeddedCliNew will return NULL
|
||||
* This amount will always be divisible by CLI_UINT_SIZE so allocated buffer
|
||||
* and internal structures can be properly aligned
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
uint16_t embeddedCliRequiredSize(EmbeddedCliConfig *config);
|
||||
|
||||
/**
|
||||
* Create new CLI.
|
||||
* Memory is allocated dynamically if cliBuffer in config is NULL.
|
||||
* After CLI is created, override function pointers to start using it
|
||||
* @param config - config for cli creation
|
||||
* @return pointer to created CLI
|
||||
*/
|
||||
EmbeddedCli *embeddedCliNew(EmbeddedCliConfig *config);
|
||||
|
||||
/**
|
||||
* Same as calling embeddedCliNew with default config.
|
||||
* @return
|
||||
*/
|
||||
EmbeddedCli *embeddedCliNewDefault(void);
|
||||
|
||||
/**
|
||||
* Receive character and put it to internal buffer
|
||||
* Actual processing is done inside embeddedCliProcess
|
||||
* You can call this function from something like interrupt service routine,
|
||||
* just make sure that you call it only from single place. Otherwise input
|
||||
* might get corrupted
|
||||
* @param cli
|
||||
* @param c - received char
|
||||
*/
|
||||
void embeddedCliReceiveChar(EmbeddedCli *cli, char c);
|
||||
|
||||
/**
|
||||
* Process rx/tx buffers. Command callbacks are called from here
|
||||
* @param cli
|
||||
*/
|
||||
void embeddedCliProcess(EmbeddedCli *cli);
|
||||
|
||||
/**
|
||||
* Add specified binding to list of bindings. If list is already full, binding
|
||||
* is not added and false is returned
|
||||
* @param cli
|
||||
* @param binding
|
||||
* @return true if binding was added, false otherwise
|
||||
*/
|
||||
bool embeddedCliAddBinding(EmbeddedCli *cli, CliCommandBinding binding);
|
||||
|
||||
/**
|
||||
* Print specified string and account for currently entered but not submitted
|
||||
* command.
|
||||
* Current command is deleted, provided string is printed (with new line) after
|
||||
* that current command is printed again, so user can continue typing it.
|
||||
* @param cli
|
||||
* @param string
|
||||
*/
|
||||
void embeddedCliPrint(EmbeddedCli *cli, const char *string);
|
||||
|
||||
/**
|
||||
* Free allocated for cli memory
|
||||
* @param cli
|
||||
*/
|
||||
void embeddedCliFree(EmbeddedCli *cli);
|
||||
|
||||
/**
|
||||
* Perform tokenization of arguments string. Original string is modified and
|
||||
* should not be used directly (only inside other token functions).
|
||||
* Individual tokens are separated by single 0x00 char, double 0x00 is put at
|
||||
* the end of token list. After calling this function, you can use other
|
||||
* token functions to get individual tokens and token count.
|
||||
*
|
||||
* Important: Call this function only once. Otherwise information will be lost if
|
||||
* more than one token existed
|
||||
* @param args - string to tokenize (must have extra writable char after 0x00)
|
||||
* @return
|
||||
*/
|
||||
void embeddedCliTokenizeArgs(char *args);
|
||||
|
||||
/**
|
||||
* Return specific token from tokenized string
|
||||
* @param tokenizedStr
|
||||
* @param pos (counted from 1)
|
||||
* @return token
|
||||
*/
|
||||
const char *embeddedCliGetToken(const char *tokenizedStr, uint16_t pos);
|
||||
|
||||
/**
|
||||
* Same as embeddedCliGetToken but works on non-const buffer
|
||||
* @param tokenizedStr
|
||||
* @param pos (counted from 1)
|
||||
* @return token
|
||||
*/
|
||||
char *embeddedCliGetTokenVariable(char *tokenizedStr, uint16_t pos);
|
||||
|
||||
/**
|
||||
* Find token in provided tokens string and return its position (counted from 1)
|
||||
* If no such token is found - 0 is returned.
|
||||
* @param tokenizedStr
|
||||
* @param token - token to find
|
||||
* @return position (increased by 1) or zero if no such token found
|
||||
*/
|
||||
uint16_t embeddedCliFindToken(const char *tokenizedStr, const char *token);
|
||||
|
||||
/**
|
||||
* Return number of tokens in tokenized string
|
||||
* @param tokenizedStr
|
||||
* @return number of tokens
|
||||
*/
|
||||
uint16_t embeddedCliGetTokenCount(const char *tokenizedStr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#endif //EMBEDDED_CLI_H
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Sviatoslav Kokurin (funbiscuit)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# Embedded CLI
|
||||
|
||||
Implementation is taken from `funbiscuit` @ [github](https://github.com/funbiscuit/embedded-cli)
|
||||
|
||||
- Only source and header where integrated.
|
||||
- Full project is included in `external/embedded-cli`
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @file cliWrapper.hpp
|
||||
* @author Gustice
|
||||
* @brief Wrapping Command-Line-Interfaces with custom endpoints
|
||||
* @details Command-Line-Interface is based on the implementation of
|
||||
* https://github.com/funbiscuit/embedded-cli
|
||||
*
|
||||
* @copyright Copyright (c) 2024
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "embedded_cli.h"
|
||||
#include <esp_log.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include "utils.hpp"
|
||||
|
||||
/* Module configuration */
|
||||
static constexpr int CLI_RX_BUFFER_SIZE = 64;
|
||||
static constexpr int CLI_CMD_BUFFER_SIZE = 64; // to also accept URLs for Updates
|
||||
static constexpr int CLI_HISTORY_SIZE = 16;
|
||||
static constexpr int CLI_BINDING_COUNT = 8;
|
||||
static constexpr std::string_view CliWelcomeString{
|
||||
"** Smart-DMX-Bridge **\n"
|
||||
" send 'help' for help-page\n"
|
||||
" NOTE: Syslog-messages are sent to client of this session\n"};
|
||||
|
||||
/**
|
||||
* @brief Wrapper for c-implementation of CLI
|
||||
*/
|
||||
class Cli {
|
||||
public:
|
||||
/// @brief Type for callback arguments
|
||||
using CArgs = std::vector<std::string> &;
|
||||
/// @brief Type for bind callback
|
||||
using BindCb = std::function<void(CArgs)>;
|
||||
/// @brief Type for command callback
|
||||
using CommandCb = std::function<void(std::string, CArgs)>;
|
||||
|
||||
/// @brief Parameter structure for command-binding
|
||||
struct Binding {
|
||||
std::string_view name;
|
||||
std::string_view help_output;
|
||||
BindCb callback;
|
||||
};
|
||||
|
||||
/// @brief Constructor
|
||||
/// @param port Reference to serial port
|
||||
/// @param cmd command callback
|
||||
Cli(CharStream &port, CommandCb cmd)
|
||||
: _port(port), _commandCb(cmd) {
|
||||
EmbeddedCliConfig *config = embeddedCliDefaultConfig();
|
||||
// NOTE: Note setting cliBuffer allows Cli to allocate needed buffer in correct size
|
||||
// config->cliBuffer = cliBuffer;
|
||||
// config->cliBufferSize = CLI_BUFFER_SIZE;
|
||||
config->rxBufferSize = CLI_RX_BUFFER_SIZE;
|
||||
config->cmdBufferSize = CLI_CMD_BUFFER_SIZE;
|
||||
config->historyBufferSize = CLI_HISTORY_SIZE;
|
||||
config->maxBindingCount = CLI_BINDING_COUNT;
|
||||
cli = embeddedCliNew(config);
|
||||
|
||||
if (cli == NULL) {
|
||||
ESP_LOGE("CLI", "Cli was not created, requires %d bytes. Check sizes!",
|
||||
embeddedCliRequiredSize(config));
|
||||
return;
|
||||
}
|
||||
cli->onCommand = genericCallback;
|
||||
cli->writeChar = writeToken;
|
||||
// appContext is a user-end variable and is bound to current instance
|
||||
cli->appContext = this;
|
||||
}
|
||||
|
||||
/// @brief Destructor
|
||||
~Cli() {
|
||||
embeddedCliFree(cli);
|
||||
}
|
||||
|
||||
/// @brief Create command binding
|
||||
bool addBinding(Binding bind) {
|
||||
BindCb *cb = new BindCb{bind.callback};
|
||||
|
||||
CliCommandBinding cBinding{.name = bind.name.begin(),
|
||||
.help = bind.help_output.begin(),
|
||||
.tokenizeArgs = true,
|
||||
// NOTE: this hard reference only works because we only provide
|
||||
// an add-function without remove
|
||||
.context = (void *)(cb),
|
||||
.binding = boundCallback};
|
||||
return embeddedCliAddBinding(cli, cBinding);
|
||||
}
|
||||
|
||||
/// @brief Static method to generate c-compatible function-pointer for bound-command-callback
|
||||
/// @details - receives and wraps arguments in string,
|
||||
/// - determines object according to appContext
|
||||
/// - calls appropriate method of appropriate object
|
||||
static void boundCallback(EmbeddedCli *cli, char *args, void *context) {
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
ESP_LOGE("CLI", "No context in callback available");
|
||||
}
|
||||
ESP_LOGD("CLI", "Received command:");
|
||||
|
||||
BindCb &cb = *((BindCb *)context);
|
||||
uint len = embeddedCliGetTokenCount(args);
|
||||
std::vector<std::string> sArgs;
|
||||
while (len-- > 0) {
|
||||
const char *const arg = embeddedCliGetToken(args, 1);
|
||||
ESP_LOGD("CLI", " arg: %s", arg);
|
||||
sArgs.emplace_back(arg);
|
||||
// shift position to end of this element to save time
|
||||
args += sArgs.back().size() + 1; // needs also to be shifted by null-delimiter
|
||||
}
|
||||
cb(sArgs);
|
||||
}
|
||||
|
||||
/// @brief Static method to generate c-compatible function-pointer for command-callback
|
||||
/// @details - receives and wraps arguments in string,
|
||||
/// - determines object according to appContext
|
||||
/// - calls command-callback of appropriate object
|
||||
static void genericCallback(EmbeddedCli *cli, CliCommand *command) {
|
||||
std::string name(command->name);
|
||||
ESP_LOGI("CLI", "Received generic command: %s", name.c_str());
|
||||
|
||||
embeddedCliTokenizeArgs(command->args);
|
||||
std::vector<std::string> sArgs;
|
||||
for (int i = 1; i <= embeddedCliGetTokenCount(command->args); ++i) {
|
||||
sArgs.emplace_back(embeddedCliGetToken(command->args, i));
|
||||
ESP_LOGI("CLI", " arg %d : %s", i, sArgs.back().c_str());
|
||||
}
|
||||
auto &commandCb = static_cast<Cli *>(cli->appContext)->_commandCb;
|
||||
commandCb(name, sArgs);
|
||||
}
|
||||
|
||||
/// @brief Process buffer and trigger commands
|
||||
void process() {
|
||||
auto r = _port.read();
|
||||
for (auto &&c : r) {
|
||||
embeddedCliReceiveChar(cli, c);
|
||||
}
|
||||
embeddedCliProcess(cli);
|
||||
}
|
||||
|
||||
/// @brief publicly exposed write method
|
||||
void write(std::string output) {
|
||||
_port.write(output);
|
||||
}
|
||||
|
||||
private:
|
||||
/// @brief Reference to hardware port
|
||||
CharStream &_port;
|
||||
/// @brief Callback for generic command
|
||||
CommandCb _commandCb;
|
||||
EmbeddedCli *cli = nullptr;
|
||||
|
||||
/// @brief Write callback
|
||||
static void writeToken(EmbeddedCli *embeddedCli, char c) {
|
||||
static_cast<Cli *>(embeddedCli->appContext)->_port.write(c);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user