diff --git a/.github/workflows/build_libraries.yml b/.github/workflows/build_libraries.yml new file mode 100644 index 000000000..59eca4df0 --- /dev/null +++ b/.github/workflows/build_libraries.yml @@ -0,0 +1,103 @@ +name: Build Host C++ / Python Libraries + +on: + pull_request: + branches: [main] + push: + branches: [main] + release: + types: [published] + workflow_dispatch: + +jobs: + build_windows: + + runs-on: windows-latest + continue-on-error: false + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: 'recursive' + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Build libraries + working-directory: lib/ + run: | + mkdir build + cd build + cmake .. + cmake --build . --config Release --target install + + - name: Upload output folder + uses: actions/upload-artifact@v4 + with: + name: libespp_windows + path: lib/pc + + build_linux: + + runs-on: ubuntu-latest + continue-on-error: false + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: 'recursive' + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Build libraries + working-directory: lib/ + run: | + ./build.sh + + - name: Upload output folder + uses: actions/upload-artifact@v4 + with: + name: libespp_linux + path: lib/pc + + build_macos: + + runs-on: macos-latest + continue-on-error: false + + steps: + - name: Setup XCode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: 'recursive' + fetch-depth: 0 + + - name: Build libraries + working-directory: lib/ + run: | + ./build.sh + + - name: Upload output folder + uses: actions/upload-artifact@v4 + with: + name: libespp_macos + path: lib/pc diff --git a/components/base_component/include/base_component.hpp b/components/base_component/include/base_component.hpp index 39dbb8aef..2d0657965 100644 --- a/components/base_component/include/base_component.hpp +++ b/components/base_component/include/base_component.hpp @@ -29,7 +29,7 @@ class BaseComponent { /// \param level The verbosity level to use for the logger /// \sa Logger::Verbosity /// \sa Logger::set_verbosity - void set_log_level(Logger::Verbosity level) { logger_.set_verbosity(level); } + void set_log_level(espp::Logger::Verbosity level) { logger_.set_verbosity(level); } /// Set the log verbosity for the logger /// \param level The verbosity level to use for the logger @@ -37,7 +37,7 @@ class BaseComponent { /// \sa set_log_level /// \sa Logger::Verbosity /// \sa Logger::set_verbosity - void set_log_verbosity(Logger::Verbosity level) { set_log_level(level); } + void set_log_verbosity(espp::Logger::Verbosity level) { set_log_level(level); } /// Get the log verbosity for the logger /// \return The verbosity level of the logger @@ -58,13 +58,14 @@ class BaseComponent { protected: BaseComponent() = default; - explicit BaseComponent(std::string_view tag, Logger::Verbosity level = Logger::Verbosity::WARN) + explicit BaseComponent(std::string_view tag, + espp::Logger::Verbosity level = espp::Logger::Verbosity::WARN) : logger_({.tag = tag, .level = level}) {} - explicit BaseComponent(const Logger::Config &logger_config) + explicit BaseComponent(const espp::Logger::Config &logger_config) : logger_(logger_config) {} /// The logger for this component - Logger logger_ = espp::Logger({.tag = "BaseComponent", .level = Logger::Verbosity::INFO}); + Logger logger_ = espp::Logger({.tag = "BaseComponent", .level = espp::Logger::Verbosity::INFO}); }; } // namespace espp diff --git a/components/cli/include/cli.hpp b/components/cli/include/cli.hpp index 368c6b0ea..b50cd58f0 100644 --- a/components/cli/include/cli.hpp +++ b/components/cli/include/cli.hpp @@ -1,5 +1,6 @@ #pragma once +#if defined(ESP_PLATFORM) #include #if CONFIG_COMPILER_CXX_EXCEPTIONS || defined(_DOXYGEN_) @@ -13,8 +14,6 @@ #include "esp_vfs_dev.h" #include "esp_vfs_usb_serial_jtag.h" -#include - #include "line_input.hpp" #ifdef CONFIG_ESP_CONSOLE_USB_CDC @@ -26,6 +25,8 @@ #define STRINGIFY2(s) #s #endif // STRINGIFY +#include + namespace espp { /** * @brief Class for implementing a basic Cli using the external cli library. @@ -388,3 +389,5 @@ class Cli : private cli::CliSession { } // namespace espp #endif // CONFIG_COMPILER_CXX_EXCEPTIONS + +#endif // ESP_PLATFORM diff --git a/components/cli/include/line_input.hpp b/components/cli/include/line_input.hpp index 3ca225884..5687a8de2 100644 --- a/components/cli/include/line_input.hpp +++ b/components/cli/include/line_input.hpp @@ -1,3 +1,7 @@ +#pragma once + +#if defined(ESP_PLATFORM) + #include #if CONFIG_COMPILER_CXX_EXCEPTIONS || defined(_DOXYGEN_) @@ -5,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -424,3 +429,5 @@ class LineInput { } // namespace espp #endif // CONFIG_COMPILER_CXX_EXCEPTIONS + +#endif // ESP_PLATFORM diff --git a/components/color/include/color.hpp b/components/color/include/color.hpp index d4b0597de..e774a550e 100644 --- a/components/color/include/color.hpp +++ b/components/color/include/color.hpp @@ -84,6 +84,10 @@ class Rgb { */ Rgb &operator+=(const Rgb &rhs); + bool operator==(const Rgb &rhs) const = default; + + bool operator!=(const Rgb &rhs) const = default; + /** * @brief Get a HSV representation of this RGB color. * @return An HSV object containing the HSV representation. @@ -135,6 +139,10 @@ class Hsv { */ Hsv &operator=(const Hsv &other) = default; + bool operator==(const Hsv &rhs) const = default; + + bool operator!=(const Hsv &rhs) const = default; + /** * @brief Assign the values of the provided Rgb object to this Hsv object. * @param rgb The Rgb object to convert and copy. @@ -156,66 +164,6 @@ class Hsv { return fg(fmt::rgb(rgb.r * 255, rgb.g * 255, rgb.b * 255)); } -// equality operators -[[maybe_unused]] static bool operator==(const Rgb &lhs, const Rgb &rhs) { - return lhs.r == rhs.r && lhs.g == rhs.g && lhs.b == rhs.b; -} - -[[maybe_unused]] static bool operator==(const Hsv &lhs, const Hsv &rhs) { - return lhs.h == rhs.h && lhs.s == rhs.s && lhs.v == rhs.v; -} - -// inequality operators -[[maybe_unused]] static bool operator!=(const Rgb &lhs, const Rgb &rhs) { return !(lhs == rhs); } - -[[maybe_unused]] static bool operator!=(const Hsv &lhs, const Hsv &rhs) { return !(lhs == rhs); } } // namespace espp -#include "format.hpp" - -// for allowing easy serialization/printing of the -// Rgb -template <> struct fmt::formatter { - // Presentation format: 'f' - floating [0,1] (default), 'd' - integer [0,255], 'x' - hex integer. - char presentation = 'f'; - - template constexpr auto parse(ParseContext &ctx) { - // Parse the presentation format and store it in the formatter: - auto it = ctx.begin(), end = ctx.end(); - if (it != end && (*it == 'f' || *it == 'd' || *it == 'x')) - presentation = *it++; - - // TODO: Check if reached the end of the range: - // if (it != end && *it != '}') throw format_error("invalid format"); - - // Return an iterator past the end of the parsed range: - return it; - } - - template auto format(espp::Rgb const &rgb, FormatContext &ctx) const { - switch (presentation) { - case 'f': - return fmt::format_to(ctx.out(), "({}, {}, {})", rgb.r, rgb.g, rgb.b); - case 'd': - return fmt::format_to(ctx.out(), "({}, {}, {})", static_cast(rgb.r * 255), - static_cast(rgb.g * 255), static_cast(rgb.b * 255)); - case 'x': - return fmt::format_to(ctx.out(), "{:#08X}", rgb.hex()); - default: - // shouldn't get here! - return fmt::format_to(ctx.out(), "({}, {}, {})", rgb.r, rgb.g, rgb.b); - } - } -}; - -// for allowing easy serialization/printing of the -// Rgb -template <> struct fmt::formatter { - template constexpr auto parse(ParseContext &ctx) const { - return ctx.begin(); - } - - template auto format(espp::Hsv const &hsv, FormatContext &ctx) const { - return fmt::format_to(ctx.out(), "({}, {}, {})", hsv.h, hsv.s, hsv.v); - } -}; +#include "color_formatters.hpp" diff --git a/components/color/include/color_formatters.hpp b/components/color/include/color_formatters.hpp new file mode 100644 index 000000000..a43949f27 --- /dev/null +++ b/components/color/include/color_formatters.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "format.hpp" + +// for allowing easy serialization/printing of the +// Rgb +template <> struct fmt::formatter { + // Presentation format: 'f' - floating [0,1] (default), 'd' - integer [0,255], 'x' - hex integer. + char presentation = 'f'; + + template constexpr auto parse(ParseContext &ctx) { + // Parse the presentation format and store it in the formatter: + auto it = ctx.begin(), end = ctx.end(); + if (it != end && (*it == 'f' || *it == 'd' || *it == 'x')) + presentation = *it++; + + // TODO: Check if reached the end of the range: + // if (it != end && *it != '}') throw format_error("invalid format"); + + // Return an iterator past the end of the parsed range: + return it; + } + + template auto format(espp::Rgb const &rgb, FormatContext &ctx) const { + switch (presentation) { + case 'f': + return fmt::format_to(ctx.out(), "({}, {}, {})", rgb.r, rgb.g, rgb.b); + case 'd': + return fmt::format_to(ctx.out(), "({}, {}, {})", static_cast(rgb.r * 255), + static_cast(rgb.g * 255), static_cast(rgb.b * 255)); + case 'x': + return fmt::format_to(ctx.out(), "{:#08X}", rgb.hex()); + default: + // shouldn't get here! + return fmt::format_to(ctx.out(), "({}, {}, {})", rgb.r, rgb.g, rgb.b); + } + } +}; + +// for allowing easy serialization/printing of the +// Hsv +template <> struct fmt::formatter { + template constexpr auto parse(ParseContext &ctx) const { + return ctx.begin(); + } + + template auto format(espp::Hsv const &hsv, FormatContext &ctx) const { + return fmt::format_to(ctx.out(), "({}, {}, {})", hsv.h, hsv.s, hsv.v); + } +}; diff --git a/components/event_manager/include/event_manager.hpp b/components/event_manager/include/event_manager.hpp index 52c1c918e..816ea46de 100644 --- a/components/event_manager/include/event_manager.hpp +++ b/components/event_manager/include/event_manager.hpp @@ -34,7 +34,7 @@ namespace espp { * \section event_manager_ex1 Event Manager Example * \snippet event_manager_example.cpp event manager example */ -class EventManager : public BaseComponent { +class EventManager : public espp::BaseComponent { public: /** * @brief Function definition for function prototypes to be called when @@ -80,7 +80,8 @@ class EventManager : public BaseComponent { * registered for that component. */ bool add_subscriber(const std::string &topic, const std::string &component, - const event_callback_fn &callback, const size_t stack_size_bytes = 8 * 1024); + const espp::EventManager::event_callback_fn &callback, + const size_t stack_size_bytes = 8192); /** * @brief Register a subscriber for \p component on \p topic. @@ -96,7 +97,8 @@ class EventManager : public BaseComponent { * registered for that component. */ bool add_subscriber(const std::string &topic, const std::string &component, - const event_callback_fn &callback, const Task::BaseConfig &task_config); + const espp::EventManager::event_callback_fn &callback, + const espp::Task::BaseConfig &task_config); /** * @brief Publish \p data on \p topic. @@ -128,7 +130,7 @@ class EventManager : public BaseComponent { protected: EventManager() - : BaseComponent("Event Manager") {} + : espp::BaseComponent("Event Manager") {} struct SubscriberData { std::mutex m; diff --git a/components/file_system/include/file_system.hpp b/components/file_system/include/file_system.hpp index 412ae2baa..329baa309 100644 --- a/components/file_system/include/file_system.hpp +++ b/components/file_system/include/file_system.hpp @@ -9,15 +9,18 @@ #include #include -#include +#include #include -#include #include -#include -#include +#if defined(ESP_PLATFORM) +#include +#include +#include #include +#include +#endif // ESP_PLATFORM #include "base_component.hpp" @@ -43,8 +46,9 @@ namespace espp { /// \snippet file_system_example.cpp file_system posix example /// \section fs_ex3 File System Info std::filesystem Example /// \snippet file_system_example.cpp file_system std filesystem example -class FileSystem : public BaseComponent { +class FileSystem : public espp::BaseComponent { public: +#if defined(ESP_PLATFORM) || defined(_DOXYGEN_) /// @brief Set whether to mount the file system as read only /// @param read_only Whether the file system is mounted as read only /// @note This only has an effect if called before the file system is mounted, @@ -64,6 +68,7 @@ class FileSystem : public BaseComponent { /// @brief Get whether the file system was grown on mount /// @return Whether the file system was grown on mount static bool is_grow_on_mount() { return grow_on_mount_; } +#endif // ESP_PLATFORM /// @brief Get a human readable string for a byte size /// @details @@ -74,24 +79,26 @@ class FileSystem : public BaseComponent { /// @return The human readable string static std::string human_readable(size_t bytes); +#if defined(ESP_PLATFORM) || defined(_DOXYGEN_) /// @brief Get the partition label /// @return The partition label static const char *get_partition_label() { return CONFIG_ESPP_FILE_SYSTEM_PARTITION_LABEL; } +#endif // ESP_PLATFORM /// @brief Get the mount point /// @details /// The mount point is the root directory of the file system. /// It is the root directory of the partition with the partition label. - /// @see get_root_path() and get_partition_label() + /// @see get_root_path() /// @return The mount point - static std::string get_mount_point() { return "/" + std::string{get_partition_label()}; } + static std::string get_mount_point(); /// @brief Get the root path /// @details /// The root path is the root directory of the file system. - /// @see get_mount_point() and get_partition_label() + /// @see get_mount_point() /// @return The root path - static std::filesystem::path get_root_path() { return std::filesystem::path{get_mount_point()}; } + static std::filesystem::path get_root_path(); /// @brief Convert file permissions to a string /// @details This method converts file permissions to a string in the format "rwxrwxrwx". @@ -249,10 +256,7 @@ class FileSystem : public BaseComponent { /// @brief Constructor /// @details /// The constructor is private to ensure that the class is a singleton. - FileSystem() - : BaseComponent("FileSystem") { - init(); - } + FileSystem(); /// @brief Initialize the file system /// @details diff --git a/components/file_system/src/file_system.cpp b/components/file_system/src/file_system.cpp index e6eb9b36a..f213eb4eb 100644 --- a/components/file_system/src/file_system.cpp +++ b/components/file_system/src/file_system.cpp @@ -5,22 +5,75 @@ using namespace espp; bool FileSystem::read_only_ = false; bool FileSystem::grow_on_mount_ = true; +FileSystem::FileSystem() + : espp::BaseComponent("FileSystem") { + init(); +} + +std::string FileSystem::get_mount_point() { +#if defined(ESP_PLATFORM) + return "/" + std::string{get_partition_label()}; +#else + // return the current working directory + return std::filesystem::current_path().string(); +#endif +} + +std::filesystem::path FileSystem::get_root_path() { +#if defined(ESP_PLATFORM) + return std::filesystem::path{get_mount_point()}; +#else + // get current working directory + return std::filesystem::current_path(); +#endif +} + size_t FileSystem::get_free_space() const { +#if defined(ESP_PLATFORM) size_t total, used; esp_littlefs_info(get_partition_label(), &total, &used); return total - used; +#else + // use std::filesystem to get free space + std::error_code ec; + auto space = std::filesystem::space(get_root_path(), ec); + if (ec) { + return 0; + } + return space.free; +#endif } size_t FileSystem::get_total_space() const { +#if defined(ESP_PLATFORM) size_t total, used; esp_littlefs_info(get_partition_label(), &total, &used); return total; +#else + // use std::filesystem to get total space + std::error_code ec; + auto space = std::filesystem::space(get_root_path(), ec); + if (ec) { + return 0; + } + return space.capacity; +#endif } size_t FileSystem::get_used_space() const { +#if defined(ESP_PLATFORM) size_t total, used; esp_littlefs_info(get_partition_label(), &total, &used); return used; +#else + // use std::filesystem to get used space + std::error_code ec; + auto space = std::filesystem::space(get_root_path(), ec); + if (ec) { + return 0; + } + return space.capacity - space.free; +#endif } std::string FileSystem::human_readable(size_t bytes) { @@ -85,6 +138,7 @@ std::vector FileSystem::get_files_in_path(const std::file return {}; } std::vector files; +#if defined(ESP_PLATFORM) // NOTE: we cannot use std::filesystem::directory_iterator because it is not implemented in // esp-idf DIR *dir = opendir(path.c_str()); @@ -118,6 +172,26 @@ std::vector FileSystem::get_files_in_path(const std::file } } closedir(dir); +#else + for (const auto &entry : fs::directory_iterator(path)) { + auto file_path = entry.path(); + file_status = fs::status(file_path, ec); + if (ec) { + logger_.warn("Failed to get status for file: {}", file_path.string()); + } + if (fs::is_directory(file_status)) { + if (include_directories) { + files.push_back(file_path); + } + if (recursive) { + auto sub_files = get_files_in_path(file_path, include_directories, recursive); + files.insert(files.end(), sub_files.begin(), sub_files.end()); + } + } else { + files.push_back(file_path); + } + } +#endif return files; } @@ -138,12 +212,20 @@ bool FileSystem::remove(const std::filesystem::path &path, std::error_code &ec) bool FileSystem::remove_file(const std::filesystem::path &path) { logger_.debug("Removing file: {}", path.string()); +#if defined(ESP_PLATFORM) return unlink(path.c_str()) == 0; +#else + return std::filesystem::remove(path); +#endif } bool FileSystem::remove_directory(const std::filesystem::path &path) { logger_.debug("Removing directory: {}", path.string()); +#if defined(ESP_PLATFORM) return rmdir(path.c_str()) == 0; +#else + return std::filesystem::remove(path); +#endif } bool FileSystem::remove_contents(const std::filesystem::path &path, std::error_code &ec) { @@ -164,6 +246,7 @@ bool FileSystem::remove_contents(const std::filesystem::path &path, std::error_c std::string FileSystem::list_directory(const std::string &path, const ListConfig &config, const std::string &prefix) { std::string result; +#if defined(ESP_PLATFORM) DIR *dir = opendir(path.c_str()); if (dir == nullptr) { return result; @@ -226,10 +309,59 @@ std::string FileSystem::list_directory(const std::string &path, const ListConfig } } closedir(dir); +#else + for (const auto &entry : std::filesystem::directory_iterator(path)) { + auto file_path = entry.path(); + auto file_status = std::filesystem::status(file_path); + if (config.type) { + if (std::filesystem::is_directory(file_status)) { + result += "d"; + } else if (std::filesystem::is_regular_file(file_status)) { + result += "-"; + } else { + result += "?"; + } + } + if (config.permissions) { + auto perms = file_status.permissions(); + result += to_string(perms); + result += " "; + } + if (config.number_of_links) { + result += "1 "; + } + if (config.owner) { + result += "owner "; + } + if (config.group) { + result += "group "; + } + if (config.size) { + if (std::filesystem::is_regular_file(file_status)) { + result += fmt::format("{:>8} ", human_readable(std::filesystem::file_size(file_path))); + } else { + result += fmt::format("{:>8} ", ""); + } + } + if (config.date_time) { + result += fmt::format("{:>12} ", get_file_time_as_string(file_path)); + } + std::string relative_name = ""; + if (prefix.size()) + relative_name += prefix + "/"; + relative_name += entry.path().filename().string(); + result += relative_name; + result += "\r\n"; + if (config.recursive && std::filesystem::is_directory(file_status)) { + result += list_directory(file_path, config, relative_name); + } + } +#endif return result; } void FileSystem::init() { +#if defined(ESP_PLATFORM) logger_.debug("Initializing file system"); esp_err_t err; @@ -270,4 +402,7 @@ void FileSystem::init() { logger_.debug("Creating root directory"); std::filesystem::create_directory(root_path); } +#else + logger_.debug("Not on ESP platform, no need to initialize file system"); +#endif } diff --git a/components/ftp/CMakeLists.txt b/components/ftp/CMakeLists.txt index bf0d7d50b..d55a33741 100644 --- a/components/ftp/CMakeLists.txt +++ b/components/ftp/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register( INCLUDE_DIRS "include" - REQUIRES base_component task socket) + REQUIRES base_component file_system task socket) diff --git a/components/ftp/example/partitions.csv b/components/ftp/example/partitions.csv index e280b0510..419600340 100644 --- a/components/ftp/example/partitions.csv +++ b/components/ftp/example/partitions.csv @@ -2,4 +2,4 @@ nvs, data, nvs, 0x9000, 0x6000 phy_init, data, phy, 0xf000, 0x1000 factory, app, factory, 0x10000, 2M -littlefs, data, spiffs, , 2M +littlefs, data, littlefs, , 1M diff --git a/components/ftp/include/ftp_client.hpp b/components/ftp/include/ftp_client.hpp index 522252288..44fa8b3d4 100644 --- a/components/ftp/include/ftp_client.hpp +++ b/components/ftp/include/ftp_client.hpp @@ -165,12 +165,12 @@ class FtpClient : public BaseComponent { command_string += "\r\n"; logger_.debug("Sending command:\n{}", command_string); std::string response_string; - detail::TcpTransmitConfig config{ + TcpSocket::TransmitConfig config{ .wait_for_response = true, .response_size = 1024, // in bytes .on_response_callback = [&response_string](const std::string_view data) { response_string += data; }, - .response_timeout = 1.0f, // in seconds + .response_timeout = std::chrono::duration(1.0f), // in seconds }; if (!control_socket_.transmit(command_string, config)) { logger_.error("Failed to send command"); @@ -200,7 +200,7 @@ class FtpClient : public BaseComponent { return false; } // get the code - std::string code_string = response_line.substr(0, 3); + std::string code_string = std::string(response_line.substr(0, 3)); // parse the code string without using stoi since it throws exceptions // and we don't want to use exceptions in this library code = 0; diff --git a/components/ftp/include/ftp_client_session.hpp b/components/ftp/include/ftp_client_session.hpp index 7a014dc36..aaf3b0406 100644 --- a/components/ftp/include/ftp_client_session.hpp +++ b/components/ftp/include/ftp_client_session.hpp @@ -10,7 +10,6 @@ #include #include -#include #include #if defined(ESP_PLATFORM) @@ -20,31 +19,17 @@ #endif #include "base_component.hpp" +#include "file_system.hpp" #include "task.hpp" #include "tcp_socket.hpp" -/// Function to convert a time_point to a time_t. -/// \details This function converts a time_point to a time_t. This function -/// is needed because the standard library does not provide a function to -/// convert a time_point to a time_t (until c++20 but support seems lacking -/// on esp32). This function is taken from -/// https://stackoverflow.com/a/61067330 -/// \tparam TP The type of the time_point. -/// \param tp The time_point to convert. -/// \return The time_t. -template std::time_t to_time_t(TP tp) { - using namespace std::chrono; - auto sctp = time_point_cast(tp - TP::clock::now() + system_clock::now()); - return system_clock::to_time_t(sctp); -} - namespace espp { /// Class representing a client that is connected to the FTP server. This /// class is used by the FtpServer class to handle the client's requests. class FtpClientSession : public BaseComponent { public: explicit FtpClientSession(int id, std::string_view local_address, - std::unique_ptr socket, + std::unique_ptr socket, const std::filesystem::path &root_path) : BaseComponent("FtpClientSession " + std::to_string(id)) , id_(id) @@ -166,7 +151,7 @@ class FtpClientSession : public BaseComponent { bool send_response(int status_code, std::string_view message, bool multiline = false) { std::string response = std::to_string(status_code) + (multiline ? "-" : " ") + std::string{message} + "\r\n"; - detail::TcpTransmitConfig config{}; // default config, no wait for response. + TcpSocket::TransmitConfig config{}; // default config, no wait for response. if (!socket_->transmit(response, config)) { logger_.error("Failed to send response"); return false; @@ -249,7 +234,7 @@ class FtpClientSession : public BaseComponent { } } // send the data - detail::TcpTransmitConfig config{}; + TcpSocket::TransmitConfig config{}; bool success = data_socket_->transmit(data, config); // close the data socket data_socket_->close(); @@ -354,7 +339,7 @@ class FtpClientSession : public BaseComponent { } } - detail::TcpTransmitConfig config{}; + TcpSocket::TransmitConfig config{}; // open the file std::ifstream file(file_path, std::ios::in | std::ios::binary); if (!file.is_open()) { @@ -790,7 +775,9 @@ class FtpClientSession : public BaseComponent { } // get the directory listing - std::string directory_listing = list_directory(current_directory_); + espp::FileSystem::ListConfig config{}; + std::string directory_listing = + espp::FileSystem::get().list_directory(current_directory_, config); logger_.debug("Directory listing:\n{}", directory_listing); if (!send_data(directory_listing)) { @@ -925,12 +912,11 @@ class FtpClientSession : public BaseComponent { if (!std::filesystem::is_regular_file(full_path)) { return send_response(550, "Not a regular file."); } - // NOTE: cannot use std::filesystem::remove because it does not work on - // littlefs, it calls POSIX remove() which is not implemented - // properly. Instead, we use unlink() directly. - if (unlink(full_path.string().data()) != 0) { - logger_.error("Failed to delete file: {}", strerror(errno)); - return send_response(550, "Failed to delete file."); + std::error_code ec; + espp::FileSystem::get().remove(full_path, ec); + if (ec) { + logger_.error("Failed to delete file: {}", ec.message()); + return send_response(550, fmt::format("Failed to delete file: {}", ec.message())); } return send_response(250, "Requested file action okay, completed."); } @@ -954,12 +940,11 @@ class FtpClientSession : public BaseComponent { if (!std::filesystem::is_directory(full_path)) { return send_response(550, "Not a directory."); } - // NOTE: cannot use std::filesystem::remove because it does not work on - // littlefs, it calls POSIX remove() which is not implemented - // properly. Instead, we use rmdir() directly. - if (rmdir(full_path.string().data()) != 0) { - logger_.error("Failed to delete directory: {}", strerror(errno)); - return send_response(550, "Failed to delete directory."); + std::error_code ec; + espp::FileSystem::get().remove(full_path, ec); + if (ec) { + logger_.error("Failed to delete directory: {}", ec.message()); + return send_response(550, fmt::format("Failed to delete directory: {}", ec.message())); } return send_response(250, "Requested file action okay, completed."); } @@ -1051,95 +1036,6 @@ class FtpClientSession : public BaseComponent { return send_response(500, "Syntax error, command unrecognized."); } - /// @brief List the contents of a directory - /// @param path The path to list - /// @return A string containing the contents of the directory - std::string list_directory(const std::filesystem::path &path) { - return list_directory(path.string()); - } - - /// @brief List the contents of a directory - /// @param path The path to list - /// @return A string containing the contents of the directory - std::string list_directory(const std::string &path) { - std::string result; - DIR *dir = opendir(path.c_str()); - if (dir == nullptr) { - return result; - } - struct dirent *entry; - namespace fs = std::filesystem; - while ((entry = readdir(dir)) != nullptr) { - // skip the current and parent directories - if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { - continue; - } - // use the config to determine output - std::error_code ec; - auto file_path = fs::path{path} / entry->d_name; - auto file_status = fs::status(file_path, ec); - if (ec) { - logger_.warn("Failed to get status for file: {}", file_path.string()); - continue; - } - - // type - if (fs::is_directory(file_status)) { - result += "d"; - } else if (fs::is_regular_file(file_status)) { - result += "-"; - } else { - result += "?"; - } - - // permissions - auto perms = file_status.permissions(); - result += (perms & fs::perms::owner_read) != fs::perms::none ? "r" : "-"; - result += (perms & fs::perms::owner_write) != fs::perms::none ? "w" : "-"; - result += (perms & fs::perms::owner_exec) != fs::perms::none ? "x" : "-"; - result += (perms & fs::perms::group_read) != fs::perms::none ? "r" : "-"; - result += (perms & fs::perms::group_write) != fs::perms::none ? "w" : "-"; - result += (perms & fs::perms::group_exec) != fs::perms::none ? "x" : "-"; - result += (perms & fs::perms::others_read) != fs::perms::none ? "r" : "-"; - result += (perms & fs::perms::others_write) != fs::perms::none ? "w" : "-"; - result += (perms & fs::perms::others_exec) != fs::perms::none ? "x" : "-"; - result += " "; - - // number of links - result += "1 "; - - // owner and group - result += "owner "; - result += "group "; - - // size - if (fs::is_regular_file(file_status)) { - result += fmt::format("{:>8} ", fs::file_size(file_path, ec)); - } else { - result += fmt::format("{:>7}0 ", ""); - } - - // date - auto ftime = fs::last_write_time(file_path, ec); - if (ec) { - result += "Jan 01 00:00 "; - } else { - // NOTE: std::chrono::system_clock::to_time_t is not implemented in ESP-IDF - // auto cftime = std::chrono::system_clock::to_time_t(ftime); - auto cftime = to_time_t(ftime); - std::tm tm = *std::localtime(&cftime); - char buffer[80]; - std::strftime(buffer, sizeof(buffer), "%b %d %H:%M", &tm); - result += fmt::format("{:>12} ", buffer); - } - - result += entry->d_name; - result += "\r\n"; - } - closedir(dir); - return result; - } - private: int id_; diff --git a/components/i2c/include/i2c.hpp b/components/i2c/include/i2c.hpp index 165e6df17..72387e1b2 100644 --- a/components/i2c/include/i2c.hpp +++ b/components/i2c/include/i2c.hpp @@ -6,7 +6,7 @@ #include #include "base_component.hpp" -#include "task.hpp" +#include "run_on_core.hpp" #include "i2c_format_helpers.hpp" @@ -89,7 +89,7 @@ class I2c : public espp::BaseComponent { auto install_fn = [i2c_port]() -> esp_err_t { return i2c_driver_install(i2c_port, I2C_MODE_MASTER, 0, 0, 0); }; - err = espp::Task::run_on_core(install_fn, config_.isr_core_id); + err = espp::task::run_on_core(install_fn, config_.isr_core_id); if (err != ESP_OK) { logger_.error("install i2c driver failed {}", esp_err_to_name(err)); ec = std::make_error_code(std::errc::io_error); diff --git a/components/interrupt/include/interrupt.hpp b/components/interrupt/include/interrupt.hpp index 882d38168..a469e9f13 100644 --- a/components/interrupt/include/interrupt.hpp +++ b/components/interrupt/include/interrupt.hpp @@ -12,6 +12,7 @@ #include "freertos/queue.h" #include "base_component.hpp" +#include "run_on_core.hpp" #include "task.hpp" namespace espp { @@ -231,7 +232,7 @@ class Interrupt : public BaseComponent { return; } auto install_fn = []() -> esp_err_t { return gpio_install_isr_service(0); }; - auto err = espp::Task::run_on_core(install_fn, core_id); + auto err = espp::task::run_on_core(install_fn, core_id); if (err != ESP_OK) { logger_.error("Failed to install ISR service: {}", esp_err_to_name(err)); return; diff --git a/components/logger/include/logger.hpp b/components/logger/include/logger.hpp index 6d9fa582e..2fca32269 100644 --- a/components/logger/include/logger.hpp +++ b/components/logger/include/logger.hpp @@ -13,6 +13,14 @@ #include "format.hpp" +// Undefine the logger verbosity levels to avoid conflicts with windows / msvc +#ifdef _MSC_VER +#undef ERROR +#undef WARN +#undef INFO +#undef DEBUG +#endif + namespace espp { /** @@ -67,10 +75,11 @@ class Logger { struct Config { std::string_view tag; /**< The TAG that will be prepended to all logs. */ bool include_time{true}; /**< Include the time in the log. */ - std::chrono::duration rate_limit{ - 0}; /**< The rate limit for the logger. Optional, if <= 0 no rate limit. @note Only calls - that have _rate_limited suffixed will be rate limited. */ - Verbosity level = Verbosity::WARN; /**< The verbosity level for the logger. */ + std::chrono::duration rate_limit = + std::chrono::duration(0); /**< The rate limit for the logger. Optional, if <= 0 no +rate limit. @note Only calls that have _rate_limited suffixed will be rate limited. */ + espp::Logger::Verbosity level = + espp::Logger::Verbosity::WARN; /**< The verbosity level for the logger. */ }; /** @@ -89,13 +98,13 @@ class Logger { * \sa Logger::Verbosity * */ - Verbosity get_verbosity() const { return level_; } + espp::Logger::Verbosity get_verbosity() const { return level_; } /** * @brief Change the verbosity for the logger. \sa Logger::Verbosity * @param level new verbosity level */ - void set_verbosity(const Verbosity level) { level_ = level; } + void set_verbosity(const espp::Logger::Verbosity level) { level_ = level; } /** * @brief Change the tag for the logger. @@ -152,7 +161,7 @@ class Logger { */ template void debug(std::string_view rt_fmt_str, Args &&...args) { #if ESPP_LOGGER_DEBUG_ENABLED - if (level_ > Verbosity::DEBUG) + if (level_ > espp::Logger::Verbosity::DEBUG) return; auto msg = format(rt_fmt_str, std::forward(args)...); if (include_time_) { @@ -172,7 +181,7 @@ class Logger { */ template void info(std::string_view rt_fmt_str, Args &&...args) { #if ESPP_LOGGER_INFO_ENABLED - if (level_ > Verbosity::INFO) + if (level_ > espp::Logger::Verbosity::INFO) return; auto msg = format(rt_fmt_str, std::forward(args)...); if (include_time_) { @@ -192,7 +201,7 @@ class Logger { */ template void warn(std::string_view rt_fmt_str, Args &&...args) { #if ESPP_LOGGER_WARN_ENABLED - if (level_ > Verbosity::WARN) + if (level_ > espp::Logger::Verbosity::WARN) return; auto msg = format(rt_fmt_str, std::forward(args)...); if (include_time_) { @@ -212,7 +221,7 @@ class Logger { */ template void error(std::string_view rt_fmt_str, Args &&...args) { #if ESPP_LOGGER_ERROR_ENABLED - if (level_ > Verbosity::ERROR) + if (level_ > espp::Logger::Verbosity::ERROR) return; auto msg = format(rt_fmt_str, std::forward(args)...); if (include_time_) { @@ -234,7 +243,7 @@ class Logger { */ template void debug_rate_limited(std::string_view rt_fmt_str, Args &&...args) { #if ESPP_LOGGER_DEBUG_ENABLED - if (level_ > Verbosity::DEBUG) + if (level_ > espp::Logger::Verbosity::DEBUG) return; if (rate_limit_ > std::chrono::duration::zero()) { auto now = std::chrono::high_resolution_clock::now(); @@ -256,7 +265,7 @@ class Logger { */ template void info_rate_limited(std::string_view rt_fmt_str, Args &&...args) { #if ESPP_LOGGER_INFO_ENABLED - if (level_ > Verbosity::INFO) + if (level_ > espp::Logger::Verbosity::INFO) return; if (rate_limit_ > std::chrono::duration::zero()) { auto now = std::chrono::high_resolution_clock::now(); @@ -278,7 +287,7 @@ class Logger { */ template void warn_rate_limited(std::string_view rt_fmt_str, Args &&...args) { #if ESPP_LOGGER_WARN_ENABLED - if (level_ > Verbosity::WARN) + if (level_ > espp::Logger::Verbosity::WARN) return; if (rate_limit_ > std::chrono::duration::zero()) { auto now = std::chrono::high_resolution_clock::now(); @@ -300,7 +309,7 @@ class Logger { */ template void error_rate_limited(std::string_view rt_fmt_str, Args &&...args) { #if ESPP_LOGGER_ERROR_ENABLED - if (level_ > Verbosity::ERROR) + if (level_ > espp::Logger::Verbosity::ERROR) return; if (rate_limit_ > std::chrono::duration::zero()) { auto now = std::chrono::high_resolution_clock::now(); @@ -339,36 +348,14 @@ class Logger { #endif } - /** - * Mutex for the tag. - */ - std::mutex tag_mutex_; - - /** - * Name given to the logger to be prepended to all logs. - */ - std::string tag_; - - /** - * Rate limit for the logger. If set to 0, no rate limiting will be - * performed. - */ - std::chrono::duration rate_limit_{0.0f}; - - /** - * Last time a log was printed. Used for rate limiting. - */ - std::chrono::high_resolution_clock::time_point last_print_{}; - - /** - * Whether to include the time in the log. - */ - std::atomic include_time_{true}; - - /** - * Current verbosity of the logger. Determines what will be printed to - * console. - */ - std::atomic level_; + std::mutex tag_mutex_; ///< Mutex for the tag. + std::string tag_; ///< Name of the logger to be prepended to all logs. + std::chrono::duration rate_limit_{ + 0.0f}; ///< Rate limit for the logger. If set to 0, no rate limiting will be performed. + std::chrono::high_resolution_clock::time_point + last_print_{}; ///< Last time a log was printed. Used for rate limiting. + std::atomic include_time_{true}; ///< Whether to include the time in the log. + std::atomic level_ = + espp::Logger::Verbosity::WARN; ///< Current verbosity level of the logger. }; } // namespace espp diff --git a/components/math/example/main/math_example.cpp b/components/math/example/main/math_example.cpp index f64c52838..26fe19417 100644 --- a/components/math/example/main/math_example.cpp +++ b/components/math/example/main/math_example.cpp @@ -76,40 +76,83 @@ extern "C" void app_main(void) { logger.info("=== gaussian ==="); { - //! [gaussian example] - std::array gammas = { - 0.10f, - 0.15f, - 0.20f, - 0.25f, - }; - espp::Gaussian gaussian({ - .gamma = gammas[0], - .alpha = 1.0f, // default - .beta = 0.5f, // default - }); - float t = 0; - fmt::print("% t"); - for (auto g : gammas) { - fmt::print(", gaussian({})", g); + { + //! [gaussian example] + std::array gammas = { + 0.10f, + 0.15f, + 0.20f, + 0.25f, + }; + espp::Gaussian gaussian({ + .gamma = gammas[0], + .alpha = 1.0f, // default + .beta = 0.5f, // default + }); + float t = 0; + fmt::print("% t"); + for (auto g : gammas) { + fmt::print(", gaussian({})", g); + } + fmt::print("\n"); + float increment = 0.05f; + int num_increments = 1.0f / increment; + for (int i = 0; i <= num_increments; i++) { + fmt::print("{}", t); + for (auto g : gammas) { + // update the gamma + gaussian.set_gamma(g); + // evaluate it + float v = gaussian(t); + // print it + fmt::print(", {}", v); + } + fmt::print("\n"); + t += increment; + } + //! [gaussian example] } - fmt::print("\n"); - float increment = 0.05f; - int num_increments = 1.0f / increment; - for (int i = 0; i <= num_increments; i++) { - fmt::print("{}", t); + + { + //! [gaussian fade in fade out example] + std::array gammas = { + 0.10f, 0.15f, 0.20f, 0.25f, 0.30f, 0.35f, 0.40f, 0.45f, + }; + espp::Gaussian fade_in({ + .gamma = gammas[0], + .alpha = 1.0f, // default + .beta = 1.0f, // default + }); + espp::Gaussian fade_out({ + .gamma = gammas[0], + .alpha = 1.0f, // default + .beta = 0.0f, // default + }); + float t = 0; + fmt::print("% t"); for (auto g : gammas) { - // update the gamma - gaussian.gamma(g); - // evaluate it - float v = gaussian(t); - // print it - fmt::print(", {}", v); + fmt::print(", fade_in({}), fade_out({})", g, g); } fmt::print("\n"); - t += increment; + float increment = 0.05f; + int num_increments = 1.0f / increment; + for (int i = 0; i <= num_increments; i++) { + fmt::print("{}", t); + for (auto g : gammas) { + // update the gamma + fade_in.set_gamma(g); + fade_out.set_gamma(g); + // evaluate it + float in = fade_in(t); + float out = fade_out(t); + // print it + fmt::print(", {}, {}", in, out); + } + fmt::print("\n"); + t += increment; + } + //! [gaussian fade in fade out example] } - //! [gaussian example] } logger.info("=== range mapper ==="); diff --git a/components/math/include/bezier.hpp b/components/math/include/bezier.hpp index 95357fc50..2e2874377 100644 --- a/components/math/include/bezier.hpp +++ b/components/math/include/bezier.hpp @@ -67,13 +67,7 @@ template class Bezier { * @param t The evaluation parameter, [0, 1]. * @return The bezier evaluated at \p t. */ - T at(float t) const { - if (weighted_) { - return weighted_eval(t); - } else { - return eval(t); - } - } + T at(float t) const { return weighted_ ? weighted_eval(t) : eval(t); } /** * @brief Evaluate the bezier at \p t. diff --git a/components/math/include/gaussian.hpp b/components/math/include/gaussian.hpp index 8f368ced6..d04109807 100644 --- a/components/math/include/gaussian.hpp +++ b/components/math/include/gaussian.hpp @@ -11,6 +11,8 @@ namespace espp { * * \section gaussian_ex1 Example * \snippet math_example.cpp gaussian example + * \section gaussian_ex2 Fade-In/Fade-Out Example + * \snippet math_example.cpp gaussian fade in fade out example */ class Gaussian { public: @@ -20,9 +22,9 @@ class Gaussian { struct Config { float gamma; ///< Slope of the gaussian, range [0, 1]. 0 is more of a thin spike from 0 up to ///< max output (alpha), 1 is more of a small wave around the max output (alpha). - float alpha = 1.0f; ///< Max amplitude of the gaussian output, defautls to 1.0. - float beta = - 0.5f; ///< Beta value for the gaussian, default to be symmetric at 0.5 in range [0,1]. + float alpha{1.0f}; ///< Max amplitude of the gaussian output, defautls to 1.0. + float beta{ + 0.5f}; ///< Beta value for the gaussian, default to be symmetric at 0.5 in range [0,1]. bool operator==(const Config &rhs) const = default; }; @@ -37,63 +39,87 @@ class Gaussian { , beta_(config.beta) {} /** - * @brief Get the currently configured gamma (shape). - * @return The current gamma (shape) value [0, 1]. + * @brief Evaluate the gaussian at \p t. + * @param t The evaluation parameter, [0, 1]. + * @return The gaussian evaluated at \p t. */ - float gamma() const { return gamma_; } + float at(float t) const { + float tmb_y = (t - beta_) / gamma_; // (t - B) / y + float power = -0.5f * tmb_y * tmb_y; // -(t - B)^2 / 2y^2 + return alpha_ * exp(power); + } /** - * @brief Set / Update the gamma (shape) value. - * @param g New gamma (shape) to use. + * @brief Evaluate the gaussian at \p t. + * @note Convienience wrapper around the at() method. + * @param t The evaluation parameter, [0, 1]. + * @return The gaussian evaluated at \p t. */ - void gamma(float g) { gamma_ = g; } + float operator()(float t) const { return at(t); } /** - * @brief Get the currently configured alpha (scaling) value. - * @return The current alpha (scaler) value. + * @brief Update the gaussian configuration. + * @param config The new configuration. */ - float alpha() const { return alpha_; } + void update(const Config &config) { + gamma_ = config.gamma; + alpha_ = config.alpha; + beta_ = config.beta; + } /** - * @brief Set / Update the alpha (scaling) value. - * @param a New alpha (scaler) to use. + * @brief Set the configuration of the gaussian. + * @param config The new configuration. */ - void alpha(float a) { alpha_ = a; } + void set_config(const Config &config) { update(config); } /** - * @brief Get the currently configured beta (shifting) value. - * @return The current beta (shifter) value [0, 1]. + * @brief Get the current configuration of the gaussian. + * @return The current configuration. */ - float beta() const { return beta_; } + Config get_config() const { return {.gamma = gamma_, .alpha = alpha_, .beta = beta_}; } /** - * @brief Set / Update the beta (shifting) value. - * @param b New beta (shifter) to use. + * @brief Get the gamma value. + * @return The gamma value. */ - void beta(float b) { beta_ = b; } + float get_gamma() const { return gamma_; } /** - * @brief Evaluate the gaussian at \p t. - * @param t The evaluation parameter, [0, 1]. - * @return The gaussian evaluated at \p t. + * @brief Get the alpha value. + * @return The alpha value. */ - float at(float t) const { - float tmb_y = (t - beta_) / gamma_; // (t - B) / y - float power = -0.5f * tmb_y * tmb_y; // -(t - B)^2 / 2y^2 - return alpha_ * exp(power); - } + float get_alpha() const { return alpha_; } /** - * @brief Evaluate the gaussian at \p t. - * @note Convienience wrapper around the at() method. - * @param t The evaluation parameter, [0, 1]. - * @return The gaussian evaluated at \p t. + * @brief Get the beta value. + * @return The beta value. */ - float operator()(float t) const { return at(t); } + float get_beta() const { return beta_; } + + /** + * @brief Set the gamma value. + * @param gamma The new gamma value. + */ + void set_gamma(float gamma) { gamma_ = gamma; } + + /** + * @brief Set the alpha value. + * @param alpha The new alpha value. + */ + void set_alpha(float alpha) { alpha_ = alpha; } + + /** + * @brief Set the beta value. + * @param beta The new beta value. + */ + void set_beta(float beta) { beta_ = beta; } protected: - float gamma_; - float alpha_; - float beta_; + float gamma_; /// class RangeMapper { */ struct Config { T center; /**< Center value for the input range. */ - T center_deadband{ - T(0)}; /**< Deadband amount around (+-) the center for which output will be 0. */ - T minimum; /**< Minimum value for the input range. */ - T maximum; /**< Maximum value for the input range. */ - T range_deadband{T(0)}; /**< Deadband amount around the minimum and maximum for which output - will be min/max output. */ - T output_center{T(0)}; /**< The center for the output. Default 0. */ - T output_range{T(1)}; /**< The range (+/-) from the center for the output. Default 1. @note Will + T center_deadband = + 0; /**< Deadband amount around (+-) the center for which output will be 0. */ + T minimum; /**< Minimum value for the input range. */ + T maximum; /**< Maximum value for the input range. */ + T range_deadband = 0; /**< Deadband amount around the minimum and maximum for which output will + be min/max output. */ + T output_center = 0; /**< The center for the output. Default 0. */ + T output_range = 1; /**< The range (+/-) from the center for the output. Default 1. @note Will be passed through std::abs() to ensure it is positive. */ - bool invert_output{ - false}; /**< Whether to invert the output (default false). @note If true will flip the sign - of the output after converting from the input distribution. */ + bool invert_output = + false; /**< Whether to invert the output (default false). @note If true will flip the sign + of the output after converting from the input distribution. */ }; /** * Initialize the range mapper with no config. */ - RangeMapper() = default; + RangeMapper() {} /** * @brief Initialize the RangeMapper. @@ -76,10 +76,6 @@ template class RangeMapper { * will be ignored. */ void configure(const Config &config) { - if (config.output_range == T(0)) { - return; - } - center_ = config.center; center_deadband_ = std::abs(config.center_deadband); minimum_ = config.minimum; @@ -90,9 +86,13 @@ template class RangeMapper { output_min_ = output_center_ - output_range_; output_max_ = output_center_ + output_range_; // positive range is the range from the (center + center_deadband) to (max - range_deadband) - pos_range_ = (maximum_ - range_deadband_ - (center_ + center_deadband_)) / output_range_; + pos_range_ = output_range_ + ? (maximum_ - range_deadband_ - (center_ + center_deadband_)) / output_range_ + : 0; // negative range is the range from the (center - center_deadband) to (min + range_deadband) - neg_range_ = (center_ - center_deadband_ - (minimum_ + range_deadband_)) / output_range_; + neg_range_ = output_range_ + ? (center_ - center_deadband_ - (minimum_ + range_deadband_)) / output_range_ + : 0; invert_output_ = config.invert_output; } @@ -281,25 +281,4 @@ typedef RangeMapper FloatRangeMapper; typedef RangeMapper IntRangeMapper; } // namespace espp -#include "fmt/format.h" - -// NOTE: right now it seems we cannot use the generic version that works in -// vector2d.hpp because it results in 'template class without a name' -// errors... - -template <> struct fmt::formatter : fmt::formatter { - auto format(const espp::FloatRangeMapper::Config &config, format_context &ctx) const { - return fmt::format_to(ctx.out(), "FloatRangeMapper[{},{},{},{},{},{},{},{}]", config.center, - config.center_deadband, config.minimum, config.maximum, - config.range_deadband, config.output_center, config.output_range, - config.invert_output); - } -}; -template <> struct fmt::formatter : fmt::formatter { - auto format(const espp::IntRangeMapper::Config &config, format_context &ctx) const { - return fmt::format_to(ctx.out(), "IntRangeMapper[{},{},{},{},{},{},{},{}]", config.center, - config.center_deadband, config.minimum, config.maximum, - config.range_deadband, config.output_center, config.output_range, - config.invert_output); - } -}; +#include "range_mapper_formatters.hpp" diff --git a/components/math/include/range_mapper_formatters.hpp b/components/math/include/range_mapper_formatters.hpp new file mode 100644 index 000000000..0aadcdb08 --- /dev/null +++ b/components/math/include/range_mapper_formatters.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "fmt/format.h" + +// NOTE: right now it seems we cannot use the generic version that works in +// vector2d.hpp because it results in 'template class without a name' +// errors... + +template <> struct fmt::formatter : fmt::formatter { + auto format(const espp::FloatRangeMapper::Config &config, format_context &ctx) const { + return fmt::format_to(ctx.out(), "FloatRangeMapper[{},{},{},{},{},{},{},{}]", config.center, + config.center_deadband, config.minimum, config.maximum, + config.range_deadband, config.output_center, config.output_range, + config.invert_output); + } +}; +template <> struct fmt::formatter : fmt::formatter { + auto format(const espp::IntRangeMapper::Config &config, format_context &ctx) const { + return fmt::format_to(ctx.out(), "IntRangeMapper[{},{},{},{},{},{},{},{}]", config.center, + config.center_deadband, config.minimum, config.maximum, + config.range_deadband, config.output_center, config.output_range, + config.invert_output); + } +}; diff --git a/components/math/include/vector2d.hpp b/components/math/include/vector2d.hpp index 495d8d336..d76b31f6d 100644 --- a/components/math/include/vector2d.hpp +++ b/components/math/include/vector2d.hpp @@ -20,7 +20,7 @@ template class Vector2d { * @param x The starting X value. * @param y The starting Y value. */ - explicit Vector2d(T x = T(0), T y = T(0)) + explicit Vector2d(T x = 0, T y = 0) : x_(x) , y_(y) {} @@ -294,17 +294,4 @@ typedef Vector2d Vector2u8; ///< Typedef for 8 bit integer 2D vectors. } // namespace espp -#include "format.hpp" - -// for allowing easy serialization/printing of the -// espp::Vector2d -template struct fmt::formatter> { - template constexpr auto parse(ParseContext &ctx) const { - return ctx.begin(); - } - - template - auto format(espp::Vector2d const &v, FormatContext &ctx) const { - return fmt::format_to(ctx.out(), "({},{})", v.x(), v.y()); - } -}; +#include "vector2d_formatters.hpp" diff --git a/components/math/include/vector2d_formatters.hpp b/components/math/include/vector2d_formatters.hpp new file mode 100644 index 000000000..8a35dec72 --- /dev/null +++ b/components/math/include/vector2d_formatters.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "format.hpp" + +// for allowing easy serialization/printing of the +// espp::Vector2d +template struct fmt::formatter> { + template constexpr auto parse(ParseContext &ctx) const { + return ctx.begin(); + } + + template + auto format(espp::Vector2d const &v, FormatContext &ctx) const { + return fmt::format_to(ctx.out(), "({},{})", v.x(), v.y()); + } +}; diff --git a/components/rtsp/CMakeLists.txt b/components/rtsp/CMakeLists.txt index bf0d7d50b..e3d5bf5a7 100644 --- a/components/rtsp/CMakeLists.txt +++ b/components/rtsp/CMakeLists.txt @@ -1,3 +1,4 @@ idf_component_register( INCLUDE_DIRS "include" + SRC_DIRS "src" REQUIRES base_component task socket) diff --git a/components/rtsp/example/partitions.csv b/components/rtsp/example/partitions.csv index e280b0510..e799571a8 100644 --- a/components/rtsp/example/partitions.csv +++ b/components/rtsp/example/partitions.csv @@ -2,4 +2,4 @@ nvs, data, nvs, 0x9000, 0x6000 phy_init, data, phy, 0xf000, 0x1000 factory, app, factory, 0x10000, 2M -littlefs, data, spiffs, , 2M +littlefs, data, spiffs, , 1M diff --git a/components/rtsp/include/rtcp_packet.hpp b/components/rtsp/include/rtcp_packet.hpp index 61c4e3aca..1056f7914 100644 --- a/components/rtsp/include/rtcp_packet.hpp +++ b/components/rtsp/include/rtcp_packet.hpp @@ -7,16 +7,18 @@ namespace espp { /// @brief A class to represent a RTCP packet /// @details This class is used to represent a RTCP packet. /// It is used as a base class for all RTCP packet types. +/// @note At the moment, this class is not used. class RtcpPacket { public: + /// @brief Constructor, default RtcpPacket() = default; - virtual ~RtcpPacket() = default; - std::string_view get_data() const { - return std::string_view(reinterpret_cast(m_buffer), m_bufferSize); - } + /// @brief Destructor, default + virtual ~RtcpPacket() = default; - void serialize() {} + /// @brief Get the buffer of the packet + /// @return The buffer of the packet + std::string_view get_data() const; protected: uint8_t *m_buffer{nullptr}; diff --git a/components/rtsp/include/rtp_packet.hpp b/components/rtsp/include/rtp_packet.hpp index 184e58f2c..269e67cc6 100644 --- a/components/rtsp/include/rtp_packet.hpp +++ b/components/rtsp/include/rtp_packet.hpp @@ -1,133 +1,156 @@ #pragma once +#include #include #include #include namespace espp { /// RtpPacket is a class to parse RTP packet. +/// It can be used to parse and serialize RTP packets. +/// The RTP header fields are stored in the class and can be modified. +/// The payload is stored in the packet_ vector and can be modified. class RtpPacket { public: /// Construct an empty RtpPacket. /// The packet_ vector is empty and the header fields are set to 0. - RtpPacket() { - // ensure that the packet_ vector is at least RTP_HEADER_SIZE bytes long - packet_.resize(RTP_HEADER_SIZE); - } + RtpPacket(); /// Construct an RtpPacket with a payload of size payload_size. - explicit RtpPacket(size_t payload_size) : payload_size_(payload_size) { - // ensure that the packet_ vector is at least RTP_HEADER_SIZE + payload_size bytes long - packet_.resize(RTP_HEADER_SIZE + payload_size); - } + /// The packet_ vector is resized to RTP_HEADER_SIZE + payload_size. + explicit RtpPacket(size_t payload_size); /// Construct an RtpPacket from a string_view. /// Store the string_view in the packet_ vector and parses the header. /// @param data The string_view to parse. - explicit RtpPacket(std::string_view data) { - packet_.assign(data.begin(), data.end()); - payload_size_ = packet_.size() - RTP_HEADER_SIZE; - if (packet_.size() >= RTP_HEADER_SIZE) - parse_rtp_header(); - } - - ~RtpPacket() {} - - /// Getters for the RTP header fields. - int get_version() const { return version_; } - bool get_padding() const { return padding_; } - bool get_extension() const { return extension_; } - int get_csrc_count() const { return csrc_count_; } - bool get_marker() const { return marker_; } - int get_payload_type() const { return payload_type_; } - int get_sequence_number() const { return sequence_number_; } - int get_timestamp() const { return timestamp_; } - int get_ssrc() const { return ssrc_; } - - /// Setters for the RTP header fields. - void set_version(int version) { version_ = version; } - void set_padding(bool padding) { padding_ = padding; } - void set_extension(bool extension) { extension_ = extension; } - void set_csrc_count(int csrc_count) { csrc_count_ = csrc_count; } - void set_marker(bool marker) { marker_ = marker; } - void set_payload_type(int payload_type) { payload_type_ = payload_type; } - void set_sequence_number(int sequence_number) { sequence_number_ = sequence_number; } - void set_timestamp(int timestamp) { timestamp_ = timestamp; } - void set_ssrc(int ssrc) { ssrc_ = ssrc; } + explicit RtpPacket(std::string_view data); + + /// Destructor. + ~RtpPacket(); + + // ----------------------------------------------------------------- + // Getters for the RTP header fields. + // ----------------------------------------------------------------- + + /// Get the RTP version. + /// @return The RTP version. + int get_version() const; + + /// Get the padding flag. + /// @return The padding flag. + bool get_padding() const; + + /// Get the extension flag. + /// @return The extension flag. + bool get_extension() const; + + /// Get the CSRC count. + /// @return The CSRC count. + int get_csrc_count() const; + + /// Get the marker flag. + /// @return The marker flag. + bool get_marker() const; + + /// Get the payload type. + /// @return The payload type. + int get_payload_type() const; + + /// Get the sequence number. + /// @return The sequence number. + int get_sequence_number() const; + + /// Get the timestamp. + /// @return The timestamp. + int get_timestamp() const; + + /// Get the SSRC. + /// @return The SSRC. + int get_ssrc() const; + + // ----------------------------------------------------------------- + // Setters for the RTP header fields. + // ----------------------------------------------------------------- + + /// Set the RTP version. + /// @param version The RTP version to set. + void set_version(int version); + + /// Set the padding flag. + /// @param padding The padding flag to set. + void set_padding(bool padding); + + /// Set the extension flag. + /// @param extension The extension flag to set. + void set_extension(bool extension); + + /// Set the CSRC count. + /// @param csrc_count The CSRC count to set. + void set_csrc_count(int csrc_count); + + /// Set the marker flag. + /// @param marker The marker flag to set. + void set_marker(bool marker); + + /// Set the payload type. + /// @param payload_type The payload type to set. + void set_payload_type(int payload_type); + + /// Set the sequence number. + /// @param sequence_number The sequence number to set. + void set_sequence_number(int sequence_number); + + /// Set the timestamp. + /// @param timestamp The timestamp to set. + void set_timestamp(int timestamp); + + /// Set the SSRC. + /// @param ssrc The SSRC to set. + void set_ssrc(int ssrc); + + // ----------------------------------------------------------------- + // Utility methods. + // ----------------------------------------------------------------- /// Serialize the RTP header. /// @note This method should be called after modifying the RTP header fields. /// @note This method does not serialize the payload. To set the payload, use /// set_payload(). /// To get the payload, use get_payload(). - void serialize() { serialize_rtp_header(); } + void serialize(); /// Get a string_view of the whole packet. /// @note The string_view is valid as long as the packet_ vector is not modified. /// @note If you manually build the packet_ vector, you should make sure that you /// call serialize() before calling this method. /// @return A string_view of the whole packet. - std::string_view get_data() const { - return std::string_view((char *)packet_.data(), packet_.size()); - } + std::string_view get_data() const; /// Get the size of the RTP header. /// @return The size of the RTP header. - size_t get_rtp_header_size() const { return RTP_HEADER_SIZE; } + size_t get_rtp_header_size() const; /// Get a string_view of the RTP header. /// @return A string_view of the RTP header. - std::string_view get_rpt_header() const { - return std::string_view((char *)packet_.data(), RTP_HEADER_SIZE); - } + std::string_view get_rpt_header() const; /// Get a reference to the packet_ vector. /// @return A reference to the packet_ vector. - std::vector &get_packet() { return packet_; } + std::vector &get_packet(); /// Get a string_view of the payload. /// @return A string_view of the payload. - std::string_view get_payload() const { - return std::string_view((char *)packet_.data() + RTP_HEADER_SIZE, payload_size_); - } + std::string_view get_payload() const; /// Set the payload. /// @param payload The payload to set. - void set_payload(std::string_view payload) { - packet_.resize(RTP_HEADER_SIZE + payload.size()); - std::copy(payload.begin(), payload.end(), packet_.begin() + RTP_HEADER_SIZE); - payload_size_ = payload.size(); - } + void set_payload(std::string_view payload); protected: static constexpr int RTP_HEADER_SIZE = 12; - void parse_rtp_header() { - version_ = (packet_[0] & 0xC0) >> 6; - padding_ = (packet_[0] & 0x20) >> 5; - extension_ = (packet_[0] & 0x10) >> 4; - csrc_count_ = packet_[0] & 0x0F; - marker_ = (packet_[1] & 0x80) >> 7; - payload_type_ = packet_[1] & 0x7F; - sequence_number_ = (packet_[2] << 8) | packet_[3]; - timestamp_ = (packet_[4] << 24) | (packet_[5] << 16) | (packet_[6] << 8) | packet_[7]; - ssrc_ = (packet_[8] << 24) | (packet_[9] << 16) | (packet_[10] << 8) | packet_[11]; - } - - void serialize_rtp_header() { - packet_[0] = (version_ << 6) | (padding_ << 5) | (extension_ << 4) | csrc_count_; - packet_[1] = (marker_ << 7) | payload_type_; - packet_[2] = sequence_number_ >> 8; - packet_[3] = sequence_number_ & 0xFF; - packet_[4] = timestamp_ >> 24; - packet_[5] = (timestamp_ >> 16) & 0xFF; - packet_[6] = (timestamp_ >> 8) & 0xFF; - packet_[7] = timestamp_ & 0xFF; - packet_[8] = ssrc_ >> 24; - packet_[9] = (ssrc_ >> 16) & 0xFF; - packet_[10] = (ssrc_ >> 8) & 0xFF; - packet_[11] = ssrc_ & 0xFF; - } + void parse_rtp_header(); + void serialize_rtp_header(); std::vector packet_; int version_{2}; diff --git a/components/rtsp/include/rtsp_client.hpp b/components/rtsp/include/rtsp_client.hpp index 30b3c3685..9bd176f29 100644 --- a/components/rtsp/include/rtsp_client.hpp +++ b/components/rtsp/include/rtsp_client.hpp @@ -1,8 +1,11 @@ #pragma once +#include "socket_msvc.hpp" + #include #include #include +#include #include #include "base_component.hpp" @@ -37,33 +40,19 @@ class RtspClient : public BaseComponent { std::string path{"/mjpeg/1"}; ///< The path to the RTSP stream on the server. Will be appended ///< to the server address and port to form the full path of the ///< form "rtsp://:" - jpeg_frame_callback_t on_jpeg_frame; ///< The callback to call when a JPEG frame is received + espp::RtspClient::jpeg_frame_callback_t + on_jpeg_frame; ///< The callback to call when a JPEG frame is received espp::Logger::Verbosity log_level = espp::Logger::Verbosity::INFO; ///< The verbosity of the logger }; /// Constructor /// \param config The configuration for the RTSP client - explicit RtspClient(const Config &config) - : BaseComponent("RtspClient", config.log_level) - , server_address_(config.server_address) - , rtsp_port_(config.rtsp_port) - , rtsp_socket_({.log_level = espp::Logger::Verbosity::WARN}) - , rtp_socket_({.log_level = espp::Logger::Verbosity::WARN}) - , rtcp_socket_({.log_level = espp::Logger::Verbosity::WARN}) - , on_jpeg_frame_(config.on_jpeg_frame) - , cseq_(0) - , path_("rtsp://" + server_address_ + ":" + std::to_string(rtsp_port_) + config.path) {} + explicit RtspClient(const Config &config); /// Destructor /// Disconnects from the RTSP server - ~RtspClient() { - std::error_code ec; - disconnect(ec); - if (ec) { - logger_.error("Error disconnecting: {}", ec.message()); - } - } + ~RtspClient(); /// Send an RTSP request to the server /// \note This is a blocking call @@ -88,148 +77,29 @@ class RtspClient : public BaseComponent { /// \return The response from the server std::string send_request(const std::string &method, const std::string &path, const std::unordered_map &extra_headers, - std::error_code &ec) { - // send the request - std::string request = method + " " + path + " RTSP/1.0\r\n"; - request += "CSeq: " + std::to_string(cseq_) + "\r\n"; - if (session_id_.size() > 0) { - request += "Session: " + session_id_ + "\r\n"; - } - for (auto &[key, value] : extra_headers) { - request += key + ": " + value + "\r\n"; - } - request += "User-Agent: rtsp-client\r\n"; - request += "Accept: application/sdp\r\n"; - request += "\r\n"; - std::string response; - auto transmit_config = espp::detail::TcpTransmitConfig{ - .wait_for_response = true, - .response_size = 1024, - .on_response_callback = - [&response](auto &response_vector) { - response.assign(response_vector.begin(), response_vector.end()); - }, - .response_timeout = std::chrono::seconds(5), - }; - // NOTE: now this call blocks until the response is received - logger_.debug("Request:\n{}", request); - if (!rtsp_socket_.transmit(request, transmit_config)) { - ec = std::make_error_code(std::errc::io_error); - logger_.error("Failed to send request"); - return {}; - } - - // TODO: how to keep receiving until we get the full response? - // if (response.find("\r\n\r\n") != std::string::npos) { - // break; - // } - - // parse the response - logger_.debug("Response:\n{}", response); - if (parse_response(response, ec)) { - return response; - } - return {}; - } + std::error_code &ec); /// Connect to the RTSP server /// Connects to the RTSP server and sends the OPTIONS request. /// \param ec The error code to set if an error occurs - void connect(std::error_code &ec) { - // exit early if error code is already set - if (ec) { - return; - } - - rtsp_socket_.reinit(); - auto did_connect = rtsp_socket_.connect({ - .ip_address = server_address_, - .port = static_cast(rtsp_port_), - }); - if (!did_connect) { - ec = std::make_error_code(std::errc::io_error); - logger_.error("Failed to connect to {}:{}", server_address_, rtsp_port_); - return; - } - - // send the options request - send_request("OPTIONS", "*", {}, ec); - } + void connect(std::error_code &ec); /// Disconnect from the RTSP server /// Disconnects from the RTSP server and sends the TEARDOWN request. /// \param ec The error code to set if an error occurs - void disconnect(std::error_code &ec) { - // send the teardown request - teardown(ec); - rtsp_socket_.reinit(); - } + void disconnect(std::error_code &ec); /// Describe the RTSP stream /// Sends the DESCRIBE request to the RTSP server and parses the response. /// \param ec The error code to set if an error occurs - void describe(std::error_code &ec) { - // exit early if the error code is set - if (ec) { - return; - } - // send the describe request - auto response = send_request("DESCRIBE", path_, {}, ec); - if (ec) { - return; - } - // sdp response is of the form: - // std::regex sdp_regex("m=video (\\d+) RTP/AVP (\\d+)"); - // parse the sdp response and get the video port without using regex - // this is a very simple sdp parser that only works for this specific case - auto sdp_start = response.find("m=video"); - if (sdp_start == std::string::npos) { - ec = std::make_error_code(std::errc::wrong_protocol_type); - logger_.error("Invalid sdp"); - return; - } - auto sdp_end = response.find("\r\n", sdp_start); - if (sdp_end == std::string::npos) { - ec = std::make_error_code(std::errc::protocol_error); - logger_.error("Incomplete sdp"); - return; - } - auto sdp = response.substr(sdp_start, sdp_end - sdp_start); - auto port_start = sdp.find(" "); - if (port_start == std::string::npos) { - ec = std::make_error_code(std::errc::protocol_error); - logger_.error("Could not find port start"); - return; - } - auto port_end = sdp.find(" ", port_start + 1); - if (port_end == std::string::npos) { - ec = std::make_error_code(std::errc::protocol_error); - logger_.error("Could not find port end"); - return; - } - auto port = sdp.substr(port_start + 1, port_end - port_start - 1); - video_port_ = std::stoi(port); - logger_.debug("Video port: {}", video_port_); - auto payload_type_start = sdp.find(" ", port_end + 1); - if (payload_type_start == std::string::npos) { - ec = std::make_error_code(std::errc::protocol_error); - logger_.error("Could not find payload type start"); - return; - } - auto payload_type = sdp.substr(payload_type_start + 1, sdp.size() - payload_type_start - 1); - video_payload_type_ = std::stoi(payload_type); - logger_.debug("Video payload type: {}", video_payload_type_); - } + void describe(std::error_code &ec); /// Setup the RTSP stream /// \note Starts the RTP and RTCP threads. /// Sends the SETUP request to the RTSP server and parses the response. /// \note The default ports are 5000 and 5001 for RTP and RTCP respectively. /// \param ec The error code to set if an error occurs - void setup(std::error_code &ec) { - // default to rtp and rtcp client ports 5000 and 5001 - setup(5000, 50001, ec); - } + void setup(std::error_code &ec); /// Setup the RTSP stream /// Sends the SETUP request to the RTSP server and parses the response. @@ -237,61 +107,22 @@ class RtspClient : public BaseComponent { /// \param rtp_port The RTP client port /// \param rtcp_port The RTCP client port /// \param ec The error code to set if an error occurs - void setup(size_t rtp_port, size_t rtcp_port, std::error_code &ec) { - // exit early if the error code is set - if (ec) { - return; - } - - // set up the transport header with the rtp and rtcp ports - auto transport_header = - "RTP/AVP;unicast;client_port=" + std::to_string(rtp_port) + "-" + std::to_string(rtcp_port); - - // send the setup request (no response is expected) - send_request("SETUP", path_, {{"Transport", transport_header}}, ec); - if (ec) { - return; - } - - init_rtp(rtp_port, ec); - init_rtcp(rtcp_port, ec); - } + void setup(size_t rtp_port, size_t rtcp_port, std::error_code &ec); /// Play the RTSP stream /// Sends the PLAY request to the RTSP server and parses the response. /// \param ec The error code to set if an error occurs - void play(std::error_code &ec) { - // exit early if the error code is set - if (ec) { - return; - } - // send the play request - send_request("PLAY", path_, {}, ec); - } + void play(std::error_code &ec); /// Pause the RTSP stream /// Sends the PAUSE request to the RTSP server and parses the response. /// \param ec The error code to set if an error occurs - void pause(std::error_code &ec) { - // exit early if the error code is set - if (ec) { - return; - } - // send the pause request - send_request("PAUSE", path_, {}, ec); - } + void pause(std::error_code &ec); /// Teardown the RTSP stream /// Sends the TEARDOWN request to the RTSP server and parses the response. /// \param ec The error code to set if an error occurs - void teardown(std::error_code &ec) { - // exit early if the error code is set - if (ec) { - return; - } - // send the teardown request - send_request("TEARDOWN", path_, {}, ec); - } + void teardown(std::error_code &ec); protected: /// Parse the RTSP response @@ -303,99 +134,19 @@ class RtspClient : public BaseComponent { /// \param response_data The response data to parse /// \param ec The error code to set if an error occurs /// \return True if the response was parsed successfully, false otherwise - bool parse_response(const std::string &response_data, std::error_code &ec) { - // exit early if the error code is set - if (ec) { - return false; - } - if (response_data.empty()) { - ec = std::make_error_code(std::errc::no_message); - logger_.error("Empty response"); - return false; - } - // RTP response is of the form: - // std::regex response_regex("RTSP/1.0 (\\d+) (.*)\r\n(.*)\r\n\r\n"); - // parse the response but don't use regex since it may be slow on embedded platforms - // make sure it matches the expected response format - if (response_data.starts_with("RTSP/1.0") != 0) { - ec = std::make_error_code(std::errc::protocol_error); - logger_.error("Invalid response"); - return false; - } - // parse the status code and message - int status_code = std::stoi(response_data.substr(9, 3)); - std::string status_message = response_data.substr(13, response_data.find("\r\n") - 13); - if (status_code != 200) { - ec = std::make_error_code(std::errc::protocol_error); - logger_.error(std::string("Request failed: ") + status_message); - return false; - } - // parse the session id - auto session_pos = response_data.find("Session: "); - if (session_pos != std::string::npos) { - session_id_ = response_data.substr(session_pos + 9, - response_data.find("\r\n", session_pos) - session_pos - 9); - } - // increment the cseq - cseq_++; - return true; - } + bool parse_response(const std::string &response_data, std::error_code &ec); /// Initialize the RTP socket /// \note Starts the RTP socket task. /// \param rtp_port The RTP client port /// \param ec The error code to set if an error occurs - void init_rtp(size_t rtp_port, std::error_code &ec) { - // exit early if the error code is set - if (ec) { - return; - } - logger_.debug("Starting rtp socket"); - auto rtp_task_config = espp::Task::Config{ - .name = "Rtp", - .callback = nullptr, - .stack_size_bytes = 16 * 1024, - }; - auto rtp_config = espp::UdpSocket::ReceiveConfig{ - .port = rtp_port, - .buffer_size = 2 * 1024, - .on_receive_callback = std::bind(&RtspClient::handle_rtp_packet, this, - std::placeholders::_1, std::placeholders::_2), - }; - if (!rtp_socket_.start_receiving(rtp_task_config, rtp_config)) { - ec = std::make_error_code(std::errc::operation_canceled); - logger_.error("Failed to start receiving rtp packets"); - return; - } - } + void init_rtp(size_t rtp_port, std::error_code &ec); /// Initialize the RTCP socket /// \note Starts the RTCP socket task. /// \param rtcp_port The RTCP client port /// \param ec The error code to set if an error occurs - void init_rtcp(size_t rtcp_port, std::error_code &ec) { - // exit early if the error code is set - if (ec) { - return; - } - logger_.debug("Starting rtcp socket"); - auto rtcp_task_config = espp::Task::Config{ - .name = "Rtcp", - .callback = nullptr, - .stack_size_bytes = 6 * 1024, - }; - auto rtcp_config = espp::UdpSocket::ReceiveConfig{ - .port = rtcp_port, - .buffer_size = 1 * 1024, - .on_receive_callback = std::bind(&RtspClient::handle_rtcp_packet, this, - std::placeholders::_1, std::placeholders::_2), - }; - if (!rtcp_socket_.start_receiving(rtcp_task_config, rtcp_config)) { - ec = std::make_error_code(std::errc::operation_canceled); - logger_.error("Failed to start receiving rtcp packets"); - return; - } - } + void init_rtcp(size_t rtcp_port, std::error_code &ec); /// Handle an RTP packet /// \note Parses the RTP packet and appends it to the current JPEG frame. @@ -404,54 +155,7 @@ class RtspClient : public BaseComponent { /// data to handle \param sender_info The sender info \return Optional data to send back to the /// sender std::optional> handle_rtp_packet(std::vector &data, - const espp::Socket::Info &sender_info) { - // jpeg frame that we are building - static std::unique_ptr jpeg_frame; - - logger_.debug("Got RTP packet of size: {}", data.size()); - - std::string_view packet(reinterpret_cast(data.data()), data.size()); - // parse the rtp packet - RtpJpegPacket rtp_jpeg_packet(packet); - auto frag_offset = rtp_jpeg_packet.get_offset(); - if (frag_offset == 0) { - // first fragment - logger_.debug("Received first fragment, size: {}, sequence number: {}", - rtp_jpeg_packet.get_data().size(), rtp_jpeg_packet.get_sequence_number()); - if (jpeg_frame) { - // we already have a frame, this is an error - logger_.warn("Received first fragment but already have a frame"); - jpeg_frame.reset(); - } - jpeg_frame = std::make_unique(rtp_jpeg_packet); - } else if (jpeg_frame) { - logger_.debug("Received middle fragment, size: {}, sequence number: {}", - rtp_jpeg_packet.get_data().size(), rtp_jpeg_packet.get_sequence_number()); - // middle fragment - jpeg_frame->append(rtp_jpeg_packet); - } else { - // we don't have a frame to append to but we got a middle fragment - // this is an error - logger_.warn("Received middle fragment without a frame"); - return {}; - } - - // check if this is the last packet of the frame (the last packet will have - // the marker bit set) - if (jpeg_frame && jpeg_frame->is_complete()) { - // get the jpeg data - auto jpeg_data = jpeg_frame->get_data(); - logger_.debug("Received jpeg frame of size: {} B", jpeg_data.size()); - // call the on_jpeg_frame callback - if (on_jpeg_frame_) { - on_jpeg_frame_(std::move(jpeg_frame)); - } - logger_.debug("Sent jpeg frame to callback, now jpeg_frame is nullptr? {}", - jpeg_frame == nullptr); - } - // return an empty vector to indicate that we don't want to send a response - return {}; - } + const espp::Socket::Info &sender_info); /// Handle an RTCP packet /// \note Parses the RTCP packet and sends a response if necessary. @@ -460,14 +164,7 @@ class RtspClient : public BaseComponent { /// \param sender_info The sender info /// \return Optional data to send back to the sender std::optional> handle_rtcp_packet(std::vector &data, - const espp::Socket::Info &sender_info) { - // receive the rtcp packet - [[maybe_unused]] std::string_view packet(reinterpret_cast(data.data()), data.size()); - // TODO: parse the rtcp packet - // return an empty vector to indicate that we don't want to send a response - return {}; - } - + const espp::Socket::Info &sender_info); std::string server_address_; int rtsp_port_; diff --git a/components/rtsp/include/rtsp_server.hpp b/components/rtsp/include/rtsp_server.hpp index 205a74d8a..4c2a5ef16 100644 --- a/components/rtsp/include/rtsp_server.hpp +++ b/components/rtsp/include/rtsp_server.hpp @@ -1,5 +1,7 @@ #pragma once +#include "socket_msvc.hpp" + #include #include #include @@ -44,89 +46,32 @@ class RtspServer : public BaseComponent { ///< up into multiple packets if they are larger than this. It seems that 1500 works ///< well for sending, but is too large for the esp32 (camera-display) to receive ///< properly. - Logger::Verbosity log_level = Logger::Verbosity::WARN; ///< The log level for the RTSP server + espp::Logger::Verbosity log_level = + espp::Logger::Verbosity::WARN; ///< The log level for the RTSP server }; /// @brief Construct an RTSP server /// @param config The configuration for the RTSP server - explicit RtspServer(const Config &config) - : BaseComponent("RTSP Server", config.log_level) - , server_address_(config.server_address) - , port_(config.port) - , path_(config.path) - , rtsp_socket_({.log_level = espp::Logger::Verbosity::WARN}) - , max_data_size_(config.max_data_size) { - // generate a random ssrc -#if defined(ESP_PLATFORM) - ssrc_ = esp_random(); -#else - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution dis; - ssrc_ = dis(gen); -#endif - } + explicit RtspServer(const Config &config); /// @brief Destroy the RTSP server - ~RtspServer() { stop(); } + ~RtspServer(); /// @brief Sets the log level for the RTSP sessions created by this server /// @note This does not affect the log level of the RTSP server itself /// @note This does not change the log level of any sessions that have /// already been created /// @param log_level The log level to set - void set_session_log_level(Logger::Verbosity log_level) { session_log_level_ = log_level; } + void set_session_log_level(espp::Logger::Verbosity log_level); /// @brief Start the RTSP server /// Starts the accept task, session task, and binds the RTSP socket /// @return True if the server was started successfully, false otherwise - bool start() { - if (accept_task_ && accept_task_->is_started()) { - logger_.error("Server is already running"); - return false; - } - - logger_.info("Starting RTSP server on port {}", port_); - - if (!rtsp_socket_.bind(port_)) { - logger_.error("Failed to bind to port {}", port_); - return false; - } - - int max_pending_connections = 5; - if (!rtsp_socket_.listen(max_pending_connections)) { - logger_.error("Failed to listen on port {}", port_); - return false; - } - - using namespace std::placeholders; - accept_task_ = std::make_unique(Task::Config{ - .name = "RTSP Accept Task", - .callback = std::bind(&RtspServer::accept_task_function, this, _1, _2), - .stack_size_bytes = 6 * 1024, - .log_level = espp::Logger::Verbosity::WARN, - }); - accept_task_->start(); - return true; - } + bool start(); /// @brief Stop the FTP server /// Stops the accept task, session task, and closes the RTSP socket - void stop() { - logger_.info("Stopping RTSP server"); - // stop the accept task - if (accept_task_) { - accept_task_->stop(); - } - // stop the session task - if (session_task_) { - session_task_->stop(); - } - // clear the list of sessions - sessions_.clear(); - // close the RTSP socket - rtsp_socket_.close(); - } + void stop(); /// @brief Send a frame over the RTSP connection /// Converts the full JPEG frame into a series of simplified RTP/JPEG @@ -134,182 +79,11 @@ class RtspServer : public BaseComponent { /// actually send it /// @note Overwrites any existing frame that has not been sent /// @param frame The frame to send - void send_frame(const JpegFrame &frame) { - // get the frame scan data - auto frame_header = frame.get_header(); - auto frame_data = frame.get_scan_data(); - - auto width = frame_header.get_width(); - auto height = frame_header.get_height(); - auto q0 = frame_header.get_quantization_table(0); - auto q1 = frame_header.get_quantization_table(1); - - // if the frame data is larger than the MTU, then we need to break it up - // into multiple RTP packets - size_t num_packets = frame_data.size() / max_data_size_ + 1; - logger_.debug("Frame data is {} bytes, breaking into {} packets", frame_data.size(), - num_packets); - - // create num_packets RtpJpegPackets - // The first packet will have the quantization tables, and the last packet - // will have the end of image marker and the marker bit set - std::vector> packets; - packets.reserve(num_packets); - for (size_t i = 0; i < num_packets; i++) { - // get the start and end indices for the current packet - size_t start_index = i * max_data_size_; - size_t end_index = std::min(start_index + max_data_size_, frame_data.size()); - - static const int type_specific = 0; - static const int fragment_type = 0; - int offset = i * max_data_size_; - - std::unique_ptr packet; - // if this is the first packet, it has the quantization tables - if (i == 0) { - // use the original q value and include the quantization tables - packet = std::make_unique( - type_specific, fragment_type, 128, width, height, q0, q1, - frame_data.substr(start_index, end_index - start_index)); - } else { - // use a different q value (less than 128) and don't include the - // quantization tables - packet = std::make_unique( - type_specific, offset, fragment_type, 96, width, height, - frame_data.substr(start_index, end_index - start_index)); - } - - // set the payload type to 26 (JPEG) - packet->set_payload_type(26); - // set the sequence number - packet->set_sequence_number(sequence_number_++); - // set the timestamp - static auto start_time = std::chrono::steady_clock::now(); - auto now = std::chrono::steady_clock::now(); - auto timestamp = - std::chrono::duration_cast(now - start_time).count(); - packet->set_timestamp(timestamp * 90); - - // set the ssrc - packet->set_ssrc(ssrc_); - - // auto mjpeg_header = packet->get_mjpeg_header(); - // std::vector mjpeg_vec(mjpeg_header.begin(), mjpeg_header.end()); - - // if it's the last packet, set the marker bit - if (i == num_packets - 1) { - packet->set_marker(true); - } - - // make sure the packet header has been serialized - packet->serialize(); - - // add the packet to the list of packets - packets.emplace_back(std::move(packet)); - } - - // now move the packets into the rtp_packets_ vector - { - std::unique_lock lock(rtp_packets_mutex_); - // move the new packets into the list - rtp_packets_ = std::move(packets); - } - } + void send_frame(const espp::JpegFrame &frame); protected: - bool accept_task_function(std::mutex &m, std::condition_variable &cv) { - // accept a new connection - auto control_socket = rtsp_socket_.accept(); - if (!control_socket) { - logger_.error("Failed to accept new connection"); - return false; - } - - logger_.info("Accepted new connection"); - - // create a new session - auto session = std::make_unique( - std::move(control_socket), - RtspSession::Config{.server_address = fmt::format("{}:{}", server_address_, port_), - .rtsp_path = path_, - .log_level = session_log_level_}); - - // add the session to the list of sessions - auto session_id = session->get_session_id(); - sessions_.emplace(session_id, std::move(session)); - - // start the session task if it is not already running - using namespace std::placeholders; - if (!session_task_ || !session_task_->is_started()) { - logger_.info("Starting session task"); - session_task_ = std::make_unique(Task::Config{ - .name = "RtspSessionTask", - .callback = std::bind(&RtspServer::session_task_function, this, _1, _2), - .stack_size_bytes = 6 * 1024, - .log_level = espp::Logger::Verbosity::WARN, - }); - session_task_->start(); - } - // we do not want to stop the task - return false; - } - - bool session_task_function(std::mutex &m, std::condition_variable &cv) { - // sleep between frames - { - using namespace std::chrono_literals; - std::unique_lock lk(m); - cv.wait_for(lk, 10ms); - } - - // when this function returns, the vector of pointers will go out of scope - // and the pointers will be deleted (which is good because it means we - // won't send the same frame twice) - std::vector> packets; - { - // copy the rtp packets into a local vector - std::unique_lock lock(rtp_packets_mutex_); - if (rtp_packets_.empty()) { - // if there is not a new frame (no packets), then simply return - // we do not want to stop the task - return false; - } - // move the packets into the local vector - packets = std::move(rtp_packets_); - } - - logger_.debug("Sending frame data to clients"); - - // for each session in sessions_ - // if the session is active - // send the latest frame to the client - std::lock_guard lk(session_mutex_); - for (auto &session : sessions_) { - [[maybe_unused]] auto session_id = session.first; - auto &session_ptr = session.second; - // send the packets to the client - for (auto &packet : packets) { - // if the session is not active or is closed, then stop sending - if (!session_ptr->is_active() || session_ptr->is_closed()) { - break; - } - session_ptr->send_rtp_packet(*packet); - } - } - // loop over the sessions and erase ones which are closed - for (auto it = sessions_.begin(); it != sessions_.end();) { - auto &session = it->second; - if (session->is_closed()) { - logger_.info("Removing session {}", session->get_session_id()); - it = sessions_.erase(it); - } else { - ++it; - } - } - - // we do not want to stop the task - return false; - } + bool accept_task_function(std::mutex &m, std::condition_variable &cv); + bool session_task_function(std::mutex &m, std::condition_variable &cv); uint32_t ssrc_; ///< the ssrc (synchronization source identifier) for the RTP packets uint16_t sequence_number_{0}; ///< the sequence number for the RTP packets @@ -318,16 +92,16 @@ class RtspServer : public BaseComponent { int port_; ///< the port of the RTSP server std::string path_; ///< the path of the RTSP server, e.g. rtsp:://:/ - TcpSocket rtsp_socket_; + espp::TcpSocket rtsp_socket_; size_t max_data_size_; std::mutex rtp_packets_mutex_; - std::vector> rtp_packets_; + std::vector> rtp_packets_; - Logger::Verbosity session_log_level_{Logger::Verbosity::WARN}; + espp::Logger::Verbosity session_log_level_{espp::Logger::Verbosity::WARN}; std::mutex session_mutex_; - std::unordered_map> sessions_; + std::unordered_map> sessions_; std::unique_ptr accept_task_; std::unique_ptr session_task_; diff --git a/components/rtsp/include/rtsp_session.hpp b/components/rtsp/include/rtsp_session.hpp index 51b61d826..b4b7dcae3 100644 --- a/components/rtsp/include/rtsp_session.hpp +++ b/components/rtsp/include/rtsp_session.hpp @@ -1,5 +1,7 @@ #pragma once +#include "socket_msvc.hpp" + #include #include #include @@ -26,100 +28,61 @@ class RtspSession : public BaseComponent { public: /// Configuration for the RTSP session struct Config { - std::string server_address; ///< The address of the server - std::string rtsp_path; ///< The RTSP path of the session - Logger::Verbosity log_level = Logger::Verbosity::WARN; ///< The log level of the session + std::string server_address; ///< The address of the server + std::string rtsp_path; ///< The RTSP path of the session + espp::Logger::Verbosity log_level = + espp::Logger::Verbosity::WARN; ///< The log level of the session }; /// @brief Construct a new RtspSession object /// @param control_socket The control socket of the session /// @param config The configuration of the session - explicit RtspSession(std::unique_ptr control_socket, const Config &config) - : BaseComponent("RtspSession", config.log_level) - , control_socket_(std::move(control_socket)) - , rtp_socket_({.log_level = Logger::Verbosity::WARN}) - , rtcp_socket_({.log_level = Logger::Verbosity::WARN}) - , session_id_(generate_session_id()) - , server_address_(config.server_address) - , rtsp_path_(config.rtsp_path) - , client_address_(control_socket_->get_remote_info().address) { - // set the logger tag to include the session id - logger_.set_tag("RtspSession " + std::to_string(session_id_)); - // start the session task to handle RTSP commands - using namespace std::placeholders; - control_task_ = std::make_unique(Task::Config{ - .name = "RtspSession " + std::to_string(session_id_), - .callback = std::bind(&RtspSession::control_task_fn, this, _1, _2), - .stack_size_bytes = 6 * 1024, - .log_level = Logger::Verbosity::WARN, - }); - control_task_->start(); - } - - ~RtspSession() { - teardown(); - // stop the session task - if (control_task_ && control_task_->is_started()) { - logger_.info("Stopping control task"); - control_task_->stop(); - } - } + explicit RtspSession(std::unique_ptr control_socket, const Config &config); + + /// @brief Destroy the RtspSession object + /// Stop the session task + ~RtspSession(); /// @brief Get the session id /// @return The session id - uint32_t get_session_id() const { return session_id_; } + uint32_t get_session_id() const; /// @brief Check if the session is closed /// @return True if the session is closed, false otherwise - bool is_closed() const { return closed_; } + bool is_closed() const; /// Get whether the session is connected /// @return True if the session is connected, false otherwise - bool is_connected() const { return control_socket_ && control_socket_->is_connected(); } + bool is_connected() const; /// Get whether the session is active /// @return True if the session is active, false otherwise - bool is_active() const { return session_active_; } + bool is_active() const; /// Mark the session as active /// This will cause the server to start sending frames to the client - void play() { session_active_ = true; } + void play(); /// Pause the session /// This will cause the server to stop sending frames to the client /// @note This does not stop the session, it just pauses it /// @note This is useful for when the client is buffering - void pause() { session_active_ = false; } + void pause(); /// Teardown the session /// This will cause the server to stop sending frames to the client /// and close the connection - void teardown() { - session_active_ = false; - closed_ = true; - } + void teardown(); /// Send an RTP packet to the client /// @param packet The RTP packet to send /// @return True if the packet was sent successfully, false otherwise - bool send_rtp_packet(const RtpPacket &packet) { - logger_.debug("Sending RTP packet"); - return rtp_socket_.send(packet.get_data(), { - .ip_address = client_address_, - .port = (size_t)client_rtp_port_, - }); - } + bool send_rtp_packet(const espp::RtpPacket &packet); /// Send an RTCP packet to the client /// @param packet The RTCP packet to send /// @return True if the packet was sent successfully, false otherwise - bool send_rtcp_packet(const RtcpPacket &packet) { - logger_.debug("Sending RTCP packet"); - return rtcp_socket_.send(packet.get_data(), { - .ip_address = client_address_, - .port = (size_t)client_rtcp_port_, - }); - } + bool send_rtcp_packet(const espp::RtcpPacket &packet); protected: /// Send a response to a RTSP request @@ -130,319 +93,77 @@ class RtspSession : public BaseComponent { /// @param body The response body (optional) /// @return True if the response was sent successfully, false otherwise bool send_response(int code, std::string_view message, int sequence_number = -1, - std::string_view headers = "", std::string_view body = "") { - // create a response - std::string response = "RTSP/1.0 " + std::to_string(code) + " " + std::string(message) + "\r\n"; - if (sequence_number != -1) { - response += "CSeq: " + std::to_string(sequence_number) + "\r\n"; - } - if (!headers.empty()) { - response += headers; - } - if (!body.empty()) { - response += "Content-Length: " + std::to_string(body.size()) + "\r\n"; - response += "\r\n"; - response += body; - } else { - response += "\r\n"; - } - logger_.info("Sending RTSP response"); - logger_.debug("{}", response); - // send the response - return control_socket_->transmit(response); - } + std::string_view headers = "", std::string_view body = ""); /// Handle a RTSP options request /// @param request The RTSP request /// @return True if the request was handled successfully, false otherwise - bool handle_rtsp_options(std::string_view request) { - int sequence_number = 0; - if (!parse_rtsp_command_sequence(request, sequence_number)) { - return handle_rtsp_invalid_request(request); - } - logger_.info("RTSP OPTIONS request"); - // create a response - int code = 200; - std::string message = "OK"; - std::string headers = "Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n"; - return send_response(code, message, sequence_number, headers); - } + bool handle_rtsp_options(std::string_view request); /// Handle a RTSP describe request /// Create a SDP description and send it back to the client /// @param request The RTSP request /// @return True if the request was handled successfully, false otherwise - bool handle_rtsp_describe(std::string_view request) { - int sequence_number = 0; - if (!parse_rtsp_command_sequence(request, sequence_number)) { - return handle_rtsp_invalid_request(request); - } - logger_.info("RTSP DESCRIBE request"); - // create a response - int code = 200; - std::string message = "OK"; - // SDP description for an MJPEG stream - std::string rtsp_path = "rtsp://" + server_address_ + "/" + rtsp_path_; - std::string body = "v=0\r\n" // version (0) - "o=- " + - std::to_string(session_id_) + " 1 IN IP4 " + server_address_ + - "\r\n" // username (none), session id, version, network type (internet), - // address type, address - "s=MJPEG Stream\r\n" // session name (can be anything) - "i=MJPEG Stream\r\n" // session name (can be anything) - "t=0 0\r\n" // start / stop - "a=control:" + - rtsp_path + - "\r\n" // the RTSP path - "a=mimetype:string;\"video/x-motion-jpeg\"\r\n" // MIME type - "m=video 0 RTP/AVP 26\r\n" // MJPEG - "c=IN IP4 0.0.0.0\r\n" // client will use the RTSP address - "b=AS:256\r\n" // 256kbps - "a=control:" + - rtsp_path + - "\r\n" - "a=udp-only\r\n"; - - std::string headers = "Content-Type: application/sdp\r\n" - "Content-Base: " + - rtsp_path + "\r\n"; - return send_response(code, message, sequence_number, headers, body); - } + bool handle_rtsp_describe(std::string_view request); /// Handle a RTSP setup request /// Create a session and send the RTP port numbers back to the client /// @param request The RTSP request /// @return True if the request was handled successfully, false otherwise - bool handle_rtsp_setup(std::string_view request) { - // parse the rtsp path from the request - std::string_view rtsp_path; - int client_rtp_port; - int client_rtcp_port; - if (!parse_rtsp_setup_request(request, rtsp_path, client_rtp_port, client_rtcp_port)) { - // the parse function will send the response, so we just need to return - return false; - } - // parse the sequence number from the request - int sequence_number = 0; - if (!parse_rtsp_command_sequence(request, sequence_number)) { - return handle_rtsp_invalid_request(request); - } - logger_.info("RTSP SETUP request"); - // save the client port numbers - client_rtp_port_ = client_rtp_port; - client_rtcp_port_ = client_rtcp_port; - // create a response - int code = 200; - std::string message = "OK"; - // flesh out the transport header - std::string headers = - "Session: " + std::to_string(session_id_) + "\r\n" + - "Transport: RTP/AVP;unicast;client_port=" + std::to_string(client_rtp_port) + "-" + - std::to_string(client_rtcp_port) + "\r\n"; - return send_response(code, message, sequence_number, headers); - } + bool handle_rtsp_setup(std::string_view request); /// Handle a RTSP play request /// After responding to the request, the server should start sending RTP /// packets to the client /// @param request The request to handle /// @return True if the request was handled successfully, false otherwise - bool handle_rtsp_play(std::string_view request) { - int sequence_number = 0; - if (!parse_rtsp_command_sequence(request, sequence_number)) { - return handle_rtsp_invalid_request(request); - } - logger_.info("RTSP PLAY request"); - play(); - int code = 200; - std::string message = "OK"; - std::string headers = - "Session: " + std::to_string(session_id_) + "\r\n" + "Range: npt=0.000-\r\n"; - return send_response(code, message, sequence_number, headers); - } + bool handle_rtsp_play(std::string_view request); /// Handle a RTSP pause request /// After responding to the request, the server should stop sending RTP /// packets to the client /// @param request The request to handle /// @return True if the request was handled successfully, false otherwise - bool handle_rtsp_pause(std::string_view request) { - int sequence_number = 0; - if (!parse_rtsp_command_sequence(request, sequence_number)) { - return handle_rtsp_invalid_request(request); - } - logger_.info("RTSP PAUSE request"); - pause(); - int code = 200; - std::string message = "OK"; - std::string headers = "Session: " + std::to_string(session_id_) + "\r\n"; - return send_response(code, message, sequence_number, headers); - } + bool handle_rtsp_pause(std::string_view request); /// Handle an RTSP teardown request /// @param request The request to handle /// @return True if the request was handled successfully, false otherwise - bool handle_rtsp_teardown(std::string_view request) { - int sequence_number = 0; - if (!parse_rtsp_command_sequence(request, sequence_number)) { - return handle_rtsp_invalid_request(request); - } - logger_.info("RTSP TEARDOWN request"); - teardown(); - int code = 200; - std::string message = "OK"; - std::string headers = "Session: " + std::to_string(session_id_) + "\r\n"; - return send_response(code, message, sequence_number, headers); - } + bool handle_rtsp_teardown(std::string_view request); /// Handle an invalid RTSP request /// @param request The request to handle /// @return True if the request was handled successfully, false otherwise - bool handle_rtsp_invalid_request(std::string_view request) { - logger_.info("RTSP invalid request"); - // create a response - int code = 400; - std::string message = "Bad Request"; - int sequence_number = 0; - if (!parse_rtsp_command_sequence(request, sequence_number)) { - return send_response(code, message); - } - return send_response(code, message, sequence_number); - } + bool handle_rtsp_invalid_request(std::string_view request); /// Handle a single RTSP request /// @note Requests are of the form "METHOD RTSP_PATH RTSP_VERSION" /// @param request The request to handle /// @return True if the request was handled successfully, false otherwise - bool handle_rtsp_request(std::string_view request) { - logger_.debug("RTSP request:\n{}", request); - // store indices of the first and second spaces - // to extract the method and the rtsp path - auto first_space_index = request.find(' '); - auto second_space_index = request.find(' ', first_space_index + 1); - auto end_of_line_index = request.find('\r'); - if (first_space_index == std::string::npos || second_space_index == std::string::npos || - end_of_line_index == std::string::npos) { - return handle_rtsp_invalid_request(request); - } - // extract the method and the rtsp path - // where the request looks like "METHOD RTSP_PATH RTSP_VERSION" - std::string_view method = request.substr(0, first_space_index); - // TODO: we should probably check that the rtsp path is correct - [[maybe_unused]] std::string_view rtsp_path = - request.substr(first_space_index + 1, second_space_index - first_space_index - 1); - // TODO: we should probably check that the rtsp version is correct - [[maybe_unused]] std::string_view rtsp_version = - request.substr(second_space_index + 1, end_of_line_index - second_space_index - 1); - // extract the request body, which is separated by an empty line (\r\n) - // from the request header - std::string_view request_body = request.substr(end_of_line_index + 2); - - // handle the request - if (method == "OPTIONS") { - return handle_rtsp_options(request_body); - } else if (method == "DESCRIBE") { - return handle_rtsp_describe(request_body); - } else if (method == "SETUP") { - return handle_rtsp_setup(request_body); - } else if (method == "PLAY") { - return handle_rtsp_play(request_body); - } else if (method == "PAUSE") { - return handle_rtsp_pause(request_body); - } else if (method == "TEARDOWN") { - return handle_rtsp_teardown(request_body); - } - - // if the method is not supported, return an error - return handle_rtsp_invalid_request(request_body); - } + bool handle_rtsp_request(std::string_view request); /// @brief The task function for the control thread /// @param m The mutex to lock when waiting on the condition variable /// @param cv The condition variable to wait on /// @return True if the task should stop, false otherwise - bool control_task_fn(std::mutex &m, std::condition_variable &cv) { - if (closed_) { - logger_.info("Session is closed, stopping control task"); - // return true to stop the task - return true; - } - if (!control_socket_) { - logger_.warn("Control socket is no longer valid, stopping control task"); - teardown(); - // return true to stop the task - return true; - } - if (!control_socket_->is_connected()) { - logger_.warn("Control socket is not connected, stopping control task"); - teardown(); - // if the control socket is not connected, return true to stop the task - return true; - } - static size_t max_request_size = 1024; - std::vector buffer; - logger_.info("Waiting for RTSP request"); - if (control_socket_->receive(buffer, max_request_size)) { - // parse the request - std::string_view request(reinterpret_cast(buffer.data()), buffer.size()); - // handle the request - if (!handle_rtsp_request(request)) { - logger_.warn("Failed to handle RTSP request"); - } - } - // the receive handles most of the blocking, so we don't need to sleep - // here, just return false to keep the task running - return false; - } + bool control_task_fn(std::mutex &m, std::condition_variable &cv); /// Generate a new RTSP session id for the client /// Session IDs are generated randomly when a client sends a SETUP request and are /// used to identify the client in subsequent requests when managing the RTP session. /// @return The new session id - uint32_t generate_session_id() { -#if defined(ESP_PLATFORM) - return esp_random(); -#else - static std::random_device rd; - static std::mt19937 gen(rd()); - static std::uniform_int_distribution<> dis(0, std::numeric_limits::max()); - return dis(gen); -#endif - } + uint32_t generate_session_id(); /// Parse the RTSP command sequence number from a request /// @param request The request to parse /// @param cseq The command sequence number (output) /// @return True if the request was parsed successfully, false otherwise - bool parse_rtsp_command_sequence(std::string_view request, int &cseq) { - // parse the cseq from the request - auto cseq_index = request.find("CSeq: "); - if (cseq_index == std::string::npos) { - return false; - } - auto cseq_end_index = request.find('\r', cseq_index); - if (cseq_end_index == std::string::npos) { - return false; - } - std::string_view cseq_str = request.substr(cseq_index + 6, cseq_end_index - cseq_index - 6); - if (cseq_str.empty()) { - return false; - } - // convert the cseq to an integer - cseq = std::stoi(std::string{cseq_str}); - return true; - } + bool parse_rtsp_command_sequence(std::string_view request, int &cseq); /// Parse a RTSP path from a request /// @param request The request to parse /// @return The RTSP path - std::string_view parse_rtsp_path(std::string_view request) { - // parse the rtsp path from the request - // where the request looks like "METHOD RTSP_PATH RTSP_VERSION" - std::string_view rtsp_path = request.substr( - request.find(' ') + 1, request.find(' ', request.find(' ') + 1) - request.find(' ') - 1); - return rtsp_path; - } + std::string_view parse_rtsp_path(std::string_view request); /// Parse a RTSP setup request /// Looks for the client RTP and RTCP port numbers in the request @@ -453,55 +174,7 @@ class RtspSession : public BaseComponent { /// @param client_rtcp_port The client RTCP port number (output) /// @return True if the request was parsed successfully, false otherwise bool parse_rtsp_setup_request(std::string_view request, std::string_view &rtsp_path, - int &client_rtp_port, int &client_rtcp_port) { - // parse the rtsp path from the request - rtsp_path = parse_rtsp_path(request); - if (rtsp_path.empty()) { - return false; - } - logger_.debug("Parsing setup request:\n{}", request); - // parse the transport header from the request - auto transport_index = request.find("Transport: "); - if (transport_index == std::string::npos) { - return false; - } - auto transport_end_index = request.find('\r', transport_index); - if (transport_end_index == std::string::npos) { - return false; - } - std::string_view transport = - request.substr(transport_index + 11, transport_end_index - transport_index - 11); - if (transport.empty()) { - return false; - } - logger_.debug("Transport header: {}", transport); - // we don't support TCP, so return an error if the transport is not RTP/AVP/UDP - if (transport.find("RTP/AVP/TCP") != std::string::npos) { - logger_.error("TCP transport is not supported"); - // TODO: this doesn't send the sequence number back to the client - send_response(461, "Unsupported Transport"); - return false; - } - - // parse the rtp port from the request - auto client_port_index = request.find("client_port="); - auto dash_index = request.find('-', client_port_index); - std::string_view rtp_port = - request.substr(client_port_index + 12, dash_index - client_port_index - 12); - if (rtp_port.empty()) { - return false; - } - // parse the rtcp port from the request - std::string_view rtcp_port = - request.substr(dash_index + 1, request.find('\r', client_port_index) - dash_index - 1); - if (rtcp_port.empty()) { - return false; - } - // convert the rtp and rtcp ports to integers - client_rtp_port = std::stoi(std::string{rtp_port}); - client_rtcp_port = std::stoi(std::string{rtcp_port}); - return true; - } + int &client_rtp_port, int &client_rtcp_port); std::unique_ptr control_socket_; espp::UdpSocket rtp_socket_; diff --git a/components/rtsp/src/rtcp_packet.cpp b/components/rtsp/src/rtcp_packet.cpp new file mode 100644 index 000000000..f6739b482 --- /dev/null +++ b/components/rtsp/src/rtcp_packet.cpp @@ -0,0 +1,7 @@ +#include "rtcp_packet.hpp" + +using namespace espp; + +std::string_view RtcpPacket::get_data() const { + return std::string_view(reinterpret_cast(m_buffer), m_bufferSize); +} diff --git a/components/rtsp/src/rtp_packet.cpp b/components/rtsp/src/rtp_packet.cpp new file mode 100644 index 000000000..e1f15eb08 --- /dev/null +++ b/components/rtsp/src/rtp_packet.cpp @@ -0,0 +1,96 @@ +#include "rtp_packet.hpp" + +using namespace espp; + +RtpPacket::RtpPacket() { + // ensure that the packet_ vector is at least RTP_HEADER_SIZE bytes long + packet_.resize(RTP_HEADER_SIZE); +} + +RtpPacket::RtpPacket(size_t payload_size) + : payload_size_(payload_size) { + // ensure that the packet_ vector is at least RTP_HEADER_SIZE + payload_size bytes long + packet_.resize(RTP_HEADER_SIZE + payload_size); +} + +RtpPacket::RtpPacket(std::string_view data) { + packet_.assign(data.begin(), data.end()); + payload_size_ = packet_.size() - RTP_HEADER_SIZE; + if (packet_.size() >= RTP_HEADER_SIZE) + parse_rtp_header(); +} + +RtpPacket::~RtpPacket() {} + +/// Getters for the RTP header fields. +int RtpPacket::get_version() const { return version_; } +bool RtpPacket::get_padding() const { return padding_; } +bool RtpPacket::get_extension() const { return extension_; } +int RtpPacket::get_csrc_count() const { return csrc_count_; } +bool RtpPacket::get_marker() const { return marker_; } +int RtpPacket::get_payload_type() const { return payload_type_; } +int RtpPacket::get_sequence_number() const { return sequence_number_; } +int RtpPacket::get_timestamp() const { return timestamp_; } +int RtpPacket::get_ssrc() const { return ssrc_; } + +/// Setters for the RTP header fields. +void RtpPacket::set_version(int version) { version_ = version; } +void RtpPacket::set_padding(bool padding) { padding_ = padding; } +void RtpPacket::set_extension(bool extension) { extension_ = extension; } +void RtpPacket::set_csrc_count(int csrc_count) { csrc_count_ = csrc_count; } +void RtpPacket::set_marker(bool marker) { marker_ = marker; } +void RtpPacket::set_payload_type(int payload_type) { payload_type_ = payload_type; } +void RtpPacket::set_sequence_number(int sequence_number) { sequence_number_ = sequence_number; } +void RtpPacket::set_timestamp(int timestamp) { timestamp_ = timestamp; } +void RtpPacket::set_ssrc(int ssrc) { ssrc_ = ssrc; } + +void RtpPacket::serialize() { serialize_rtp_header(); } + +std::string_view RtpPacket::get_data() const { + return std::string_view((char *)packet_.data(), packet_.size()); +} + +size_t RtpPacket::get_rtp_header_size() const { return RTP_HEADER_SIZE; } + +std::string_view RtpPacket::get_rpt_header() const { + return std::string_view((char *)packet_.data(), RTP_HEADER_SIZE); +} + +std::vector &RtpPacket::get_packet() { return packet_; } + +std::string_view RtpPacket::get_payload() const { + return std::string_view((char *)packet_.data() + RTP_HEADER_SIZE, payload_size_); +} + +void RtpPacket::set_payload(std::string_view payload) { + packet_.resize(RTP_HEADER_SIZE + payload.size()); + std::copy(payload.begin(), payload.end(), packet_.begin() + RTP_HEADER_SIZE); + payload_size_ = payload.size(); +} + +void RtpPacket::parse_rtp_header() { + version_ = (packet_[0] & 0xC0) >> 6; + padding_ = (packet_[0] & 0x20) >> 5; + extension_ = (packet_[0] & 0x10) >> 4; + csrc_count_ = packet_[0] & 0x0F; + marker_ = (packet_[1] & 0x80) >> 7; + payload_type_ = packet_[1] & 0x7F; + sequence_number_ = (packet_[2] << 8) | packet_[3]; + timestamp_ = (packet_[4] << 24) | (packet_[5] << 16) | (packet_[6] << 8) | packet_[7]; + ssrc_ = (packet_[8] << 24) | (packet_[9] << 16) | (packet_[10] << 8) | packet_[11]; +} + +void RtpPacket::serialize_rtp_header() { + packet_[0] = (version_ << 6) | (padding_ << 5) | (extension_ << 4) | csrc_count_; + packet_[1] = (marker_ << 7) | payload_type_; + packet_[2] = sequence_number_ >> 8; + packet_[3] = sequence_number_ & 0xFF; + packet_[4] = timestamp_ >> 24; + packet_[5] = (timestamp_ >> 16) & 0xFF; + packet_[6] = (timestamp_ >> 8) & 0xFF; + packet_[7] = timestamp_ & 0xFF; + packet_[8] = ssrc_ >> 24; + packet_[9] = (ssrc_ >> 16) & 0xFF; + packet_[10] = (ssrc_ >> 8) & 0xFF; + packet_[11] = ssrc_ & 0xFF; +} diff --git a/components/rtsp/src/rtsp_client.cpp b/components/rtsp/src/rtsp_client.cpp new file mode 100644 index 000000000..f326a5d7a --- /dev/null +++ b/components/rtsp/src/rtsp_client.cpp @@ -0,0 +1,346 @@ +#include "rtsp_client.hpp" + +using namespace espp; + +RtspClient::RtspClient(const Config &config) + : BaseComponent("RtspClient", config.log_level) + , server_address_(config.server_address) + , rtsp_port_(config.rtsp_port) + , rtsp_socket_({.log_level = espp::Logger::Verbosity::WARN}) + , rtp_socket_({.log_level = espp::Logger::Verbosity::WARN}) + , rtcp_socket_({.log_level = espp::Logger::Verbosity::WARN}) + , on_jpeg_frame_(config.on_jpeg_frame) + , cseq_(0) + , path_("rtsp://" + server_address_ + ":" + std::to_string(rtsp_port_) + config.path) {} + +RtspClient::~RtspClient() { + std::error_code ec; + disconnect(ec); + if (ec) { + logger_.error("Error disconnecting: {}", ec.message()); + } +} + +std::string +RtspClient::send_request(const std::string &method, const std::string &path, + const std::unordered_map &extra_headers, + std::error_code &ec) { + // send the request + std::string request = method + " " + path + " RTSP/1.0\r\n"; + request += "CSeq: " + std::to_string(cseq_) + "\r\n"; + if (session_id_.size() > 0) { + request += "Session: " + session_id_ + "\r\n"; + } + for (auto &[key, value] : extra_headers) { + request += key + ": " + value + "\r\n"; + } + request += "User-Agent: rtsp-client\r\n"; + request += "Accept: application/sdp\r\n"; + request += "\r\n"; + std::string response; + auto transmit_config = espp::TcpSocket::TransmitConfig{ + .wait_for_response = true, + .response_size = 1024, + .on_response_callback = + [&response](auto &response_vector) { + response.assign(response_vector.begin(), response_vector.end()); + }, + .response_timeout = std::chrono::seconds(5), + }; + // NOTE: now this call blocks until the response is received + logger_.debug("Request:\n{}", request); + if (!rtsp_socket_.transmit(request, transmit_config)) { + ec = std::make_error_code(std::errc::io_error); + logger_.error("Failed to send request"); + return {}; + } + + // TODO: how to keep receiving until we get the full response? + // if (response.find("\r\n\r\n") != std::string::npos) { + // break; + // } + + // parse the response + logger_.debug("Response:\n{}", response); + if (parse_response(response, ec)) { + return response; + } + return {}; +} + +void RtspClient::connect(std::error_code &ec) { + // exit early if error code is already set + if (ec) { + return; + } + + rtsp_socket_.reinit(); + auto did_connect = rtsp_socket_.connect({ + .ip_address = server_address_, + .port = static_cast(rtsp_port_), + }); + if (!did_connect) { + ec = std::make_error_code(std::errc::io_error); + logger_.error("Failed to connect to {}:{}", server_address_, rtsp_port_); + return; + } + + // send the options request + send_request("OPTIONS", "*", {}, ec); +} + +void RtspClient::disconnect(std::error_code &ec) { + // send the teardown request + teardown(ec); + rtsp_socket_.reinit(); +} + +void RtspClient::describe(std::error_code &ec) { + // exit early if the error code is set + if (ec) { + return; + } + // send the describe request + auto response = send_request("DESCRIBE", path_, {}, ec); + if (ec) { + return; + } + // sdp response is of the form: + // std::regex sdp_regex("m=video (\\d+) RTP/AVP (\\d+)"); + // parse the sdp response and get the video port without using regex + // this is a very simple sdp parser that only works for this specific case + auto sdp_start = response.find("m=video"); + if (sdp_start == std::string::npos) { + ec = std::make_error_code(std::errc::wrong_protocol_type); + logger_.error("Invalid sdp"); + return; + } + auto sdp_end = response.find("\r\n", sdp_start); + if (sdp_end == std::string::npos) { + ec = std::make_error_code(std::errc::protocol_error); + logger_.error("Incomplete sdp"); + return; + } + auto sdp = response.substr(sdp_start, sdp_end - sdp_start); + auto port_start = sdp.find(" "); + if (port_start == std::string::npos) { + ec = std::make_error_code(std::errc::protocol_error); + logger_.error("Could not find port start"); + return; + } + auto port_end = sdp.find(" ", port_start + 1); + if (port_end == std::string::npos) { + ec = std::make_error_code(std::errc::protocol_error); + logger_.error("Could not find port end"); + return; + } + auto port = sdp.substr(port_start + 1, port_end - port_start - 1); + video_port_ = std::stoi(port); + logger_.debug("Video port: {}", video_port_); + auto payload_type_start = sdp.find(" ", port_end + 1); + if (payload_type_start == std::string::npos) { + ec = std::make_error_code(std::errc::protocol_error); + logger_.error("Could not find payload type start"); + return; + } + auto payload_type = sdp.substr(payload_type_start + 1, sdp.size() - payload_type_start - 1); + video_payload_type_ = std::stoi(payload_type); + logger_.debug("Video payload type: {}", video_payload_type_); +} + +void RtspClient::setup(std::error_code &ec) { + // default to rtp and rtcp client ports 5000 and 5001 + setup(5000, 50001, ec); +} + +void RtspClient::setup(size_t rtp_port, size_t rtcp_port, std::error_code &ec) { + // exit early if the error code is set + if (ec) { + return; + } + + // set up the transport header with the rtp and rtcp ports + auto transport_header = + "RTP/AVP;unicast;client_port=" + std::to_string(rtp_port) + "-" + std::to_string(rtcp_port); + + // send the setup request (no response is expected) + send_request("SETUP", path_, {{"Transport", transport_header}}, ec); + if (ec) { + return; + } + + init_rtp(rtp_port, ec); + init_rtcp(rtcp_port, ec); +} + +void RtspClient::play(std::error_code &ec) { + // exit early if the error code is set + if (ec) { + return; + } + // send the play request + send_request("PLAY", path_, {}, ec); +} + +void RtspClient::pause(std::error_code &ec) { + // exit early if the error code is set + if (ec) { + return; + } + // send the pause request + send_request("PAUSE", path_, {}, ec); +} + +void RtspClient::teardown(std::error_code &ec) { + // exit early if the error code is set + if (ec) { + return; + } + // send the teardown request + send_request("TEARDOWN", path_, {}, ec); +} + +bool RtspClient::parse_response(const std::string &response_data, std::error_code &ec) { + // exit early if the error code is set + if (ec) { + return false; + } + if (response_data.empty()) { + ec = std::make_error_code(std::errc::no_message); + logger_.error("Empty response"); + return false; + } + // RTP response is of the form: + // std::regex response_regex("RTSP/1.0 (\\d+) (.*)\r\n(.*)\r\n\r\n"); + // parse the response but don't use regex since it may be slow on embedded platforms + // make sure it matches the expected response format + if (response_data.starts_with("RTSP/1.0") != 0) { + ec = std::make_error_code(std::errc::protocol_error); + logger_.error("Invalid response"); + return false; + } + // parse the status code and message + int status_code = std::stoi(response_data.substr(9, 3)); + std::string status_message = response_data.substr(13, response_data.find("\r\n") - 13); + if (status_code != 200) { + ec = std::make_error_code(std::errc::protocol_error); + logger_.error(std::string("Request failed: ") + status_message); + return false; + } + // parse the session id + auto session_pos = response_data.find("Session: "); + if (session_pos != std::string::npos) { + session_id_ = response_data.substr(session_pos + 9, + response_data.find("\r\n", session_pos) - session_pos - 9); + } + // increment the cseq + cseq_++; + return true; +} + +void RtspClient::init_rtp(size_t rtp_port, std::error_code &ec) { + // exit early if the error code is set + if (ec) { + return; + } + logger_.debug("Starting rtp socket"); + auto rtp_task_config = espp::Task::Config{ + .name = "Rtp", + .callback = nullptr, + .stack_size_bytes = 16 * 1024, + }; + auto rtp_config = espp::UdpSocket::ReceiveConfig{ + .port = rtp_port, + .buffer_size = 2 * 1024, + .on_receive_callback = std::bind(&RtspClient::handle_rtp_packet, this, std::placeholders::_1, + std::placeholders::_2), + }; + if (!rtp_socket_.start_receiving(rtp_task_config, rtp_config)) { + ec = std::make_error_code(std::errc::operation_canceled); + logger_.error("Failed to start receiving rtp packets"); + return; + } +} + +void RtspClient::init_rtcp(size_t rtcp_port, std::error_code &ec) { + // exit early if the error code is set + if (ec) { + return; + } + logger_.debug("Starting rtcp socket"); + auto rtcp_task_config = espp::Task::Config{ + .name = "Rtcp", + .callback = nullptr, + .stack_size_bytes = 6 * 1024, + }; + auto rtcp_config = espp::UdpSocket::ReceiveConfig{ + .port = rtcp_port, + .buffer_size = 1 * 1024, + .on_receive_callback = std::bind(&RtspClient::handle_rtcp_packet, this, std::placeholders::_1, + std::placeholders::_2), + }; + if (!rtcp_socket_.start_receiving(rtcp_task_config, rtcp_config)) { + ec = std::make_error_code(std::errc::operation_canceled); + logger_.error("Failed to start receiving rtcp packets"); + return; + } +} + +std::optional> +RtspClient::handle_rtp_packet(std::vector &data, const espp::Socket::Info &sender_info) { + // jpeg frame that we are building + static std::unique_ptr jpeg_frame; + + logger_.debug("Got RTP packet of size: {}", data.size()); + + std::string_view packet(reinterpret_cast(data.data()), data.size()); + // parse the rtp packet + RtpJpegPacket rtp_jpeg_packet(packet); + auto frag_offset = rtp_jpeg_packet.get_offset(); + if (frag_offset == 0) { + // first fragment + logger_.debug("Received first fragment, size: {}, sequence number: {}", + rtp_jpeg_packet.get_data().size(), rtp_jpeg_packet.get_sequence_number()); + if (jpeg_frame) { + // we already have a frame, this is an error + logger_.warn("Received first fragment but already have a frame"); + jpeg_frame.reset(); + } + jpeg_frame = std::make_unique(rtp_jpeg_packet); + } else if (jpeg_frame) { + logger_.debug("Received middle fragment, size: {}, sequence number: {}", + rtp_jpeg_packet.get_data().size(), rtp_jpeg_packet.get_sequence_number()); + // middle fragment + jpeg_frame->append(rtp_jpeg_packet); + } else { + // we don't have a frame to append to but we got a middle fragment + // this is an error + logger_.warn("Received middle fragment without a frame"); + return {}; + } + + // check if this is the last packet of the frame (the last packet will have + // the marker bit set) + if (jpeg_frame && jpeg_frame->is_complete()) { + // get the jpeg data + auto jpeg_data = jpeg_frame->get_data(); + logger_.debug("Received jpeg frame of size: {} B", jpeg_data.size()); + // call the on_jpeg_frame callback + if (on_jpeg_frame_) { + on_jpeg_frame_(std::move(jpeg_frame)); + } + logger_.debug("Sent jpeg frame to callback, now jpeg_frame is nullptr? {}", + jpeg_frame == nullptr); + } + // return an empty vector to indicate that we don't want to send a response + return {}; +} + +std::optional> +RtspClient::handle_rtcp_packet(std::vector &data, const espp::Socket::Info &sender_info) { + // receive the rtcp packet + [[maybe_unused]] std::string_view packet(reinterpret_cast(data.data()), data.size()); + // TODO: parse the rtcp packet + // return an empty vector to indicate that we don't want to send a response + return {}; +} diff --git a/components/rtsp/src/rtsp_server.cpp b/components/rtsp/src/rtsp_server.cpp new file mode 100644 index 000000000..587807943 --- /dev/null +++ b/components/rtsp/src/rtsp_server.cpp @@ -0,0 +1,248 @@ +#include "rtsp_server.hpp" + +using namespace espp; + +RtspServer::RtspServer(const Config &config) + : BaseComponent("RTSP Server", config.log_level) + , server_address_(config.server_address) + , port_(config.port) + , path_(config.path) + , rtsp_socket_({.log_level = espp::Logger::Verbosity::WARN}) + , max_data_size_(config.max_data_size) { + // generate a random ssrc +#if defined(ESP_PLATFORM) + ssrc_ = esp_random(); +#else + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dis; + ssrc_ = dis(gen); +#endif +} + +RtspServer::~RtspServer() { stop(); } + +void RtspServer::set_session_log_level(Logger::Verbosity log_level) { + session_log_level_ = log_level; +} + +bool RtspServer::start() { + if (accept_task_ && accept_task_->is_started()) { + logger_.error("Server is already running"); + return false; + } + + logger_.info("Starting RTSP server on port {}", port_); + + if (!rtsp_socket_.bind(port_)) { + logger_.error("Failed to bind to port {}", port_); + return false; + } + + int max_pending_connections = 5; + if (!rtsp_socket_.listen(max_pending_connections)) { + logger_.error("Failed to listen on port {}", port_); + return false; + } + + using namespace std::placeholders; + accept_task_ = std::make_unique(Task::Config{ + .name = "RTSP Accept Task", + .callback = std::bind(&RtspServer::accept_task_function, this, _1, _2), + .stack_size_bytes = 6 * 1024, + .log_level = espp::Logger::Verbosity::WARN, + }); + accept_task_->start(); + return true; +} + +void RtspServer::stop() { + logger_.info("Stopping RTSP server"); + // stop the accept task + if (accept_task_) { + accept_task_->stop(); + } + // stop the session task + if (session_task_) { + session_task_->stop(); + } + // clear the list of sessions + sessions_.clear(); + // close the RTSP socket + rtsp_socket_.close(); +} + +void RtspServer::send_frame(const espp::JpegFrame &frame) { + // get the frame scan data + auto frame_header = frame.get_header(); + auto frame_data = frame.get_scan_data(); + + auto width = frame_header.get_width(); + auto height = frame_header.get_height(); + auto q0 = frame_header.get_quantization_table(0); + auto q1 = frame_header.get_quantization_table(1); + + // if the frame data is larger than the MTU, then we need to break it up + // into multiple RTP packets + size_t num_packets = frame_data.size() / max_data_size_ + 1; + logger_.debug("Frame data is {} bytes, breaking into {} packets", frame_data.size(), num_packets); + + // create num_packets RtpJpegPackets + // The first packet will have the quantization tables, and the last packet + // will have the end of image marker and the marker bit set + std::vector> packets; + packets.reserve(num_packets); + for (size_t i = 0; i < num_packets; i++) { + // get the start and end indices for the current packet + size_t start_index = i * max_data_size_; + size_t end_index = std::min(start_index + max_data_size_, frame_data.size()); + + static const int type_specific = 0; + static const int fragment_type = 0; + int offset = i * max_data_size_; + + std::unique_ptr packet; + // if this is the first packet, it has the quantization tables + if (i == 0) { + // use the original q value and include the quantization tables + packet = std::make_unique( + type_specific, fragment_type, 128, width, height, q0, q1, + frame_data.substr(start_index, end_index - start_index)); + } else { + // use a different q value (less than 128) and don't include the + // quantization tables + packet = std::make_unique( + type_specific, offset, fragment_type, 96, width, height, + frame_data.substr(start_index, end_index - start_index)); + } + + // set the payload type to 26 (JPEG) + packet->set_payload_type(26); + // set the sequence number + packet->set_sequence_number(sequence_number_++); + // set the timestamp + static auto start_time = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + auto timestamp = + std::chrono::duration_cast(now - start_time).count(); + packet->set_timestamp(timestamp * 90); + + // set the ssrc + packet->set_ssrc(ssrc_); + + // auto mjpeg_header = packet->get_mjpeg_header(); + // std::vector mjpeg_vec(mjpeg_header.begin(), mjpeg_header.end()); + + // if it's the last packet, set the marker bit + if (i == num_packets - 1) { + packet->set_marker(true); + } + + // make sure the packet header has been serialized + packet->serialize(); + + // add the packet to the list of packets + packets.emplace_back(std::move(packet)); + } + + // now move the packets into the rtp_packets_ vector + { + std::unique_lock lock(rtp_packets_mutex_); + // move the new packets into the list + rtp_packets_ = std::move(packets); + } +} + +bool RtspServer::accept_task_function(std::mutex &m, std::condition_variable &cv) { + // accept a new connection + auto control_socket = rtsp_socket_.accept(); + if (!control_socket) { + logger_.error("Failed to accept new connection"); + return false; + } + + logger_.info("Accepted new connection"); + + // create a new session + auto session = std::make_unique( + std::move(control_socket), + RtspSession::Config{.server_address = fmt::format("{}:{}", server_address_, port_), + .rtsp_path = path_, + .log_level = session_log_level_}); + + // add the session to the list of sessions + auto session_id = session->get_session_id(); + sessions_.emplace(session_id, std::move(session)); + + // start the session task if it is not already running + using namespace std::placeholders; + if (!session_task_ || !session_task_->is_started()) { + logger_.info("Starting session task"); + session_task_ = std::make_unique(Task::Config{ + .name = "RtspSessionTask", + .callback = std::bind(&RtspServer::session_task_function, this, _1, _2), + .stack_size_bytes = 6 * 1024, + .log_level = espp::Logger::Verbosity::WARN, + }); + session_task_->start(); + } + // we do not want to stop the task + return false; +} + +bool RtspServer::session_task_function(std::mutex &m, std::condition_variable &cv) { + // sleep between frames + { + using namespace std::chrono_literals; + std::unique_lock lk(m); + cv.wait_for(lk, 10ms); + } + + // when this function returns, the vector of pointers will go out of scope + // and the pointers will be deleted (which is good because it means we + // won't send the same frame twice) + std::vector> packets; + { + // copy the rtp packets into a local vector + std::unique_lock lock(rtp_packets_mutex_); + if (rtp_packets_.empty()) { + // if there is not a new frame (no packets), then simply return + // we do not want to stop the task + return false; + } + // move the packets into the local vector + packets = std::move(rtp_packets_); + } + + logger_.debug("Sending frame data to clients"); + + // for each session in sessions_ + // if the session is active + // send the latest frame to the client + std::lock_guard lk(session_mutex_); + for (auto &session : sessions_) { + [[maybe_unused]] auto session_id = session.first; + auto &session_ptr = session.second; + // send the packets to the client + for (auto &packet : packets) { + // if the session is not active or is closed, then stop sending + if (!session_ptr->is_active() || session_ptr->is_closed()) { + break; + } + session_ptr->send_rtp_packet(*packet); + } + } + // loop over the sessions and erase ones which are closed + for (auto it = sessions_.begin(); it != sessions_.end();) { + auto &session = it->second; + if (session->is_closed()) { + logger_.info("Removing session {}", session->get_session_id()); + it = sessions_.erase(it); + } else { + ++it; + } + } + + // we do not want to stop the task + return false; +} diff --git a/components/rtsp/src/rtsp_session.cpp b/components/rtsp/src/rtsp_session.cpp new file mode 100644 index 000000000..f162e3360 --- /dev/null +++ b/components/rtsp/src/rtsp_session.cpp @@ -0,0 +1,389 @@ +#include "rtsp_session.hpp" + +using namespace espp; + +RtspSession::RtspSession(std::unique_ptr control_socket, const Config &config) + : BaseComponent("RtspSession", config.log_level) + , control_socket_(std::move(control_socket)) + , rtp_socket_({.log_level = Logger::Verbosity::WARN}) + , rtcp_socket_({.log_level = Logger::Verbosity::WARN}) + , session_id_(generate_session_id()) + , server_address_(config.server_address) + , rtsp_path_(config.rtsp_path) + , client_address_(control_socket_->get_remote_info().address) { + // set the logger tag to include the session id + logger_.set_tag("RtspSession " + std::to_string(session_id_)); + // start the session task to handle RTSP commands + using namespace std::placeholders; + control_task_ = std::make_unique(Task::Config{ + .name = "RtspSession " + std::to_string(session_id_), + .callback = std::bind(&RtspSession::control_task_fn, this, _1, _2), + .stack_size_bytes = 6 * 1024, + .log_level = Logger::Verbosity::WARN, + }); + control_task_->start(); +} + +RtspSession::~RtspSession() { + teardown(); + // stop the session task + if (control_task_ && control_task_->is_started()) { + logger_.info("Stopping control task"); + control_task_->stop(); + } +} + +uint32_t RtspSession::get_session_id() const { return session_id_; } + +bool RtspSession::is_closed() const { return closed_; } + +bool RtspSession::is_connected() const { + return control_socket_ && control_socket_->is_connected(); +} + +bool RtspSession::is_active() const { return session_active_; } + +void RtspSession::play() { session_active_ = true; } + +void RtspSession::pause() { session_active_ = false; } + +void RtspSession::teardown() { + session_active_ = false; + closed_ = true; +} + +bool RtspSession::send_rtp_packet(const RtpPacket &packet) { + logger_.debug("Sending RTP packet"); + return rtp_socket_.send(packet.get_data(), { + .ip_address = client_address_, + .port = (size_t)client_rtp_port_, + }); +} + +bool RtspSession::send_rtcp_packet(const RtcpPacket &packet) { + logger_.debug("Sending RTCP packet"); + return rtcp_socket_.send(packet.get_data(), { + .ip_address = client_address_, + .port = (size_t)client_rtcp_port_, + }); +} + +bool RtspSession::send_response(int code, std::string_view message, int sequence_number, + std::string_view headers, std::string_view body) { + // create a response + std::string response = "RTSP/1.0 " + std::to_string(code) + " " + std::string(message) + "\r\n"; + if (sequence_number != -1) { + response += "CSeq: " + std::to_string(sequence_number) + "\r\n"; + } + if (!headers.empty()) { + response += headers; + } + if (!body.empty()) { + response += "Content-Length: " + std::to_string(body.size()) + "\r\n"; + response += "\r\n"; + response += body; + } else { + response += "\r\n"; + } + logger_.info("Sending RTSP response"); + logger_.debug("{}", response); + // send the response + return control_socket_->transmit(response); +} + +bool RtspSession::handle_rtsp_options(std::string_view request) { + int sequence_number = 0; + if (!parse_rtsp_command_sequence(request, sequence_number)) { + return handle_rtsp_invalid_request(request); + } + logger_.info("RTSP OPTIONS request"); + // create a response + int code = 200; + std::string message = "OK"; + std::string headers = "Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n"; + return send_response(code, message, sequence_number, headers); +} + +bool RtspSession::handle_rtsp_describe(std::string_view request) { + int sequence_number = 0; + if (!parse_rtsp_command_sequence(request, sequence_number)) { + return handle_rtsp_invalid_request(request); + } + logger_.info("RTSP DESCRIBE request"); + // create a response + int code = 200; + std::string message = "OK"; + // SDP description for an MJPEG stream + std::string rtsp_path = "rtsp://" + server_address_ + "/" + rtsp_path_; + std::string body = "v=0\r\n" // version (0) + "o=- " + + std::to_string(session_id_) + " 1 IN IP4 " + server_address_ + + "\r\n" // username (none), session id, version, network type (internet), + // address type, address + "s=MJPEG Stream\r\n" // session name (can be anything) + "i=MJPEG Stream\r\n" // session name (can be anything) + "t=0 0\r\n" // start / stop + "a=control:" + + rtsp_path + + "\r\n" // the RTSP path + "a=mimetype:string;\"video/x-motion-jpeg\"\r\n" // MIME type + "m=video 0 RTP/AVP 26\r\n" // MJPEG + "c=IN IP4 0.0.0.0\r\n" // client will use the RTSP address + "b=AS:256\r\n" // 256kbps + "a=control:" + + rtsp_path + + "\r\n" + "a=udp-only\r\n"; + + std::string headers = "Content-Type: application/sdp\r\n" + "Content-Base: " + + rtsp_path + "\r\n"; + return send_response(code, message, sequence_number, headers, body); +} + +bool RtspSession::handle_rtsp_setup(std::string_view request) { + // parse the rtsp path from the request + std::string_view rtsp_path; + int client_rtp_port; + int client_rtcp_port; + if (!parse_rtsp_setup_request(request, rtsp_path, client_rtp_port, client_rtcp_port)) { + // the parse function will send the response, so we just need to return + return false; + } + // parse the sequence number from the request + int sequence_number = 0; + if (!parse_rtsp_command_sequence(request, sequence_number)) { + return handle_rtsp_invalid_request(request); + } + logger_.info("RTSP SETUP request"); + // save the client port numbers + client_rtp_port_ = client_rtp_port; + client_rtcp_port_ = client_rtcp_port; + // create a response + int code = 200; + std::string message = "OK"; + // flesh out the transport header + std::string headers = + "Session: " + std::to_string(session_id_) + "\r\n" + + "Transport: RTP/AVP;unicast;client_port=" + std::to_string(client_rtp_port) + "-" + + std::to_string(client_rtcp_port) + "\r\n"; + return send_response(code, message, sequence_number, headers); +} + +bool RtspSession::handle_rtsp_play(std::string_view request) { + int sequence_number = 0; + if (!parse_rtsp_command_sequence(request, sequence_number)) { + return handle_rtsp_invalid_request(request); + } + logger_.info("RTSP PLAY request"); + play(); + int code = 200; + std::string message = "OK"; + std::string headers = + "Session: " + std::to_string(session_id_) + "\r\n" + "Range: npt=0.000-\r\n"; + return send_response(code, message, sequence_number, headers); +} + +bool RtspSession::handle_rtsp_pause(std::string_view request) { + int sequence_number = 0; + if (!parse_rtsp_command_sequence(request, sequence_number)) { + return handle_rtsp_invalid_request(request); + } + logger_.info("RTSP PAUSE request"); + pause(); + int code = 200; + std::string message = "OK"; + std::string headers = "Session: " + std::to_string(session_id_) + "\r\n"; + return send_response(code, message, sequence_number, headers); +} + +bool RtspSession::handle_rtsp_teardown(std::string_view request) { + int sequence_number = 0; + if (!parse_rtsp_command_sequence(request, sequence_number)) { + return handle_rtsp_invalid_request(request); + } + logger_.info("RTSP TEARDOWN request"); + teardown(); + int code = 200; + std::string message = "OK"; + std::string headers = "Session: " + std::to_string(session_id_) + "\r\n"; + return send_response(code, message, sequence_number, headers); +} + +bool RtspSession::handle_rtsp_invalid_request(std::string_view request) { + logger_.info("RTSP invalid request"); + // create a response + int code = 400; + std::string message = "Bad Request"; + int sequence_number = 0; + if (!parse_rtsp_command_sequence(request, sequence_number)) { + return send_response(code, message); + } + return send_response(code, message, sequence_number); +} + +bool RtspSession::handle_rtsp_request(std::string_view request) { + logger_.debug("RTSP request:\n{}", request); + // store indices of the first and second spaces + // to extract the method and the rtsp path + auto first_space_index = request.find(' '); + auto second_space_index = request.find(' ', first_space_index + 1); + auto end_of_line_index = request.find('\r'); + if (first_space_index == std::string::npos || second_space_index == std::string::npos || + end_of_line_index == std::string::npos) { + return handle_rtsp_invalid_request(request); + } + // extract the method and the rtsp path + // where the request looks like "METHOD RTSP_PATH RTSP_VERSION" + std::string_view method = request.substr(0, first_space_index); + // TODO: we should probably check that the rtsp path is correct + [[maybe_unused]] std::string_view rtsp_path = + request.substr(first_space_index + 1, second_space_index - first_space_index - 1); + // TODO: we should probably check that the rtsp version is correct + [[maybe_unused]] std::string_view rtsp_version = + request.substr(second_space_index + 1, end_of_line_index - second_space_index - 1); + // extract the request body, which is separated by an empty line (\r\n) + // from the request header + std::string_view request_body = request.substr(end_of_line_index + 2); + + // handle the request + if (method == "OPTIONS") { + return handle_rtsp_options(request_body); + } else if (method == "DESCRIBE") { + return handle_rtsp_describe(request_body); + } else if (method == "SETUP") { + return handle_rtsp_setup(request_body); + } else if (method == "PLAY") { + return handle_rtsp_play(request_body); + } else if (method == "PAUSE") { + return handle_rtsp_pause(request_body); + } else if (method == "TEARDOWN") { + return handle_rtsp_teardown(request_body); + } + + // if the method is not supported, return an error + return handle_rtsp_invalid_request(request_body); +} + +bool RtspSession::control_task_fn(std::mutex &m, std::condition_variable &cv) { + if (closed_) { + logger_.info("Session is closed, stopping control task"); + // return true to stop the task + return true; + } + if (!control_socket_) { + logger_.warn("Control socket is no longer valid, stopping control task"); + teardown(); + // return true to stop the task + return true; + } + if (!control_socket_->is_connected()) { + logger_.warn("Control socket is not connected, stopping control task"); + teardown(); + // if the control socket is not connected, return true to stop the task + return true; + } + static size_t max_request_size = 1024; + std::vector buffer; + logger_.info("Waiting for RTSP request"); + if (control_socket_->receive(buffer, max_request_size)) { + // parse the request + std::string_view request(reinterpret_cast(buffer.data()), buffer.size()); + // handle the request + if (!handle_rtsp_request(request)) { + logger_.warn("Failed to handle RTSP request"); + } + } + // the receive handles most of the blocking, so we don't need to sleep + // here, just return false to keep the task running + return false; +} + +uint32_t RtspSession::generate_session_id() { +#if defined(ESP_PLATFORM) + return esp_random(); +#else + static std::random_device rd; + static std::mt19937 gen(rd()); + static std::uniform_int_distribution<> dis(0, std::numeric_limits::max()); + return dis(gen); +#endif +} + +bool RtspSession::parse_rtsp_command_sequence(std::string_view request, int &cseq) { + // parse the cseq from the request + auto cseq_index = request.find("CSeq: "); + if (cseq_index == std::string::npos) { + return false; + } + auto cseq_end_index = request.find('\r', cseq_index); + if (cseq_end_index == std::string::npos) { + return false; + } + std::string_view cseq_str = request.substr(cseq_index + 6, cseq_end_index - cseq_index - 6); + if (cseq_str.empty()) { + return false; + } + // convert the cseq to an integer + cseq = std::stoi(std::string{cseq_str}); + return true; +} + +std::string_view RtspSession::parse_rtsp_path(std::string_view request) { + // parse the rtsp path from the request + // where the request looks like "METHOD RTSP_PATH RTSP_VERSION" + std::string_view rtsp_path = request.substr( + request.find(' ') + 1, request.find(' ', request.find(' ') + 1) - request.find(' ') - 1); + return rtsp_path; +} + +bool RtspSession::parse_rtsp_setup_request(std::string_view request, std::string_view &rtsp_path, + int &client_rtp_port, int &client_rtcp_port) { + // parse the rtsp path from the request + rtsp_path = parse_rtsp_path(request); + if (rtsp_path.empty()) { + return false; + } + logger_.debug("Parsing setup request:\n{}", request); + // parse the transport header from the request + auto transport_index = request.find("Transport: "); + if (transport_index == std::string::npos) { + return false; + } + auto transport_end_index = request.find('\r', transport_index); + if (transport_end_index == std::string::npos) { + return false; + } + std::string_view transport = + request.substr(transport_index + 11, transport_end_index - transport_index - 11); + if (transport.empty()) { + return false; + } + logger_.debug("Transport header: {}", transport); + // we don't support TCP, so return an error if the transport is not RTP/AVP/UDP + if (transport.find("RTP/AVP/TCP") != std::string::npos) { + logger_.error("TCP transport is not supported"); + // TODO: this doesn't send the sequence number back to the client + send_response(461, "Unsupported Transport"); + return false; + } + + // parse the rtp port from the request + auto client_port_index = request.find("client_port="); + auto dash_index = request.find('-', client_port_index); + std::string_view rtp_port = + request.substr(client_port_index + 12, dash_index - client_port_index - 12); + if (rtp_port.empty()) { + return false; + } + // parse the rtcp port from the request + std::string_view rtcp_port = + request.substr(dash_index + 1, request.find('\r', client_port_index) - dash_index - 1); + if (rtcp_port.empty()) { + return false; + } + // convert the rtp and rtcp ports to integers + client_rtp_port = std::stoi(std::string{rtp_port}); + client_rtcp_port = std::stoi(std::string{rtcp_port}); + return true; +} diff --git a/components/socket/CMakeLists.txt b/components/socket/CMakeLists.txt index dd3a9d46e..e076d16cc 100644 --- a/components/socket/CMakeLists.txt +++ b/components/socket/CMakeLists.txt @@ -1,3 +1,4 @@ idf_component_register( INCLUDE_DIRS "include" + SRC_DIRS "src" PRIV_REQUIRES base_component task lwip) diff --git a/components/socket/example/main/socket_example.cpp b/components/socket/example/main/socket_example.cpp index 49efa5d56..06f02097a 100644 --- a/components/socket/example/main/socket_example.cpp +++ b/components/socket/example/main/socket_example.cpp @@ -344,7 +344,7 @@ fmt::print(fg(fmt::terminal_color::yellow) | fmt::emphasis::bold, std::vector data{0, 1, 2, 3, 4}; std::transform(data.begin(), data.end(), data.begin(), [](const auto &d) { return d + iterations; }); - auto transmit_config = espp::detail::TcpTransmitConfig{ + auto transmit_config = espp::TcpSocket::TransmitConfig{ .wait_for_response = true, .response_size = 128, .on_response_callback = diff --git a/components/socket/include/socket.hpp b/components/socket/include/socket.hpp index 2075cde3f..2bec5fc54 100644 --- a/components/socket/include/socket.hpp +++ b/components/socket/include/socket.hpp @@ -1,19 +1,25 @@ #pragma once -#include -#include -#include -#include +#include "socket_msvc.hpp" +#ifdef _MSC_VER +typedef unsigned int sock_type_t; +#else +/* Assume that any non-Windows platform uses POSIX-style sockets instead. */ #include +#include /* Needed for getaddrinfo() and freeaddrinfo() */ #include #include #include - -#if !defined(ESP_PLATFORM) -#include +#include /* Needed for close() */ +typedef int sock_type_t; #endif +#include +#include +#include +#include + #include #include "base_component.hpp" @@ -50,86 +56,44 @@ class Socket : public BaseComponent { * @param addr IPv4 address string * @param prt port number */ - void init_ipv4(const std::string &addr, size_t prt) { - address = addr; - port = prt; - auto server_address = ipv4_ptr(); - server_address->sin_family = AF_INET; - server_address->sin_addr.s_addr = inet_addr(address.c_str()); - server_address->sin_port = htons(port); - } + void init_ipv4(const std::string &addr, size_t prt); /** * @brief Gives access to IPv4 sockaddr structure (sockaddr_in) for use * with low level socket calls like sendto / recvfrom. * @return *sockaddr_in pointer to ipv4 data structure */ - struct sockaddr_in *ipv4_ptr() { - return (struct sockaddr_in *)&raw; - } + struct sockaddr_in *ipv4_ptr(); /** * @brief Gives access to IPv6 sockaddr structure (sockaddr_in6) for use * with low level socket calls like sendto / recvfrom. * @return *sockaddr_in6 pointer to ipv6 data structure */ - struct sockaddr_in6 *ipv6_ptr() { - return (struct sockaddr_in6 *)&raw; - } + struct sockaddr_in6 *ipv6_ptr(); /** * @brief Will update address and port based on the curent data in raw. */ - void update() { - if (raw.ss_family == PF_INET) { - address = inet_ntoa(((struct sockaddr_in *)&raw)->sin_addr); - port = ((struct sockaddr_in *)&raw)->sin_port; - } else if (raw.ss_family == PF_INET6) { -#if defined(ESP_PLATFORM) - address = inet_ntoa(((struct sockaddr_in6 *)&raw)->sin6_addr); -#else - char str[INET6_ADDRSTRLEN]; - inet_ntop(AF_INET6, &(((struct sockaddr_in6 *)&raw)->sin6_addr), str, INET6_ADDRSTRLEN); - address = str; -#endif - port = ((struct sockaddr_in6 *)&raw)->sin6_port; - } - } + void update(); /** * @brief Fill this Info from the provided sockaddr struct. * @param &source_address sockaddr info filled out by recvfrom. */ - void from_sockaddr(const struct sockaddr_storage &source_address) { - memcpy(&raw, &source_address, sizeof(source_address)); - update(); - } + void from_sockaddr(const struct sockaddr_storage &source_address); /** * @brief Fill this Info from the provided sockaddr struct. * @param &source_address sockaddr info filled out by recvfrom. */ - void from_sockaddr(const struct sockaddr_in &source_address) { - memcpy(&raw, &source_address, sizeof(source_address)); - address = inet_ntoa(source_address.sin_addr); - port = source_address.sin_port; - } + void from_sockaddr(const struct sockaddr_in &source_address); /** * @brief Fill this Info from the provided sockaddr struct. * @param &source_address sockaddr info filled out by recvfrom. */ - void from_sockaddr(const struct sockaddr_in6 &source_address) { -#if defined(ESP_PLATFORM) - address = inet_ntoa(source_address.sin6_addr); -#else - char str[INET6_ADDRSTRLEN]; - inet_ntop(AF_INET6, &(source_address.sin6_addr), str, INET6_ADDRSTRLEN); - address = str; -#endif - port = source_address.sin6_port; - memcpy(&raw, &source_address, sizeof(source_address)); - } + void from_sockaddr(const struct sockaddr_in6 &source_address); }; /** @@ -156,10 +120,7 @@ class Socket : public BaseComponent { * @param logger_config configuration for the logger associated with the * socket. */ - explicit Socket(int socket_fd, const Logger::Config &logger_config) - : BaseComponent(logger_config) { - socket_ = socket_fd; - } + explicit Socket(sock_type_t socket_fd, const espp::Logger::Config &logger_config); /** * @brief Initialize the socket (calling init()). @@ -167,28 +128,25 @@ class Socket : public BaseComponent { * @param logger_config configuration for the logger associated with the * socket. */ - explicit Socket(Type type, const Logger::Config &logger_config) - : BaseComponent(logger_config) { - init(type); - } + explicit Socket(Type type, const espp::Logger::Config &logger_config); /** * @brief Tear down any resources associted with the socket. */ - ~Socket() { cleanup(); } + ~Socket(); /** * @brief Is the socket valid. * @return true if the socket file descriptor is >= 0. */ - bool is_valid() const { return socket_ >= 0; } + bool is_valid() const; /** * @brief Is the socket valid. * @param socket_fd Socket file descriptor. * @return true if the socket file descriptor is >= 0. */ - static bool is_valid(int socket_fd) { return socket_fd >= 0; } + static bool is_valid_fd(sock_type_t socket_fd); /** * @brief Get the Socket::Info for the socket. @@ -197,75 +155,21 @@ class Socket : public BaseComponent { * structure. * @return Socket::Info for the socket. */ - std::optional get_ipv4_info() { - struct sockaddr_storage addr; - socklen_t addr_len = sizeof(addr); - if (getsockname(socket_, (struct sockaddr *)&addr, &addr_len) < 0) { - logger_.error("getsockname() failed: {} - {}", errno, strerror(errno)); - return {}; - } - Info info; - info.from_sockaddr(addr); - return info; - } + std::optional get_ipv4_info(); /** * @brief Set the receive timeout on the provided socket. * @param timeout requested timeout, must be > 0. * @return true if SO_RECVTIMEO was successfully set. */ - bool set_receive_timeout(const std::chrono::duration &timeout) { - float seconds = timeout.count(); - if (seconds <= 0) { - return true; - } - float intpart; - float fractpart = modf(seconds, &intpart); - const time_t response_timeout_s = (int)intpart; - const time_t response_timeout_us = (int)(fractpart * 1E6); - //// Alternatively we could do this: - // int microseconds = - // (int)(std::chrono::duration_cast(timeout).count()) % (int)1E6; - // const time_t response_timeout_s = floor(seconds); - // const time_t response_timeout_us = microseconds; - - struct timeval tv; - tv.tv_sec = response_timeout_s; - tv.tv_usec = response_timeout_us; - int err = setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv)); - if (err < 0) { - return false; - } - return true; - } + bool set_receive_timeout(const std::chrono::duration &timeout); /** * @brief Allow others to use this address/port combination after we're done * with it. * @return true if SO_REUSEADDR and SO_REUSEPORT were successfully set. */ - bool enable_reuse() { -#if !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM) - fmt::print(fg(fmt::color::red), "CONFIG_LWIP_SO_REUSE not defined!\n"); - return false; -#else // CONFIG_LWIP_SO_REUSE || !defined(ESP_PLATFORM) - int err = 0; - int enabled = 1; - err = setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)); - if (err < 0) { - fmt::print(fg(fmt::color::red), "Couldn't set SO_REUSEADDR\n"); - return false; - } -#if !defined(ESP_PLATFORM) - err = setsockopt(socket_, SOL_SOCKET, SO_REUSEPORT, &enabled, sizeof(enabled)); - if (err < 0) { - fmt::print(fg(fmt::color::red), "Couldn't set SO_REUSEPORT\n"); - return false; - } -#endif // !defined(ESP_PLATFORM) - return true; -#endif // !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM) - } + bool enable_reuse(); /** * @brief Configure the socket to be multicast (if time_to_live > 0). @@ -276,22 +180,7 @@ class Socket : public BaseComponent { * @param loopback_enabled Whether to receive our own multicast packets. * @return true if IP_MULTICAST_TTL and IP_MULTICAST_LOOP were set. */ - bool make_multicast(uint8_t time_to_live = 1, uint8_t loopback_enabled = true) { - int err = 0; - // Assign multicast TTL - separate from normal interface TTL - err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_TTL, &time_to_live, sizeof(uint8_t)); - if (err < 0) { - fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_TTL\n"); - return false; - } - // select whether multicast traffic should be received by this device, too - err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_LOOP, &loopback_enabled, sizeof(uint8_t)); - if (err < 0) { - fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_LOOP\n"); - return false; - } - return true; - } + bool make_multicast(uint8_t time_to_live = 1, uint8_t loopback_enabled = true); /** * @brief If this is a server socket, add it to the provided the multicast @@ -305,131 +194,45 @@ class Socket : public BaseComponent { * @param multicast_group multicast group to join. * @return true if IP_ADD_MEMBERSHIP was successfully set. */ - bool add_multicast_group(const std::string &multicast_group) { - struct ip_mreq imreq; - int err = 0; - - // Configure source interface -#if defined(ESP_PLATFORM) - imreq.imr_interface.s_addr = IPADDR_ANY; - // Configure multicast address to listen to - err = inet_aton(multicast_group.c_str(), &imreq.imr_multiaddr.s_addr); -#else - imreq.imr_interface.s_addr = htonl(INADDR_ANY); - // Configure multicast address to listen to - err = inet_aton(multicast_group.c_str(), &imreq.imr_multiaddr); -#endif + bool add_multicast_group(const std::string &multicast_group); - if (err != 1 || !IN_MULTICAST(ntohl(imreq.imr_multiaddr.s_addr))) { - // it's not actually a multicast address, so return false? - fmt::print(fg(fmt::color::red), "Not a valid multicast address ({})\n", multicast_group); - return false; - } - - // Assign the IPv4 multicast source interface, via its IP - // (only necessary if this socket is IPV4 only) - struct in_addr iaddr; - err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_IF, &iaddr, sizeof(struct in_addr)); - if (err < 0) { - fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_IF: {} - '{}'\n", errno, - strerror(errno)); - return false; - } - - err = setsockopt(socket_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(struct ip_mreq)); - if (err < 0) { - fmt::print(fg(fmt::color::red), "Couldn't set IP_ADD_MEMBERSHIP: {} - '{}'\n", errno, - strerror(errno)); - return false; - } - - return true; - } - - int select(std::chrono::microseconds timeout) { - fd_set readfds; - fd_set writefds; - fd_set exceptfds; - FD_ZERO(&readfds); - FD_ZERO(&writefds); - FD_ZERO(&exceptfds); - FD_SET(socket_, &readfds); - FD_SET(socket_, &writefds); - FD_SET(socket_, &exceptfds); - int nfds = socket_ + 1; - // convert timeout to timeval - struct timeval tv; - tv.tv_sec = std::chrono::duration_cast(timeout).count(); - tv.tv_usec = std::chrono::duration_cast(timeout).count() % 1000000; - int retval = ::select(nfds, &readfds, &writefds, &exceptfds, &tv); - if (retval < 0) { - logger_.error("select failed: {} - '{}'", errno, strerror(errno)); - return -1; - } - if (retval == 0) { - logger_.warn("select timed out"); - return 0; - } - if (FD_ISSET(socket_, &readfds)) { - logger_.debug("select read"); - } - if (FD_ISSET(socket_, &writefds)) { - logger_.debug("select write"); - } - if (FD_ISSET(socket_, &exceptfds)) { - logger_.debug("select except"); - } - return retval; - } + /** + * @brief Select on the socket for read events. + * @param timeout how long to wait for an event. + * @return number of events that occurred. + */ + int select(const std::chrono::microseconds &timeout); protected: /** * @brief Create the TCP socket and enable reuse. * @return true if the socket was initialized properly, false otherwise */ - bool init(Type type) { - // actually make the socket - socket_ = socket(address_family_, (int)type, ip_protocol_); - if (!is_valid()) { - logger_.error("Cannot create socket: {} - '{}'", errno, strerror(errno)); - return false; - } - // cppcheck-suppress knownConditionTrueFalse - if (!enable_reuse()) { - logger_.error("Cannot enable reuse: {} - '{}'", errno, strerror(errno)); - return false; - } - return true; - } + bool init(Type type); + + /** + * @brief Get the string representation of the last error that occurred. + * @return string representation of the last error that occurred. + */ + std::string error_string() const; + + /** + * @brief Get the string representation of the last error that occurred. + * @param error error code to get the string representation of. + * @return string representation of the last error that occurred. + */ + std::string error_string(int error) const; /** * @brief If the socket was created, we shut it down and close it here. */ - void cleanup() { - if (is_valid()) { - shutdown(socket_, 0); - close(socket_); - socket_ = -1; - logger_.info("Closed socket"); - } - } + void cleanup(); static constexpr int address_family_{AF_INET}; static constexpr int ip_protocol_{IPPROTO_IP}; - int socket_; + sock_type_t socket_; }; } // namespace espp -// for allowing easy serialization/printing of the -// espp::Socket::Info -template <> struct fmt::formatter { - template constexpr auto parse(ParseContext &ctx) const { - return ctx.begin(); - } - - template - auto format(espp::Socket::Info const &info, FormatContext &ctx) const { - return fmt::format_to(ctx.out(), "{}:{}", info.address, info.port); - } -}; +#include "socket_formatters.hpp" diff --git a/components/socket/include/socket_formatters.hpp b/components/socket/include/socket_formatters.hpp new file mode 100644 index 000000000..a68e9c7cd --- /dev/null +++ b/components/socket/include/socket_formatters.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "format.hpp" + +// for allowing easy serialization/printing of the +// espp::Socket::Info +template <> struct fmt::formatter { + template constexpr auto parse(ParseContext &ctx) const { + return ctx.begin(); + } + + template + auto format(espp::Socket::Info const &info, FormatContext &ctx) const { + return fmt::format_to(ctx.out(), "{}:{}", info.address, info.port); + } +}; diff --git a/components/socket/include/socket_msvc.hpp b/components/socket/include/socket_msvc.hpp new file mode 100644 index 000000000..f63882b29 --- /dev/null +++ b/components/socket/include/socket_msvc.hpp @@ -0,0 +1,16 @@ +#ifdef _MSC_VER +extern "C" { +// if we don't define NOMINMAX, windows.h will define min and max as macros +// which will conflict with std::min and std::max +#define NOMINMAX \ +// /* See http://stackoverflow.com/questions/12765743/getaddrinfo-on-win32 */ +// #ifndef _WIN32_WINNT +// #define _WIN32_WINNT 0x0501 /* Windows XP. */ +// #endif +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +} +#endif diff --git a/components/socket/include/tcp_socket.hpp b/components/socket/include/tcp_socket.hpp index bb956f551..42bdc7ec0 100644 --- a/components/socket/include/tcp_socket.hpp +++ b/components/socket/include/tcp_socket.hpp @@ -1,36 +1,20 @@ #pragma once +#include "socket_msvc.hpp" + +#ifndef _MSC_VER +#include +#endif // _MSC_VER + #include #include #include -#include - #include "logger.hpp" #include "socket.hpp" #include "task.hpp" namespace espp { -namespace detail { -/** - * @brief Config struct for sending data to a remote TCP socket. - * @note This is only used when waiting for a response from the remote. - * @note This must be outside the TcpSocket class because of a gcc bug that - * still has not been fixed. See - * https://stackoverflow.com/questions/53408962/try-to-understand-compiler-error-message-default-member-initializer-required-be - */ -struct TcpTransmitConfig { - bool wait_for_response{false}; /**< Whether to wait for a response from the remote or not. */ - size_t response_size{ - 0}; /**< If waiting for a response, this is the maximum size response we will receive. */ - Socket::response_callback_fn on_response_callback{ - nullptr}; /**< If waiting for a response, this is an optional handler which is provided the - response data. */ - std::chrono::duration response_timeout{ - 0.5f}; /**< If waiting for a response, this is the maximum timeout to wait. */ -}; -} // namespace detail - /** * @brief Class for managing sending and receiving data using TCP/IP. Can be * used to create client or server sockets. @@ -52,8 +36,8 @@ class TcpSocket : public Socket { * @brief Config struct for the TCP socket. */ struct Config { - Logger::Verbosity log_level{ - Logger::Verbosity::WARN}; /**< Verbosity level for the TCP socket logger. */ + espp::Logger::Verbosity log_level{ + espp::Logger::Verbosity::WARN}; /**< Verbosity level for the TCP socket logger. */ }; /** @@ -64,77 +48,63 @@ class TcpSocket : public Socket { size_t port; /**< Port number to send data to.*/ }; + /** + * @brief Config struct for sending data to a remote TCP socket. + * @note This is only used when waiting for a response from the remote. + */ + struct TransmitConfig { + bool wait_for_response = false; /**< Whether to wait for a response from the remote or not. */ + size_t response_size = + 0; /**< If waiting for a response, this is the maximum size response we will receive. */ + espp::Socket::response_callback_fn on_response_callback = nullptr; /**< If waiting for a + response, this is an optional handler which is provided the response data. */ + std::chrono::duration response_timeout = std::chrono::duration( + 0.5f); /**< If waiting for a response, this is the maximum timeout to wait. */ + + static TransmitConfig Default() { return {}; } + }; + /** * @brief Initialize the socket and associated resources. * @note Enables keepalive on the socket. * @param config Config for the socket. */ - explicit TcpSocket(const Config &config) - : Socket(Type::STREAM, Logger::Config{.tag = "TcpSocket", .level = config.log_level}) { - set_keepalive(); - } + explicit TcpSocket(const espp::TcpSocket::Config &config); /** * @brief Tear down any resources associted with the socket. */ - ~TcpSocket() { - // we have to explicitly call cleanup here so that the server accept / - // read will return and the task can stop. - cleanup(); - } + ~TcpSocket(); /** * @brief Reinitialize the socket, cleaning it up if first it is already * initalized. */ - void reinit() { - if (is_valid()) { - // cleanup our socket - cleanup(); - } - init(Type::STREAM); - } + void reinit(); /** * @brief Close the socket. */ - void close() { ::close(socket_); } + void close(); /** * @brief Check if the socket is connected to a remote endpoint. * @return true if the socket is connected to a remote endpoint. */ - bool is_connected() const { return connected_; } + bool is_connected() const; /** * @brief Open a connection to the remote TCP server. * @param connect_config ConnectConfig struct describing the server endpoint. * @return true if the client successfully connected to the server. */ - bool connect(const ConnectConfig &connect_config) { - if (!is_valid()) { - logger_.error("Socket invalid, cannot connect"); - return false; - } - Socket::Info server_info; - server_info.init_ipv4(connect_config.ip_address, connect_config.port); - auto server_address = server_info.ipv4_ptr(); - logger_.info("Client connecting to {}", server_info); - // connect - int error = ::connect(socket_, (struct sockaddr *)server_address, sizeof(*server_address)); - if (error != 0) { - logger_.error("Could not connect to the server: {} - '{}'", errno, strerror(errno)); - return false; - } - connected_ = true; - return true; - } + bool connect(const espp::TcpSocket::ConnectConfig &connect_config); /** * @brief Get the remote endpoint info. * @return The remote endpoint info. */ - const Socket::Info &get_remote_info() const { return remote_info_; } + const espp::Socket::Info &get_remote_info() const; /** * @brief Send data to the endpoint already connected to by TcpSocket::connect. @@ -144,14 +114,13 @@ class TcpSocket : public Socket { * send_config which will be provided the response data for * processing. * @param data vector of bytes to send to the remote endpoint. - * @param transmit_config detail::TcpTransmitConfig struct indicating whether to wait for a + * @param transmit_config TransmitConfig struct indicating whether to wait for a * response. * @return true if the data was sent, false otherwise. */ bool transmit(const std::vector &data, - const detail::TcpTransmitConfig &transmit_config = {}) { - return transmit(std::string_view{(const char *)data.data(), data.size()}, transmit_config); - } + const espp::TcpSocket::TransmitConfig &transmit_config = + espp::TcpSocket::TransmitConfig::Default()); /** * @brief Send data to the endpoint already connected to by TcpSocket::connect. @@ -161,14 +130,13 @@ class TcpSocket : public Socket { * send_config which will be provided the response data for * processing. * @param data vector of bytes to send to the remote endpoint. - * @param transmit_config detail::TcpTransmitConfig struct indicating whether to wait for a + * @param transmit_config TransmitConfig struct indicating whether to wait for a * response. * @return true if the data was sent, false otherwise. */ bool transmit(const std::vector &data, - const detail::TcpTransmitConfig &transmit_config = {}) { - return transmit(std::string_view{(const char *)data.data(), data.size()}, transmit_config); - } + const espp::TcpSocket::TransmitConfig &transmit_config = + espp::TcpSocket::TransmitConfig::Default()); /** * @brief Send data to the endpoint already connected to by TcpSocket::connect. @@ -178,56 +146,12 @@ class TcpSocket : public Socket { * send_config which will be provided the response data for * processing. * @param data string view of bytes to send to the remote endpoint. - * @param transmit_config detail::TcpTransmitConfig struct indicating whether to wait for a + * @param transmit_config TransmitConfig struct indicating whether to wait for a * response. * @return true if the data was sent, false otherwise. */ - bool transmit(std::string_view data, const detail::TcpTransmitConfig &transmit_config = {}) { - if (!is_valid()) { - logger_.error("Socket invalid, cannot send"); - return false; - } - // set the receive timeout - if (!set_receive_timeout(transmit_config.response_timeout)) { - logger_.error("Could not set receive timeout to {}: {} - '{}'", - transmit_config.response_timeout.count(), errno, strerror(errno)); - return false; - } - // write - logger_.info("Client sending {} bytes", data.size()); - int num_bytes_sent = write(socket_, data.data(), data.size()); - if (num_bytes_sent < 0) { - logger_.error("Error occurred during sending: {} - '{}'", errno, strerror(errno)); - // update our connection state here since remote end was likely closed... - connected_ = false; - return false; - } - logger_.debug("Client sent {} bytes", num_bytes_sent); - // we don't need to wait for a response and the socket is good; - if (!transmit_config.wait_for_response) { - return true; - } - if (transmit_config.response_size == 0) { - logger_.warn("Response requested, but response_size=0, not waiting for response!"); - // NOTE: we did send successfully, so we return true and warn about - // misconfiguration - return true; - } - std::vector received_data; - logger_.info("Client waiting for response"); - // read - if (!receive(received_data, transmit_config.response_size)) { - logger_.warn("Client could not get response, remote socket might have closed!"); - // TODO: should we upate our connected_ variable here? - return false; - } - logger_.info("Client got {} bytes of response", received_data.size()); - if (transmit_config.on_response_callback) { - logger_.debug("Client calling response callback"); - transmit_config.on_response_callback(received_data); - } - return true; - } + bool transmit(std::string_view data, const espp::TcpSocket::TransmitConfig &transmit_config = + espp::TcpSocket::TransmitConfig::Default()); /** * @brief Call read on the socket, assuming it has already been configured @@ -237,19 +161,7 @@ class TcpSocket : public Socket { * @param max_num_bytes Maximum number of bytes to receive. * @return true if successfully received, false otherwise. */ - bool receive(std::vector &data, size_t max_num_bytes) { - // make some space for received data - put it on the heap so that our - // stack usage doesn't change depending on max_num_bytes - std::unique_ptr receive_buffer(new uint8_t[max_num_bytes]()); - int num_bytes_received = receive(receive_buffer.get(), max_num_bytes); - if (num_bytes_received > 0) { - logger_.info("Received {} bytes", num_bytes_received); - uint8_t *data_ptr = (uint8_t *)receive_buffer.get(); - data.assign(data_ptr, data_ptr + num_bytes_received); - return true; - } - return false; - } + bool receive(std::vector &data, size_t max_num_bytes); /** * @brief Call read on the socket, assuming it has already been configured @@ -261,56 +173,14 @@ class TcpSocket : public Socket { * @param max_num_bytes Maximum number of bytes to receive. * @return Number of bytes received. */ - size_t receive(uint8_t *data, size_t max_num_bytes) { - if (!is_valid()) { - logger_.error("Socket invalid, cannot receive."); - return 0; - } - if (!is_connected()) { - logger_.error("Socket not connected, cannot receive."); - return 0; - } - logger_.info("Receiving up to {} bytes", max_num_bytes); - // now actually read data from the socket - int num_bytes_received = ::recv(socket_, data, max_num_bytes, 0); - // if we didn't receive anything return false and don't do anything else - if (num_bytes_received < 0) { - // if we got an error, log it and return 0 - logger_.debug("Receive failed: {} - '{}'", errno, strerror(errno)); - return 0; - } else if (num_bytes_received == 0) { - logger_.warn("Remote socket closed!"); - // update our connection state here since remote end was closed... - connected_ = false; - } else { - logger_.debug("Received {} bytes", num_bytes_received); - } - return num_bytes_received; - } + size_t receive(uint8_t *data, size_t max_num_bytes); /** * @brief Bind the socket as a server on \p port. * @param port The port to which to bind the socket. * @return true if the socket was bound. */ - bool bind(int port) { - if (!is_valid()) { - logger_.error("Socket invalid, cannot bind."); - return false; - } - struct sockaddr_in server_addr; - // configure the server socket accordingly - assume IPV4 and bind to the - // any address "0.0.0.0" - server_addr.sin_addr.s_addr = htonl(INADDR_ANY); - server_addr.sin_family = address_family_; - server_addr.sin_port = htons(port); - auto err = ::bind(socket_, (struct sockaddr *)&server_addr, sizeof(server_addr)); - if (err < 0) { - logger_.error("Unable to bind: {} - '{}'", errno, strerror(errno)); - return false; - } - return true; - } + bool bind(int port); /** * @brief Listen for incoming client connections. @@ -320,18 +190,7 @@ class TcpSocket : public Socket { * @param max_pending_connections Max number of allowed pending connections. * @return True if socket was able to start listening. */ - bool listen(int max_pending_connections) { - if (!is_valid()) { - logger_.error("Socket invalid, cannot listen."); - return false; - } - auto err = ::listen(socket_, max_pending_connections); - if (err < 0) { - logger_.error("Unable to listen: {} - '{}'", errno, strerror(errno)); - return false; - } - return true; - } + bool listen(int max_pending_connections); /** * @brief Accept an incoming connection. @@ -341,26 +200,7 @@ class TcpSocket : public Socket { * @return A unique pointer to a TcpClientSession if a connection was * accepted, nullptr otherwise. */ - std::unique_ptr accept() { - if (!is_valid()) { - logger_.error("Socket invalid, cannot accept incoming connections."); - return nullptr; - } - Socket::Info connected_client_info; - auto sender_address = connected_client_info.ipv4_ptr(); - socklen_t socklen = sizeof(*sender_address); - // accept connection - auto accepted_socket = ::accept(socket_, (struct sockaddr *)sender_address, &socklen); - if (accepted_socket < 0) { - logger_.error("Could not accept connection: {} - '{}'", errno, strerror(errno)); - return nullptr; - } - connected_client_info.update(); - logger_.info("Server accepted connection with {}", connected_client_info); - // NOTE: have to use new here because we can't use make_unique with a - // protected or private constructor - return std::unique_ptr(new TcpSocket(accepted_socket, connected_client_info)); - } + std::unique_ptr accept(); protected: /** @@ -372,57 +212,13 @@ class TcpSocket : public Socket { * @param socket_fd The socket file descriptor for the connection. * @param remote_info The remote endpoint info. */ - explicit TcpSocket(int socket_fd, const Socket::Info &remote_info) - : Socket(socket_fd, Logger::Config{.tag = "TcpSocket", .level = Logger::Verbosity::WARN}) - , remote_info_(remote_info) { - connected_ = true; - set_keepalive(); - } - - bool set_keepalive(std::chrono::seconds idle_time = std::chrono::seconds{60}, - std::chrono::seconds interval = std::chrono::seconds{10}, int max_probes = 5) { - if (!is_valid()) { - logger_.error("Socket invalid, cannot set keepalive."); - return false; - } - int optval = 1; - // enable keepalive - auto err = setsockopt(socket_, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)); - if (err < 0) { - logger_.error("Unable to set keepalive: {} - '{}'", errno, strerror(errno)); - return false; - } - -#if defined(__APPLE__) - // TODO: figure out how to set keepidle on macos -#else - // set the idle time - optval = idle_time.count(); - err = setsockopt(socket_, IPPROTO_TCP, TCP_KEEPIDLE, &optval, sizeof(optval)); - if (err < 0) { - logger_.error("Unable to set keepalive idle time: {} - '{}'", errno, strerror(errno)); - return false; - } -#endif + explicit TcpSocket(sock_type_t socket_fd, const espp::Socket::Info &remote_info); - // set the interval - optval = interval.count(); - err = setsockopt(socket_, IPPROTO_TCP, TCP_KEEPINTVL, &optval, sizeof(optval)); - if (err < 0) { - logger_.error("Unable to set keepalive interval: {} - '{}'", errno, strerror(errno)); - return false; - } - // set the max probes - optval = max_probes; - err = setsockopt(socket_, IPPROTO_TCP, TCP_KEEPCNT, &optval, sizeof(optval)); - if (err < 0) { - logger_.error("Unable to set keepalive max probes: {} - '{}'", errno, strerror(errno)); - return false; - } - return true; - } + bool set_keepalive(const std::chrono::seconds &idle_time = std::chrono::seconds{60}, + const std::chrono::seconds &interval = std::chrono::seconds{10}, + int max_probes = 5); bool connected_{false}; - Socket::Info remote_info_{}; + espp::Socket::Info remote_info_{}; }; } // namespace espp diff --git a/components/socket/include/udp_socket.hpp b/components/socket/include/udp_socket.hpp index 83a25cf42..09ead27a6 100644 --- a/components/socket/include/udp_socket.hpp +++ b/components/socket/include/udp_socket.hpp @@ -1,5 +1,7 @@ #pragma once +#include "socket_msvc.hpp" + #include #include #include @@ -44,7 +46,7 @@ class UdpSocket : public Socket { bool is_multicast_endpoint{false}; /**< Whether this should be a multicast endpoint. */ std::string multicast_group{ ""}; /**< If this is a multicast endpoint, this is the group it belongs to. */ - receive_callback_fn on_receive_callback{ + espp::Socket::receive_callback_fn on_receive_callback{ nullptr}; /**< Function containing business logic to handle data received. */ }; @@ -55,33 +57,28 @@ class UdpSocket : public Socket { bool wait_for_response{false}; /**< Whether to wait for a response from the remote or not. */ size_t response_size{ 0}; /**< If waiting for a response, this is the maximum size response we will receive. */ - response_callback_fn on_response_callback{ + espp::Socket::response_callback_fn on_response_callback{ nullptr}; /**< If waiting for a response, this is an optional handler which is provided the response data. */ - std::chrono::duration response_timeout{ - 0.5f}; /**< If waiting for a response, this is the maximum timeout to wait. */ + std::chrono::duration response_timeout = std::chrono::duration( + 0.5f); /**< If waiting for a response, this is the maximum timeout to wait. */ }; struct Config { - Logger::Verbosity log_level{ - Logger::Verbosity::WARN}; /**< Verbosity level for the UDP socket logger. */ + espp::Logger::Verbosity log_level{ + espp::Logger::Verbosity::WARN}; /**< Verbosity level for the UDP socket logger. */ }; /** * @brief Initialize the socket and associated resources. * @param config Config for the socket. */ - explicit UdpSocket(const Config &config) - : Socket(Type::DGRAM, Logger::Config{.tag = "UdpSocket", .level = config.log_level}) {} + explicit UdpSocket(const Config &config); /** * @brief Tear down any resources associted with the socket. */ - ~UdpSocket() { - // we have to explicitly call cleanup here so that the server recvfrom - // will return and the task can stop. - cleanup(); - } + ~UdpSocket(); /** * @brief Send data to the endpoint specified by the send_config. @@ -99,9 +96,7 @@ class UdpSocket : public Socket { * to wait for a response. * @return true if the data was sent, false otherwise. */ - bool send(const std::vector &data, const SendConfig &send_config) { - return send(std::string_view{(const char *)data.data(), data.size()}, send_config); - } + bool send(const std::vector &data, const SendConfig &send_config); /** * @brief Send data to the endpoint specified by the send_config. @@ -119,60 +114,7 @@ class UdpSocket : public Socket { * to wait for a response. * @return true if the data was sent, false otherwise. */ - bool send(std::string_view data, const SendConfig &send_config) { - if (!is_valid()) { - logger_.error("Socket invalid, cannot send"); - return false; - } - if (send_config.is_multicast_endpoint) { - // configure it for multicast - if (!make_multicast()) { - logger_.error("Cannot make multicast: {} - '{}'", errno, strerror(errno)); - return false; - } - } - // set the receive timeout - if (!set_receive_timeout(send_config.response_timeout)) { - logger_.error("Could not set receive timeout to {}: {} - '{}'", - send_config.response_timeout.count(), errno, strerror(errno)); - return false; - } - // sendto - Socket::Info server_info; - server_info.init_ipv4(send_config.ip_address, send_config.port); - auto server_address = server_info.ipv4_ptr(); - logger_.info("Client sending {} bytes to {}:{}", data.size(), send_config.ip_address, - send_config.port); - int num_bytes_sent = sendto(socket_, data.data(), data.size(), 0, - (struct sockaddr *)server_address, sizeof(*server_address)); - if (num_bytes_sent < 0) { - logger_.error("Error occurred during sending: {} - '{}'", errno, strerror(errno)); - return false; - } - logger_.debug("Client sent {} bytes", num_bytes_sent); - // we don't need to wait for a response and the socket is good; - if (!send_config.wait_for_response) { - return true; - } - if (send_config.response_size == 0) { - logger_.warn("Response requested, but response_size=0, not waiting for response!"); - // NOTE: we did send successfully, so we return true and warn about - // misconfiguration - return true; - } - std::vector received_data; - logger_.info("Client waiting for response"); - if (!receive(send_config.response_size, received_data, server_info)) { - logger_.warn("Client could not get response"); - return false; - } - logger_.info("Client got {} bytes of response", received_data.size()); - if (send_config.on_response_callback) { - logger_.debug("Client calling response callback"); - send_config.on_response_callback(received_data); - } - return true; - } + bool send(std::string_view data, const SendConfig &send_config); /** * @brief Call recvfrom on the socket, assuming it has already been @@ -184,33 +126,7 @@ class UdpSocket : public Socket { * will be populated with the information about the sender. * @return true if successfully received, false otherwise. */ - bool receive(size_t max_num_bytes, std::vector &data, Socket::Info &remote_info) { - if (!is_valid()) { - logger_.error("Socket invalid, cannot receive."); - return false; - } - // recvfrom - auto remote_address = remote_info.ipv4_ptr(); - socklen_t socklen = sizeof(*remote_address); - // put it on the heap so that our stack usage doesn't change depending on - // max_num_bytes - std::unique_ptr receive_buffer(new uint8_t[max_num_bytes]()); - // now actually receive - logger_.info("Receiving up to {} bytes", max_num_bytes); - int num_bytes_received = recvfrom(socket_, receive_buffer.get(), max_num_bytes, 0, - (struct sockaddr *)remote_address, &socklen); - // if we didn't receive anything return false and don't do anything else - if (num_bytes_received < 0) { - logger_.error("Receive failed: {} - '{}'", errno, strerror(errno)); - return false; - } - // we received data, so call the callback function if one was provided. - uint8_t *data_ptr = (uint8_t *)receive_buffer.get(); - data.assign(data_ptr, data_ptr + num_bytes_received); - remote_info.update(); - logger_.debug("Received {} bytes from {}", num_bytes_received, remote_info); - return true; - } + bool receive(size_t max_num_bytes, std::vector &data, Socket::Info &remote_info); /** * @brief Configure a server socket and start a thread to continuously @@ -220,50 +136,7 @@ class UdpSocket : public Socket { * @param receive_config ReceiveConfig struct with socket and callback info. * @return true if the socket was created and task was started, false otherwise. */ - bool start_receiving(Task::Config &task_config, const ReceiveConfig &receive_config) { - if (task_ && task_->is_started()) { - logger_.error("Server is alrady receiving"); - return false; - } - if (!is_valid()) { - logger_.error("Socket invalid, cannot start receiving."); - return false; - } - server_receive_callback_ = receive_config.on_receive_callback; - // bind - struct sockaddr_in server_addr; - // configure the server socket accordingly - assume IPV4 and bind to the - // any address "0.0.0.0" - server_addr.sin_addr.s_addr = htonl(INADDR_ANY); - server_addr.sin_family = address_family_; - server_addr.sin_port = htons(receive_config.port); - int err = bind(socket_, (struct sockaddr *)&server_addr, sizeof(server_addr)); - if (err < 0) { - logger_.error("Unable to bind: {} - '{}'", errno, strerror(errno)); - return false; - } - if (receive_config.is_multicast_endpoint) { - // enable multicast - if (!make_multicast()) { - logger_.error("Unable to make bound socket multicast: {} - '{}'", errno, strerror(errno)); - return false; - } - // add multicast group - if (!add_multicast_group(receive_config.multicast_group)) { - logger_.error("Unable to add multicast group to bound socket: {} - '{}'", errno, - strerror(errno)); - return false; - } - } - // set the callback function - using namespace std::placeholders; - task_config.callback = - std::bind(&UdpSocket::server_task_function, this, receive_config.buffer_size, _1, _2); - // start the thread - task_ = Task::make_unique(task_config); - task_->start(); - return true; - } + bool start_receiving(Task::Config &task_config, const ReceiveConfig &receive_config); protected: /** @@ -279,40 +152,7 @@ class UdpSocket : public Socket { * interruptible wait / delay. * @return Return true if the task should stop; false if it should continue. */ - bool server_task_function(size_t buffer_size, std::mutex &m, std::condition_variable &cv) { - // receive data - std::vector received_data; - Socket::Info sender_info; - if (!receive(buffer_size, received_data, sender_info)) { - // if we failed to receive, then likely we should delay a little bit - using namespace std::chrono_literals; - std::unique_lock lk(m); - cv.wait_for(lk, 1ms); - return false; - } - if (!server_receive_callback_) { - logger_.error("Server receive callback is invalid"); - return false; - } - // callback - auto maybe_response = server_receive_callback_(received_data, sender_info); - // send if callback returned data - if (!maybe_response.has_value()) { - return false; - } - auto response = maybe_response.value(); - // sendto - logger_.info("Server responding to {} with message of length {}", sender_info, response.size()); - auto sender_address = sender_info.ipv4_ptr(); - int num_bytes_sent = sendto(socket_, response.data(), response.size(), 0, - (struct sockaddr *)sender_address, sizeof(*sender_address)); - if (num_bytes_sent < 0) { - logger_.error("Error occurred responding: {} - '{}'", errno, strerror(errno)); - } - logger_.info("Server responded with {} bytes", num_bytes_sent); - // don't want to stop the task - return false; - } + bool server_task_function(size_t buffer_size, std::mutex &m, std::condition_variable &cv); std::unique_ptr task_; receive_callback_fn server_receive_callback_; diff --git a/components/socket/src/socket.cpp b/components/socket/src/socket.cpp new file mode 100644 index 000000000..e6265c0e1 --- /dev/null +++ b/components/socket/src/socket.cpp @@ -0,0 +1,349 @@ +#include "socket.hpp" + +using namespace espp; + +void Socket::Info::init_ipv4(const std::string &addr, size_t prt) { + address = addr; + port = prt; + auto server_address = ipv4_ptr(); + server_address->sin_family = AF_INET; + server_address->sin_addr.s_addr = inet_addr(address.c_str()); + server_address->sin_port = htons(port); +} + +struct sockaddr_in *Socket::Info::ipv4_ptr() { + return (struct sockaddr_in *)&raw; +} + +struct sockaddr_in6 *Socket::Info::ipv6_ptr() { + return (struct sockaddr_in6 *)&raw; +} + +void Socket::Info::update() { + if (raw.ss_family == PF_INET) { + address = inet_ntoa(((struct sockaddr_in *)&raw)->sin_addr); + port = ((struct sockaddr_in *)&raw)->sin_port; + } else if (raw.ss_family == PF_INET6) { +#if defined(ESP_PLATFORM) + address = inet_ntoa(((struct sockaddr_in6 *)&raw)->sin6_addr); +#else + char str[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &(((struct sockaddr_in6 *)&raw)->sin6_addr), str, INET6_ADDRSTRLEN); + address = str; +#endif + port = ((struct sockaddr_in6 *)&raw)->sin6_port; + } +} + +void Socket::Info::from_sockaddr(const struct sockaddr_storage &source_address) { + memcpy(&raw, &source_address, sizeof(source_address)); + update(); +} + +void Socket::Info::from_sockaddr(const struct sockaddr_in &source_address) { + memcpy(&raw, &source_address, sizeof(source_address)); + address = inet_ntoa(source_address.sin_addr); + port = source_address.sin_port; +} + +void Socket::Info::from_sockaddr(const struct sockaddr_in6 &source_address) { +#if defined(ESP_PLATFORM) + address = inet_ntoa(source_address.sin6_addr); +#else + char str[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &(source_address.sin6_addr), str, INET6_ADDRSTRLEN); + address = str; +#endif + port = source_address.sin6_port; + memcpy(&raw, &source_address, sizeof(source_address)); +} + +[[maybe_unused]] static bool _socket_initialized = false; +Socket::Socket(sock_type_t socket_fd, const Logger::Config &logger_config) + : BaseComponent(logger_config) { +#ifdef _MSC_VER + if (!_socket_initialized) { + logger_.debug("Initializing Winsock"); + WSADATA wsa_data; + int err = WSAStartup(MAKEWORD(1, 1), &wsa_data); + if (err != 0) { + logger_.error("WSAStartup failed: {}", error_string(err)); + } + _socket_initialized = true; + } +#endif + socket_ = socket_fd; +} + +Socket::Socket(Type type, const Logger::Config &logger_config) + : BaseComponent(logger_config) { +#ifdef _MSC_VER + if (!_socket_initialized) { + logger_.debug("Initializing Winsock"); + WSADATA wsa_data; + int err = WSAStartup(MAKEWORD(1, 1), &wsa_data); + if (err != 0) { + logger_.error("WSAStartup failed: {}", error_string(err)); + } + _socket_initialized = true; + } +#endif + init(type); +} + +Socket::~Socket() { cleanup(); } + +bool Socket::is_valid() const { +#ifdef _MSC_VER + return socket_ != INVALID_SOCKET; +#else + return socket_ >= 0; +#endif +} + +bool Socket::is_valid_fd(sock_type_t socket_fd) { +#ifdef _MSC_VER + return socket_fd != INVALID_SOCKET; +#else + return socket_fd >= 0; +#endif +} + +std::optional Socket::get_ipv4_info() { + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + if (getsockname(socket_, (struct sockaddr *)&addr, &addr_len) < 0) { + logger_.error("getsockname() failed: {}", error_string()); + return {}; + } + Info info; + info.from_sockaddr(addr); + return info; +} + +bool Socket::set_receive_timeout(const std::chrono::duration &timeout) { + float seconds = timeout.count(); + if (seconds <= 0) { + return true; + } + float intpart; + float fractpart = modf(seconds, &intpart); + const time_t response_timeout_s = (int)intpart; + const time_t response_timeout_us = (int)(fractpart * 1E6); + //// Alternatively we could do this: + // int microseconds = + // (int)(std::chrono::duration_cast(timeout).count()) % (int)1E6; + // const time_t response_timeout_s = floor(seconds); + // const time_t response_timeout_us = microseconds; + + struct timeval tv; + tv.tv_sec = response_timeout_s; + tv.tv_usec = response_timeout_us; + int err = setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv)); + if (err < 0) { + return false; + } + return true; +} + +bool Socket::enable_reuse() { +#if !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM) + fmt::print(fg(fmt::color::red), "CONFIG_LWIP_SO_REUSE not defined!\n"); + return false; +#else // CONFIG_LWIP_SO_REUSE || !defined(ESP_PLATFORM) + int err = 0; + int enabled = 1; + err = setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, (const char *)&enabled, sizeof(enabled)); + if (err < 0) { + fmt::print(fg(fmt::color::red), "Couldn't set SO_REUSEADDR: {}\n", error_string()); + return false; + } +#if !defined(ESP_PLATFORM) +#ifdef _MSC_VER + // NOTE: according to stackoverflow, we have to set broadcast instead of reuseport + err = setsockopt(socket_, SOL_SOCKET, SO_BROADCAST, (const char *)&enabled, sizeof(enabled)); + if (err < 0) { + fmt::print(fg(fmt::color::red), "Couldn't set SO_BROADCAST: {}\n", error_string()); + return false; + } +#else + err = setsockopt(socket_, SOL_SOCKET, SO_REUSEPORT, (const char *)&enabled, sizeof(enabled)); + if (err < 0) { + fmt::print(fg(fmt::color::red), "Couldn't set SO_REUSEPORT: {}\n", error_string()); + return false; + } +#endif // _MSC_VER +#endif // !defined(ESP_PLATFORM) + return true; +#endif // !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM) +} + +bool Socket::make_multicast(uint8_t time_to_live, uint8_t loopback_enabled) { + int err = 0; + // Assign multicast TTL - separate from normal interface TTL + err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_TTL, (const char *)&time_to_live, + sizeof(uint8_t)); + if (err < 0) { + fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_TTL: {}\n", error_string()); + return false; + } + // select whether multicast traffic should be received by this device, too + err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_LOOP, (const char *)&loopback_enabled, + sizeof(uint8_t)); + if (err < 0) { + fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_LOOP: {}\n", error_string()); + return false; + } + return true; +} + +bool Socket::add_multicast_group(const std::string &multicast_group) { + struct ip_mreq imreq; + int err = 0; + + // Configure source interface +#if defined(ESP_PLATFORM) + imreq.imr_interface.s_addr = IPADDR_ANY; + // Configure multicast address to listen to + err = inet_aton(multicast_group.c_str(), &imreq.imr_multiaddr.s_addr); +#else + imreq.imr_interface.s_addr = htonl(INADDR_ANY); + // Configure multicast address to listen to +#ifdef _MSC_VER + err = inet_pton(AF_INET, multicast_group.c_str(), &imreq.imr_multiaddr); +#else + err = inet_aton(multicast_group.c_str(), &imreq.imr_multiaddr); +#endif // _MSC_VER +#endif // defined(ESP_PLATFORM) + + if (err != 1 || !IN_MULTICAST(ntohl(imreq.imr_multiaddr.s_addr))) { + // it's not actually a multicast address, so return false? + fmt::print(fg(fmt::color::red), "Not a valid multicast address ({})\n", multicast_group); + return false; + } + + // Assign the IPv4 multicast source interface, via its IP + // (only necessary if this socket is IPV4 only) + struct in_addr iaddr; + err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_IF, (const char *)&iaddr, + sizeof(struct in_addr)); + if (err < 0) { + fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_IF: {}\n", error_string()); + return false; + } + + err = setsockopt(socket_, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char *)&imreq, + sizeof(struct ip_mreq)); + if (err < 0) { + fmt::print(fg(fmt::color::red), "Couldn't set IP_ADD_MEMBERSHIP: {}\n", error_string()); + return false; + } + + return true; +} + +int Socket::select(const std::chrono::microseconds &timeout) { + fd_set readfds; + fd_set writefds; + fd_set exceptfds; + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&exceptfds); + FD_SET(socket_, &readfds); + FD_SET(socket_, &writefds); + FD_SET(socket_, &exceptfds); + int nfds = socket_ + 1; + // convert timeout to timeval + struct timeval tv; + tv.tv_sec = std::chrono::duration_cast(timeout).count(); + tv.tv_usec = std::chrono::duration_cast(timeout).count() % 1000000; + int retval = ::select(nfds, &readfds, &writefds, &exceptfds, &tv); + if (retval < 0) { + logger_.error("select failed: {}", error_string()); + return -1; + } + if (retval == 0) { + logger_.warn("select timed out"); + return 0; + } + if (FD_ISSET(socket_, &readfds)) { + logger_.debug("select read"); + } + if (FD_ISSET(socket_, &writefds)) { + logger_.debug("select write"); + } + if (FD_ISSET(socket_, &exceptfds)) { + logger_.debug("select except"); + } + return retval; +} + +bool Socket::init(Socket::Type type) { + // actually make the socket + socket_ = socket(address_family_, (int)type, ip_protocol_); + if (!is_valid()) { + logger_.error("Cannot create socket: {}", error_string()); + return false; + } + if (!enable_reuse()) { + logger_.error("Cannot enable reuse: {}", error_string()); + return false; + } + return true; +} + +std::string Socket::error_string() const { +#ifdef _MSC_VER + int err = WSAGetLastError(); + return error_string(err); +#else + return error_string(errno); +#endif +} + +std::string Socket::error_string(int err) const { +#ifdef _MSC_VER + if (err == WSAEWOULDBLOCK) { + return "WSAEWOULDBLOCK"; + } else if (err == WSAECONNRESET) { + return "WSAECONNRESET"; + } else if (err == WSAECONNABORTED) { + return "WSAECONNABORTED"; + } else if (err == WSAECONNREFUSED) { + return "WSAECONNREFUSED"; + } else if (err == WSAETIMEDOUT) { + return "WSAETIMEDOUT"; + } else if (err = WSAEINTR) { + return "WSAEINTR"; + } else if (err == WSAENOTSOCK) { + return "WSAENOTSOCK"; + } else if (err == WSANOTINITIALISED) { + return "WSANOTINITIALISED"; + } else { + return fmt::format("Unknown error: {0} ({0:#x})", (int)err); + } +#else + return fmt::format("{} - '{}'", err, strerror(err)); +#endif +} + +void Socket::cleanup() { + if (is_valid()) { + int status = 0; +#ifdef _MSC_VER + status = shutdown(socket_, SD_BOTH); + if (status == 0) { + closesocket(socket_); + } + socket_ = INVALID_SOCKET; +#else + status = shutdown(socket_, SHUT_RDWR); + if (status == 0) { + close(socket_); + } + socket_ = -1; +#endif + + logger_.info("Closed socket"); + } +} diff --git a/components/socket/src/tcp_socket.cpp b/components/socket/src/tcp_socket.cpp new file mode 100644 index 000000000..73f871297 --- /dev/null +++ b/components/socket/src/tcp_socket.cpp @@ -0,0 +1,246 @@ +#include "tcp_socket.hpp" + +using namespace espp; + +TcpSocket::TcpSocket(const TcpSocket::Config &config) + : Socket(Type::STREAM, Logger::Config{.tag = "TcpSocket", .level = config.log_level}) { + set_keepalive(); +} + +TcpSocket::~TcpSocket() { + // we have to explicitly call cleanup here so that the server accept / + // read will return and the task can stop. + cleanup(); +} + +void TcpSocket::reinit() { + if (is_valid()) { + // cleanup our socket + cleanup(); + } + init(Type::STREAM); +} + +void TcpSocket::close() { ::close(socket_); } + +bool TcpSocket::is_connected() const { return connected_; } + +bool TcpSocket::connect(const TcpSocket::ConnectConfig &connect_config) { + if (!is_valid()) { + logger_.error("Socket invalid, cannot connect"); + return false; + } + Socket::Info server_info; + server_info.init_ipv4(connect_config.ip_address, connect_config.port); + auto server_address = server_info.ipv4_ptr(); + logger_.info("Client connecting to {}", server_info); + // connect + int error = ::connect(socket_, (struct sockaddr *)server_address, sizeof(*server_address)); + if (error != 0) { + logger_.error("Could not connect to the server: {}", error_string()); + return false; + } + connected_ = true; + return true; +} + +const Socket::Info &TcpSocket::get_remote_info() const { return remote_info_; } + +bool TcpSocket::transmit(const std::vector &data, const TransmitConfig &transmit_config) { + return transmit(std::string_view{(const char *)data.data(), data.size()}, transmit_config); +} + +bool TcpSocket::transmit(const std::vector &data, const TransmitConfig &transmit_config) { + return transmit(std::string_view{(const char *)data.data(), data.size()}, transmit_config); +} + +bool TcpSocket::transmit(std::string_view data, const TransmitConfig &transmit_config) { + if (!is_valid()) { + logger_.error("Socket invalid, cannot send"); + return false; + } + // set the receive timeout + if (!set_receive_timeout(transmit_config.response_timeout)) { + logger_.error("Could not set receive timeout to {}: {}", + transmit_config.response_timeout.count(), error_string()); + return false; + } + // write + logger_.info("Client sending {} bytes", data.size()); + int num_bytes_sent = write(socket_, data.data(), data.size()); + if (num_bytes_sent < 0) { + logger_.error("Error occurred during sending: {}", error_string()); + // update our connection state here since remote end was likely closed... + connected_ = false; + return false; + } + logger_.debug("Client sent {} bytes", num_bytes_sent); + // we don't need to wait for a response and the socket is good; + if (!transmit_config.wait_for_response) { + return true; + } + if (transmit_config.response_size == 0) { + logger_.warn("Response requested, but response_size=0, not waiting for response!"); + // NOTE: we did send successfully, so we return true and warn about + // misconfiguration + return true; + } + std::vector received_data; + logger_.info("Client waiting for response"); + // read + if (!receive(received_data, transmit_config.response_size)) { + logger_.warn("Client could not get response, remote socket might have closed!"); + // TODO: should we upate our connected_ variable here? + return false; + } + logger_.info("Client got {} bytes of response", received_data.size()); + if (transmit_config.on_response_callback) { + logger_.debug("Client calling response callback"); + transmit_config.on_response_callback(received_data); + } + return true; +} + +bool TcpSocket::receive(std::vector &data, size_t max_num_bytes) { + // make some space for received data - put it on the heap so that our + // stack usage doesn't change depending on max_num_bytes + std::unique_ptr receive_buffer(new uint8_t[max_num_bytes]()); + int num_bytes_received = receive(receive_buffer.get(), max_num_bytes); + if (num_bytes_received > 0) { + logger_.info("Received {} bytes", num_bytes_received); + uint8_t *data_ptr = (uint8_t *)receive_buffer.get(); + data.assign(data_ptr, data_ptr + num_bytes_received); + return true; + } + return false; +} + +size_t TcpSocket::receive(uint8_t *data, size_t max_num_bytes) { + if (!is_valid()) { + logger_.error("Socket invalid, cannot receive."); + return 0; + } + if (!is_connected()) { + logger_.error("Socket not connected, cannot receive."); + return 0; + } + logger_.info("Receiving up to {} bytes", max_num_bytes); + // now actually read data from the socket + int num_bytes_received = ::recv(socket_, (char *)data, max_num_bytes, 0); + // if we didn't receive anything return false and don't do anything else + if (num_bytes_received < 0) { + // if we got an error, log it and return 0 + logger_.debug("Receive failed: {}", error_string()); + return 0; + } else if (num_bytes_received == 0) { + logger_.warn("Remote socket closed!"); + // update our connection state here since remote end was closed... + connected_ = false; + } else { + logger_.debug("Received {} bytes", num_bytes_received); + } + return num_bytes_received; +} + +bool TcpSocket::bind(int port) { + if (!is_valid()) { + logger_.error("Socket invalid, cannot bind."); + return false; + } + struct sockaddr_in server_addr; + // configure the server socket accordingly - assume IPV4 and bind to the + // any address "0.0.0.0" + server_addr.sin_addr.s_addr = htonl(INADDR_ANY); + server_addr.sin_family = address_family_; + server_addr.sin_port = htons(port); + auto err = ::bind(socket_, (struct sockaddr *)&server_addr, sizeof(server_addr)); + if (err < 0) { + logger_.error("Unable to bind: {}", error_string()); + return false; + } + return true; +} + +bool TcpSocket::listen(int max_pending_connections) { + if (!is_valid()) { + logger_.error("Socket invalid, cannot listen."); + return false; + } + auto err = ::listen(socket_, max_pending_connections); + if (err < 0) { + logger_.error("Unable to listen: {}", error_string()); + return false; + } + return true; +} + +std::unique_ptr TcpSocket::accept() { + if (!is_valid()) { + logger_.error("Socket invalid, cannot accept incoming connections."); + return nullptr; + } + Socket::Info connected_client_info; + auto sender_address = connected_client_info.ipv4_ptr(); + socklen_t socklen = sizeof(*sender_address); + // accept connection + auto accepted_socket = ::accept(socket_, (struct sockaddr *)sender_address, &socklen); + if (accepted_socket < 0) { + logger_.error("Could not accept connection: {}", error_string()); + return nullptr; + } + connected_client_info.update(); + logger_.info("Server accepted connection with {}", connected_client_info); + // NOTE: have to use new here because we can't use make_unique with a + // protected or private constructor + return std::unique_ptr(new TcpSocket(accepted_socket, connected_client_info)); +} + +TcpSocket::TcpSocket(sock_type_t socket_fd, const Socket::Info &remote_info) + : Socket(socket_fd, Logger::Config{.tag = "TcpSocket", .level = Logger::Verbosity::WARN}) + , remote_info_(remote_info) { + connected_ = true; + set_keepalive(); +} + +bool TcpSocket::set_keepalive(const std::chrono::seconds &idle_time, + const std::chrono::seconds &interval, int max_probes) { + if (!is_valid()) { + logger_.error("Socket invalid, cannot set keepalive."); + return false; + } + int optval = 1; + // enable keepalive + auto err = setsockopt(socket_, SOL_SOCKET, SO_KEEPALIVE, (const char *)&optval, sizeof(optval)); + if (err < 0) { + logger_.error("Unable to set keepalive: {}", error_string()); + return false; + } + +#if defined(__APPLE__) + // TODO: figure out how to set keepidle on macos +#else + // set the idle time + optval = idle_time.count(); + err = setsockopt(socket_, IPPROTO_TCP, TCP_KEEPIDLE, (const char *)&optval, sizeof(optval)); + if (err < 0) { + logger_.error("Unable to set keepalive idle time: {}", error_string()); + return false; + } +#endif + + // set the interval + optval = interval.count(); + err = setsockopt(socket_, IPPROTO_TCP, TCP_KEEPINTVL, (const char *)&optval, sizeof(optval)); + if (err < 0) { + logger_.error("Unable to set keepalive interval: {}", error_string()); + return false; + } + // set the max probes + optval = max_probes; + err = setsockopt(socket_, IPPROTO_TCP, TCP_KEEPCNT, (const char *)&optval, sizeof(optval)); + if (err < 0) { + logger_.error("Unable to set keepalive max probes: {}", error_string()); + return false; + } + return true; +} diff --git a/components/socket/src/udp_socket.cpp b/components/socket/src/udp_socket.cpp new file mode 100644 index 000000000..73a149823 --- /dev/null +++ b/components/socket/src/udp_socket.cpp @@ -0,0 +1,183 @@ +#include "udp_socket.hpp" + +using namespace espp; + +UdpSocket::UdpSocket(const UdpSocket::Config &config) + : Socket(Type::DGRAM, Logger::Config{.tag = "UdpSocket", .level = config.log_level}) {} + +UdpSocket::~UdpSocket() { + // we have to explicitly call cleanup here so that the server recvfrom + // will return and the task can stop. + cleanup(); +} + +bool UdpSocket::send(const std::vector &data, const UdpSocket::SendConfig &send_config) { + return send(std::string_view{(const char *)data.data(), data.size()}, send_config); +} + +bool UdpSocket::send(std::string_view data, const UdpSocket::SendConfig &send_config) { + if (!is_valid()) { + logger_.error("Socket invalid, cannot send"); + return false; + } + if (send_config.is_multicast_endpoint) { + // configure it for multicast + if (!make_multicast()) { + logger_.error("Cannot make multicast: {}", error_string()); + return false; + } + } + if (send_config.wait_for_response) { + // set the receive timeout + if (!set_receive_timeout(send_config.response_timeout)) { + logger_.error("Could not set receive timeout to {}: {}", send_config.response_timeout.count(), + error_string()); + return false; + } + } + // sendto + Socket::Info server_info; + server_info.init_ipv4(send_config.ip_address, send_config.port); + auto server_address = server_info.ipv4_ptr(); + logger_.info("Client sending {} bytes to {}:{}", data.size(), send_config.ip_address, + send_config.port); + int num_bytes_sent = sendto(socket_, data.data(), data.size(), 0, + (struct sockaddr *)server_address, sizeof(*server_address)); + if (num_bytes_sent < 0) { + logger_.error("Error occurred during sending: {}", error_string()); + return false; + } + logger_.debug("Client sent {} bytes", num_bytes_sent); + // we don't need to wait for a response and the socket is good; + if (!send_config.wait_for_response) { + return true; + } + if (send_config.response_size == 0) { + logger_.warn("Response requested, but response_size=0, not waiting for response!"); + // NOTE: we did send successfully, so we return true and warn about + // misconfiguration + return true; + } + std::vector received_data; + logger_.info("Client waiting for response"); + if (!receive(send_config.response_size, received_data, server_info)) { + logger_.warn("Client could not get response"); + return false; + } + logger_.info("Client got {} bytes of response", received_data.size()); + if (send_config.on_response_callback) { + logger_.debug("Client calling response callback"); + send_config.on_response_callback(received_data); + } + return true; +} + +bool UdpSocket::receive(size_t max_num_bytes, std::vector &data, + Socket::Info &remote_info) { + if (!is_valid()) { + logger_.error("Socket invalid, cannot receive."); + return false; + } + // recvfrom + auto remote_address = remote_info.ipv4_ptr(); + socklen_t socklen = sizeof(*remote_address); + // put it on the heap so that our stack usage doesn't change depending on + // max_num_bytes + std::unique_ptr receive_buffer(new uint8_t[max_num_bytes]()); + // now actually receive + logger_.info("Receiving up to {} bytes", max_num_bytes); + int num_bytes_received = recvfrom(socket_, (char *)receive_buffer.get(), max_num_bytes, 0, + (struct sockaddr *)remote_address, &socklen); + // if we didn't receive anything return false and don't do anything else + if (num_bytes_received < 0) { + logger_.error("Receive failed: {}", error_string()); + return false; + } + // we received data, so call the callback function if one was provided. + uint8_t *data_ptr = (uint8_t *)receive_buffer.get(); + data.assign(data_ptr, data_ptr + num_bytes_received); + remote_info.update(); + logger_.debug("Received {} bytes from {}", num_bytes_received, remote_info); + return true; +} + +bool UdpSocket::start_receiving(Task::Config &task_config, + const UdpSocket::ReceiveConfig &receive_config) { + if (task_ && task_->is_started()) { + logger_.error("Server is alrady receiving"); + return false; + } + if (!is_valid()) { + logger_.error("Socket invalid, cannot start receiving."); + return false; + } + server_receive_callback_ = receive_config.on_receive_callback; + // bind + struct sockaddr_in server_addr; + // configure the server socket accordingly - assume IPV4 and bind to the + // any address "0.0.0.0" + server_addr.sin_addr.s_addr = htonl(INADDR_ANY); + server_addr.sin_family = address_family_; + server_addr.sin_port = htons(receive_config.port); + int err = bind(socket_, (struct sockaddr *)&server_addr, sizeof(server_addr)); + if (err < 0) { + logger_.error("Unable to bind: {}", error_string()); + return false; + } + if (receive_config.is_multicast_endpoint) { + // enable multicast + if (!make_multicast()) { + logger_.error("Unable to make bound socket multicast: {}", error_string()); + return false; + } + // add multicast group + if (!add_multicast_group(receive_config.multicast_group)) { + logger_.error("Unable to add multicast group to bound socket: {}", error_string()); + return false; + } + } + // set the callback function + using namespace std::placeholders; + task_config.callback = + std::bind(&UdpSocket::server_task_function, this, receive_config.buffer_size, _1, _2); + // start the thread + task_ = Task::make_unique(task_config); + task_->start(); + return true; +} + +bool UdpSocket::server_task_function(size_t buffer_size, std::mutex &m, + std::condition_variable &cv) { + // receive data + std::vector received_data; + Socket::Info sender_info; + if (!receive(buffer_size, received_data, sender_info)) { + // if we failed to receive, then likely we should delay a little bit + using namespace std::chrono_literals; + std::unique_lock lk(m); + cv.wait_for(lk, 1ms); + return false; + } + if (!server_receive_callback_) { + logger_.error("Server receive callback is invalid"); + return false; + } + // callback + auto maybe_response = server_receive_callback_(received_data, sender_info); + // send if callback returned data + if (!maybe_response.has_value()) { + return false; + } + auto response = maybe_response.value(); + // sendto + logger_.info("Server responding to {} with message of length {}", sender_info, response.size()); + auto sender_address = sender_info.ipv4_ptr(); + int num_bytes_sent = sendto(socket_, (const char *)response.data(), response.size(), 0, + (struct sockaddr *)sender_address, sizeof(*sender_address)); + if (num_bytes_sent < 0) { + logger_.error("Error occurred responding: {}", error_string()); + } + logger_.info("Server responded with {} bytes", num_bytes_sent); + // don't want to stop the task + return false; +} diff --git a/components/state_machine/CMakeLists.txt b/components/state_machine/CMakeLists.txt index 8e1f31853..d2e8b4e58 100644 --- a/components/state_machine/CMakeLists.txt +++ b/components/state_machine/CMakeLists.txt @@ -1,3 +1,4 @@ idf_component_register( INCLUDE_DIRS "../../external/magic_enum/include/magic_enum" "include" + SRC_DIRS "src" ) diff --git a/components/state_machine/include/deep_history_state.hpp b/components/state_machine/include/deep_history_state.hpp index 790c29b1c..d95afad45 100644 --- a/components/state_machine/include/deep_history_state.hpp +++ b/components/state_machine/include/deep_history_state.hpp @@ -2,7 +2,8 @@ #include "state_base.hpp" -namespace espp::state_machine { +namespace espp { +namespace state_machine { /** * @brief Deep History Pseudostates exist purely to re-implement the @@ -33,4 +34,5 @@ class DeepHistoryState : public StateBase { } } }; -} // namespace espp::state_machine +} // namespace state_machine +} // namespace espp diff --git a/components/state_machine/include/shallow_history_state.hpp b/components/state_machine/include/shallow_history_state.hpp index ca3f0d6ba..172e54575 100644 --- a/components/state_machine/include/shallow_history_state.hpp +++ b/components/state_machine/include/shallow_history_state.hpp @@ -2,7 +2,8 @@ #include "state_base.hpp" -namespace espp::state_machine { +namespace espp { +namespace state_machine { /** * @brief Shallow History Pseudostates exist purely to re-implement @@ -33,4 +34,5 @@ class ShallowHistoryState : public StateBase { } } }; -} // namespace espp::state_machine +} // namespace state_machine +} // namespace espp diff --git a/components/state_machine/include/state_base.hpp b/components/state_machine/include/state_base.hpp index 9778164ec..2253ab7dd 100644 --- a/components/state_machine/include/state_base.hpp +++ b/components/state_machine/include/state_base.hpp @@ -1,7 +1,8 @@ #pragma once #include -namespace espp::state_machine { +namespace espp { +namespace state_machine { // Base Class for Events, abstract so you never instantiate. class EventBase { @@ -31,60 +32,54 @@ class StateBase { /** * @brief Default constructor */ - StateBase() - : _activeState(this) - , _parentState(nullptr) {} + StateBase(); /** * @brief Constructor that sets the parent state. * @param[in] parent Pointer to parent state */ - explicit StateBase(StateBase *parent) - : _activeState(this) - , _parentState(parent) {} + explicit StateBase(StateBase *parent); /** * @brief Destructor */ - virtual ~StateBase(void) {} + virtual ~StateBase(void); /** * @brief Will be generated to call entry() then handle any child * initialization. Finally calls makeActive on the leaf. */ - virtual void initialize(void){}; + virtual void initialize(void); /** * @brief Will be generated to run the entry() function defined in * the model. */ - virtual void entry(void){}; + virtual void entry(void); /** * @brief Will be generated to run the exit() function defined in * the model. */ - virtual void exit(void){}; + virtual void exit(void); /** * @brief Calls handleEvent on the activeLeaf. * @param[in] event Event needing to be handled * @return true if event is consumed, false otherwise */ - virtual bool handleEvent(EventBase *event) { return false; } + virtual bool handleEvent(EventBase *event); /** * @brief Will be generated to run the tick() function defined in * the model and then call _activeState->tick(). */ - virtual void tick(void) { - if (_activeState != this && _activeState != nullptr) - _activeState->tick(); - }; + virtual void tick(void); /** + * @brief Returns the timer period for the state. */ - virtual double getTimerPeriod(void) { return 0; } + virtual double getTimerPeriod(void); /** * @brief Will be known from the model so will be generated in @@ -93,91 +88,62 @@ class StateBase { * during external transition handling. * @return Pointer to initial substate */ - virtual StateBase *getInitial(void) { return this; }; + virtual StateBase *getInitial(void); /** * @brief Recurses down to the leaf state and calls the exit * actions as it unwinds. */ - void exitChildren(void) { - if (_activeState != nullptr && _activeState != this) { - _activeState->exitChildren(); - _activeState->exit(); - } - } + void exitChildren(void); /** * @brief Will return _activeState if it exists, otherwise will * return nullptr. * @return Pointer to last active substate */ - StateBase *getActiveChild(void) { return _activeState; } + StateBase *getActiveChild(void); /** * @brief Will return the active leaf state, otherwise will return * nullptr. * @return Pointer to last active leaf state. */ - StateBase *getActiveLeaf(void) { - if (_activeState != nullptr && _activeState != this) - return _activeState->getActiveLeaf(); - else - return this; - } + StateBase *getActiveLeaf(void); /** * @brief Make this state the active substate of its parent and * then recurse up through the tree to the root. * @note Should only be called on leaf nodes! */ - virtual void makeActive(void) { - if (_parentState != nullptr) { - _parentState->setActiveChild(this); - _parentState->makeActive(); - } - } + virtual void makeActive(void); /** * @brief Update the active child state. */ - void setActiveChild(StateBase *childState) { _activeState = childState; } + void setActiveChild(StateBase *childState); /** * @brief Sets the currentlyActive state to the last active state * and re-initializes them. */ - void setShallowHistory(void) { - if (_activeState != nullptr && _activeState != this) { - _activeState->entry(); - _activeState->initialize(); - } else { - initialize(); - } - } + void setShallowHistory(void); /** * @brief Go to the last active leaf of this state. If none * exists, re-initialize. */ - void setDeepHistory(void) { - if (_activeState != nullptr && _activeState != this) { - _activeState->entry(); - _activeState->setDeepHistory(); - } else { - initialize(); - } - } + void setDeepHistory(void); /** * @brief Will set the parent state. * @param[in] parent Pointer to parent state */ - void setParentState(StateBase *parent) { _parentState = parent; } + void setParentState(StateBase *parent); /** * @brief Will return the parent state. */ - StateBase *getParentState(void) { return _parentState; } + StateBase *getParentState(void); // Pointer to the currently or most recently active substate of this // state. @@ -187,4 +153,5 @@ class StateBase { StateBase *_parentState; }; // class StateBase -} // namespace espp::state_machine +} // namespace state_machine +} // namespace espp diff --git a/components/state_machine/src/state_base.cpp b/components/state_machine/src/state_base.cpp new file mode 100644 index 000000000..6e8760b9b --- /dev/null +++ b/components/state_machine/src/state_base.cpp @@ -0,0 +1,77 @@ +#include "state_base.hpp" + +using namespace espp::state_machine; + +StateBase::StateBase() + : _activeState(this) + , _parentState(nullptr) {} + +StateBase::StateBase(StateBase *parent) + : _activeState(this) + , _parentState(parent) {} + +StateBase::~StateBase(void) {} + +void StateBase::initialize(void){}; + +void StateBase::entry(void){}; + +void StateBase::exit(void){}; + +bool StateBase::handleEvent(EventBase *event) { return false; } + +void StateBase::tick(void) { + if (_activeState != this && _activeState != nullptr) + _activeState->tick(); +}; + +double StateBase::getTimerPeriod(void) { return 0; } + +StateBase *StateBase::getInitial(void) { return this; }; + +void StateBase::exitChildren(void) { + if (_activeState != nullptr && _activeState != this) { + _activeState->exitChildren(); + _activeState->exit(); + } +} + +StateBase *StateBase::getActiveChild(void) { return _activeState; } + +StateBase *StateBase::getActiveLeaf(void) { + if (_activeState != nullptr && _activeState != this) + return _activeState->getActiveLeaf(); + else + return this; +} + +void StateBase::makeActive(void) { + if (_parentState != nullptr) { + _parentState->setActiveChild(this); + _parentState->makeActive(); + } +} + +void StateBase::setActiveChild(StateBase *childState) { _activeState = childState; } + +void StateBase::setShallowHistory(void) { + if (_activeState != nullptr && _activeState != this) { + _activeState->entry(); + _activeState->initialize(); + } else { + initialize(); + } +} + +void StateBase::setDeepHistory(void) { + if (_activeState != nullptr && _activeState != this) { + _activeState->entry(); + _activeState->setDeepHistory(); + } else { + initialize(); + } +} + +void StateBase::setParentState(StateBase *parent) { _parentState = parent; } + +StateBase *StateBase::getParentState(void) { return _parentState; } diff --git a/components/task/CMakeLists.txt b/components/task/CMakeLists.txt index 0d74279ec..83e99e484 100644 --- a/components/task/CMakeLists.txt +++ b/components/task/CMakeLists.txt @@ -1,3 +1,4 @@ idf_component_register( INCLUDE_DIRS "include" + SRC_DIRS "src" REQUIRES base_component pthread) diff --git a/components/task/example/main/task_example.cpp b/components/task/example/main/task_example.cpp index b3b132464..5986b93f9 100644 --- a/components/task/example/main/task_example.cpp +++ b/components/task/example/main/task_example.cpp @@ -1,6 +1,7 @@ #include #include +#include "run_on_core.hpp" #include "task.hpp" using namespace std::chrono_literals; @@ -342,8 +343,8 @@ extern "C" void app_main(void) { // test running a function that returns void on a specific core auto task_fn = []() -> void { fmt::print("Void Task running on core {}\n", xPortGetCoreID()); }; - espp::Task::run_on_core(task_fn, 0, 3 * 1024); - espp::Task::run_on_core(task_fn, 1, 3 * 1024); + espp::task::run_on_core(task_fn, 0, 3 * 1024); + espp::task::run_on_core(task_fn, 1, 3 * 1024); fmt::print("Void Function returned\n"); // test running a function that returns bool on a specific core @@ -352,9 +353,9 @@ extern "C" void app_main(void) { fmt::print("Bool Task running on core {}\n", core_id); return core_id == 1; }; - auto result0 = espp::Task::run_on_core(task_fn2, 0, 3 * 1024); + auto result0 = espp::task::run_on_core(task_fn2, 0, 3 * 1024); fmt::print("Bool Function returned {}\n", result0); - auto result1 = espp::Task::run_on_core(task_fn2, 1, 3 * 1024); + auto result1 = espp::task::run_on_core(task_fn2, 1, 3 * 1024); fmt::print("Bool Function returned {}\n", result1); // test running a function that returns esp_err_t on a specific core @@ -363,9 +364,9 @@ extern "C" void app_main(void) { fmt::print("esp_err_t Task running on core {}\n", core_id); return core_id == 1 ? ESP_OK : ESP_FAIL; }; - auto err0 = espp::Task::run_on_core(task_fn3, 0, 3 * 1024); + auto err0 = espp::task::run_on_core(task_fn3, 0, 3 * 1024); fmt::print("esp_err_t Function returned {}\n", esp_err_to_name(err0)); - auto err1 = espp::Task::run_on_core(task_fn3, 1, 3 * 1024); + auto err1 = espp::task::run_on_core(task_fn3, 1, 3 * 1024); fmt::print("esp_err_t Function returned {}\n", esp_err_to_name(err1)); //! [run on core example] } diff --git a/components/task/include/run_on_core.hpp b/components/task/include/run_on_core.hpp new file mode 100644 index 000000000..9f67cfa23 --- /dev/null +++ b/components/task/include/run_on_core.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include "task.hpp" + +namespace espp { +namespace task { +#if defined(ESP_PLATFORM) || defined(_DOXYGEN_) +/// Run the given function on the specific core, then return the result (if any) +/// @details This function will run the given function on the specified core, +/// then return the result (if any). If the provided core is the same +/// as the current core, the function will run directly. If the +/// provided core is different, the function will be run on the +/// specified core and the result will be returned to the calling +/// thread. Note that this function will block the calling thread until +/// the function has completed, regardless of the core it is run on. +/// @param f The function to run +/// @param core_id The core to run the function on +/// @param stack_size_bytes The stack size to allocate for the function +/// @param priority The priority of the task +/// @note This function is only available on ESP32 +/// @note If you provide a core_id < 0, the function will run on the current +/// core (same core as the caller) +/// @note If you provide a core_id >= configNUM_CORES, the function will run on +/// the last core +static auto run_on_core(const auto &f, int core_id, size_t stack_size_bytes = 2048, + size_t priority = 5) { + if (core_id < 0 || core_id == xPortGetCoreID()) { + // If no core id specified or we are already executing on the desired core, + // run the function directly + return f(); + } else { + // Otherwise run the function on the desired core + if (core_id > configNUM_CORES - 1) { + // If the core id is larger than the number of cores, run on the last core + core_id = configNUM_CORES - 1; + } + std::mutex mutex; + std::unique_lock lock(mutex); // cppcheck-suppress localMutex + std::condition_variable cv; ///< Signal for when the task is done / function is run + if constexpr (!std::is_void_v) { + // the function returns something + decltype(f()) ret_val; + auto f_task = espp::Task::make_unique(espp::Task::Config{ + .name = "run_on_core_task", + .callback = [&mutex, &cv, &f, &ret_val](auto &cb_m, auto &cb_cv) -> bool { + // synchronize with the main thread - block here until the main thread + // waits on the condition variable (cv), then run the function + std::unique_lock lock(mutex); + // run the function + ret_val = f(); + // signal that the task is done + cv.notify_all(); + return true; // stop the task + }, + .stack_size_bytes = stack_size_bytes, + .priority = priority, + .core_id = core_id, + }); + f_task->start(); + cv.wait(lock); + return ret_val; + } else { + // the function returns void + auto f_task = espp::Task::make_unique(espp::Task::Config{ + .name = "run_on_core_task", + .callback = [&mutex, &cv, &f](auto &cb_m, auto &cb_cv) -> bool { + // synchronize with the main thread - block here until the main thread + // waits on the condition variable (cv), then run the function + std::unique_lock lock(mutex); + // run the function + f(); + // signal that the task is done + cv.notify_all(); + return true; // stop the task + }, + .stack_size_bytes = stack_size_bytes, + .priority = priority, + .core_id = core_id, + }); + f_task->start(); + cv.wait(lock); + } + } +} +#endif +} // namespace task +} // namespace espp diff --git a/components/task/include/task.hpp b/components/task/include/task.hpp index 8b38d05aa..96863b119 100644 --- a/components/task/include/task.hpp +++ b/components/task/include/task.hpp @@ -42,7 +42,7 @@ namespace espp { * \section run_on_core_ex1 Run on Core Example * \snippet task_example.cpp run on core example */ -class Task : public BaseComponent { +class Task : public espp::BaseComponent { public: /** * @brief Task callback function signature. @@ -86,8 +86,8 @@ class Task : public BaseComponent { * that may have a Task as a member. */ struct BaseConfig { - std::string name; /**< Name of the task */ - size_t stack_size_bytes{4 * 1024}; /**< Stack Size (B) allocated to the task. */ + std::string name; /**< Name of the task */ + size_t stack_size_bytes{4096}; /**< Stack Size (B) allocated to the task. */ size_t priority{0}; /**< Priority of the task, 0 is lowest priority on ESP / FreeRTOS. */ int core_id{-1}; /**< Core ID of the task, -1 means it is not pinned to any core. */ }; @@ -102,12 +102,13 @@ class Task : public BaseComponent { * instead. */ struct Config { - std::string name; /**< Name of the task */ - callback_fn callback; /**< Callback function */ - size_t stack_size_bytes{4 * 1024}; /**< Stack Size (B) allocated to the task. */ + std::string name; /**< Name of the task */ + espp::Task::callback_fn callback; /**< Callback function */ + size_t stack_size_bytes{4096}; /**< Stack Size (B) allocated to the task. */ size_t priority{0}; /**< Priority of the task, 0 is lowest priority on ESP / FreeRTOS. */ int core_id{-1}; /**< Core ID of the task, -1 means it is not pinned to any core. */ - Logger::Verbosity log_level{Logger::Verbosity::WARN}; /**< Log verbosity for the task. */ + espp::Logger::Verbosity log_level{ + espp::Logger::Verbosity::WARN}; /**< Log verbosity for the task. */ }; /** @@ -116,9 +117,10 @@ class Task : public BaseComponent { * or mutex in the callback. */ struct SimpleConfig { - simple_callback_fn callback; /**< Callback function */ - BaseConfig task_config; /**< Base configuration for the task. */ - Logger::Verbosity log_level{Logger::Verbosity::WARN}; /**< Log verbosity for the task. */ + espp::Task::simple_callback_fn callback; /**< Callback function */ + espp::Task::BaseConfig task_config; /**< Base configuration for the task. */ + espp::Logger::Verbosity log_level{ + espp::Logger::Verbosity::WARN}; /**< Log verbosity for the task. */ }; /** @@ -128,40 +130,29 @@ class Task : public BaseComponent { * wait_until. */ struct AdvancedConfig { - callback_fn callback; /**< Callback function */ - BaseConfig task_config; /**< Base configuration for the task. */ - Logger::Verbosity log_level{Logger::Verbosity::WARN}; /**< Log verbosity for the task. */ + espp::Task::callback_fn callback; /**< Callback function */ + espp::Task::BaseConfig task_config; /**< Base configuration for the task. */ + espp::Logger::Verbosity log_level{ + espp::Logger::Verbosity::WARN}; /**< Log verbosity for the task. */ }; /** * @brief Construct a new Task object using the Config struct. * @param config Config struct to initialize the Task with. */ - explicit Task(const Config &config) - : BaseComponent(config.name, config.log_level) - , name_(config.name) - , callback_(config.callback) - , config_({config.name, config.stack_size_bytes, config.priority, config.core_id}) {} + explicit Task(const espp::Task::Config &config); /** * @brief Construct a new Task object using the SimpleConfig struct. * @param config SimpleConfig struct to initialize the Task with. */ - explicit Task(const SimpleConfig &config) - : BaseComponent(config.task_config.name, config.log_level) - , name_(config.task_config.name) - , simple_callback_(config.callback) - , config_(config.task_config) {} + explicit Task(const espp::Task::SimpleConfig &config); /** * @brief Construct a new Task object using the AdvancedConfig struct. * @param config AdvancedConfig struct to initialize the Task with. */ - explicit Task(const AdvancedConfig &config) - : BaseComponent(config.task_config.name, config.log_level) - , name_(config.task_config.name) - , callback_(config.callback) - , config_(config.task_config) {} + explicit Task(const espp::Task::AdvancedConfig &config); /** * @brief Get a unique pointer to a new task created with \p config. @@ -169,9 +160,7 @@ class Task : public BaseComponent { * @param config Config struct to initialize the Task with. * @return std::unique_ptr pointer to the newly created task. */ - static std::unique_ptr make_unique(const Config &config) { - return std::make_unique(config); - } + static std::unique_ptr make_unique(const espp::Task::Config &config); /** * @brief Get a unique pointer to a new task created with \p config. @@ -179,9 +168,7 @@ class Task : public BaseComponent { * @param config SimpleConfig struct to initialize the Task with. * @return std::unique_ptr pointer to the newly created task. */ - static std::unique_ptr make_unique(const SimpleConfig &config) { - return std::make_unique(config); - } + static std::unique_ptr make_unique(const espp::Task::SimpleConfig &config); /** * @brief Get a unique pointer to a new task created with \p config. @@ -189,80 +176,19 @@ class Task : public BaseComponent { * @param config AdvancedConfig struct to initialize the Task with. * @return std::unique_ptr pointer to the newly created task. */ - static std::unique_ptr make_unique(const AdvancedConfig &config) { - return std::make_unique(config); - } + static std::unique_ptr make_unique(const espp::Task::AdvancedConfig &config); /** * @brief Destroy the task, stopping it if it was started. */ - ~Task() { - logger_.debug("Destroying task"); - // stop the task if it was started - if (started_) { - stop(); - } - // ensure we stop the thread if it's still around - if (thread_.joinable()) { - thread_.join(); - } - logger_.debug("Task destroyed"); - } + ~Task(); /** * @brief Start executing the task. * * @return true if the task started, false if it was already started. */ - bool start() { - logger_.debug("Starting task"); - if (started_) { - logger_.warn("Task already started!"); - return false; - } - -#if defined(ESP_PLATFORM) - auto thread_config = esp_pthread_get_default_config(); - thread_config.thread_name = name_.c_str(); - auto core_id = config_.core_id; - if (core_id >= 0) - thread_config.pin_to_core = core_id; - if (core_id >= portNUM_PROCESSORS) { - logger_.error("core_id ({}) is larger than portNUM_PROCESSORS ({}), cannot create Task '{}'", - core_id, portNUM_PROCESSORS, name_); - return false; - } - thread_config.stack_size = config_.stack_size_bytes; - thread_config.prio = config_.priority; - // this will set the config for the next created thread - auto err = esp_pthread_set_cfg(&thread_config); - if (err == ESP_ERR_NO_MEM) { - logger_.error("Out of memory, cannot create Task '{}'", name_); - return false; - } - if (err == ESP_ERR_INVALID_ARG) { - // see - // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/pthread.html?highlight=esp_pthread_set_cfg#_CPPv419esp_pthread_set_cfgPK17esp_pthread_cfg_t - logger_.error( - "Configured stack size ({}) is less than PTHREAD_STACK_MIN ({}), cannot create Task '{}'", - config_.stack_size_bytes, PTHREAD_STACK_MIN, name_); - return false; - } -#endif - - if (thread_.joinable()) { - thread_.join(); - } - - // set the atomic so that when the thread starts it won't immediately - // exit. - started_ = true; - // create and start the std::thread - thread_ = std::thread(&Task::thread_function, this); - - logger_.debug("Task started"); - return true; - } + bool start(); /** * @brief Stop the task execution, blocking until it stops. @@ -270,32 +196,21 @@ class Task : public BaseComponent { * @return true if the task stopped, false if it was not started / already * stopped. */ - bool stop() { - logger_.debug("Stopping task"); - if (started_) { - started_ = false; - cv_.notify_all(); - if (thread_.joinable()) { - thread_.join(); - } - } - logger_.debug("Task stopped"); - return true; - } + bool stop(); /** * @brief Has the task been started or not? * * @return true if the task is started / running, false otherwise. */ - bool is_started() const { return started_; } + bool is_started() const; /** * @brief Is the task running? * * @return true if the task is running, false otherwise. */ - bool is_running() const { return is_started(); } + bool is_running() const; #if defined(ESP_PLATFORM) || defined(_DOXYGEN_) /** @@ -304,10 +219,7 @@ class Task : public BaseComponent { * water mark (B) * @note This function is only available on ESP32 */ - static std::string get_info() { - return fmt::format("[T] '{}',{},{},{}\n", pcTaskGetName(nullptr), xPortGetCoreID(), - uxTaskPriorityGet(nullptr), uxTaskGetStackHighWaterMark(nullptr)); - } + static std::string get_info(); /** * @brief Get the info (as a string) for the provided \p task. @@ -316,117 +228,11 @@ class Task : public BaseComponent { * water mark (B) * @note This function is only available on ESP32 */ - static std::string get_info(const Task &task) { - TaskHandle_t freertos_handle = xTaskGetHandle(task.name_.c_str()); - return fmt::format("[T] '{}',{},{},{}\n", pcTaskGetName(freertos_handle), xPortGetCoreID(), - uxTaskPriorityGet(freertos_handle), - uxTaskGetStackHighWaterMark(freertos_handle)); - } - - /// Run the given function on the specific core, then return the result (if any) - /// @details This function will run the given function on the specified core, - /// then return the result (if any). If the provided core is the same - /// as the current core, the function will run directly. If the - /// provided core is different, the function will be run on the - /// specified core and the result will be returned to the calling - /// thread. Note that this function will block the calling thread until - /// the function has completed, regardless of the core it is run on. - /// @param f The function to run - /// @param core_id The core to run the function on - /// @param stack_size_bytes The stack size to allocate for the function - /// @param priority The priority of the task - /// @note This function is only available on ESP32 - /// @note If you provide a core_id < 0, the function will run on the current - /// core (same core as the caller) - /// @note If you provide a core_id >= configNUM_CORES, the function will run on - /// the last core - static auto run_on_core(const auto &f, int core_id, size_t stack_size_bytes = 2048, - size_t priority = 5) { - if (core_id < 0 || core_id == xPortGetCoreID()) { - // If no core id specified or we are already executing on the desired core, - // run the function directly - return f(); - } else { - // Otherwise run the function on the desired core - if (core_id > configNUM_CORES - 1) { - // If the core id is larger than the number of cores, run on the last core - core_id = configNUM_CORES - 1; - } - std::mutex mutex; - std::unique_lock lock(mutex); // cppcheck-suppress localMutex - std::condition_variable cv; ///< Signal for when the task is done / function is run - if constexpr (!std::is_void_v) { - // the function returns something - decltype(f()) ret_val; - auto f_task = espp::Task::make_unique(espp::Task::Config{ - .name = "run_on_core_task", - .callback = [&mutex, &cv, &f, &ret_val](auto &cb_m, auto &cb_cv) -> bool { - // synchronize with the main thread - block here until the main thread - // waits on the condition variable (cv), then run the function - std::unique_lock lock(mutex); - // run the function - ret_val = f(); - // signal that the task is done - cv.notify_all(); - return true; // stop the task - }, - .stack_size_bytes = stack_size_bytes, - .priority = priority, - .core_id = core_id, - }); - f_task->start(); - cv.wait(lock); - return ret_val; - } else { - // the function returns void - auto f_task = espp::Task::make_unique(espp::Task::Config{ - .name = "run_on_core_task", - .callback = [&mutex, &cv, &f](auto &cb_m, auto &cb_cv) -> bool { - // synchronize with the main thread - block here until the main thread - // waits on the condition variable (cv), then run the function - std::unique_lock lock(mutex); - // run the function - f(); - // signal that the task is done - cv.notify_all(); - return true; // stop the task - }, - .stack_size_bytes = stack_size_bytes, - .priority = priority, - .core_id = core_id, - }); - f_task->start(); - cv.wait(lock); - } - } - } + static std::string get_info(const Task &task); #endif protected: - void thread_function() { - while (started_) { - if (callback_) { - bool should_stop = callback_(cv_m_, cv_); - if (should_stop) { - // callback returned true, so stop running the thread function - logger_.debug("Callback requested stop, thread_function exiting"); - started_ = false; - break; - } - } else if (simple_callback_) { - bool should_stop = simple_callback_(); - if (should_stop) { - // callback returned true, so stop running the thread function - logger_.debug("Callback requested stop, thread_function exiting"); - started_ = false; - break; - } - } else { - started_ = false; - break; - } - } - } + void thread_function(); /** * @brief Name of the task (used in logs and taks monitoring). @@ -457,27 +263,4 @@ class Task : public BaseComponent { }; } // namespace espp -// for printing of BaseConfig using libfmt -template <> struct fmt::formatter { - constexpr auto parse(format_parse_context &ctx) const { return ctx.begin(); } - - template - auto format(const espp::Task::BaseConfig &config, FormatContext &ctx) const { - return fmt::format_to( - ctx.out(), - "Task::BaseConfig{{name: '{}', stack_size_bytes: {}, priority: {}, core_id: {}}}", - config.name, config.stack_size_bytes, config.priority, config.core_id); - } -}; - -// for printing of Task::Config using libfmt -template <> struct fmt::formatter { - constexpr auto parse(format_parse_context &ctx) const { return ctx.begin(); } - - template - auto format(const espp::Task::Config &config, FormatContext &ctx) const { - return fmt::format_to( - ctx.out(), "Task::Config{{name: '{}', stack_size_bytes: {}, priority: {}, core_id: {}}}", - config.name, config.stack_size_bytes, config.priority, config.core_id); - } -}; +#include "task_formatters.hpp" diff --git a/components/task/include/task_formatters.hpp b/components/task/include/task_formatters.hpp new file mode 100644 index 000000000..bc6f6426a --- /dev/null +++ b/components/task/include/task_formatters.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "format.hpp" + +// for printing of BaseConfig using libfmt +template <> struct fmt::formatter { + constexpr auto parse(format_parse_context &ctx) const { return ctx.begin(); } + + template + auto format(const espp::Task::BaseConfig &config, FormatContext &ctx) const { + return fmt::format_to( + ctx.out(), + "Task::BaseConfig{{name: '{}', stack_size_bytes: {}, priority: {}, core_id: {}}}", + config.name, config.stack_size_bytes, config.priority, config.core_id); + } +}; + +// for printing of Task::Config using libfmt +template <> struct fmt::formatter { + constexpr auto parse(format_parse_context &ctx) const { return ctx.begin(); } + + template + auto format(const espp::Task::Config &config, FormatContext &ctx) const { + return fmt::format_to( + ctx.out(), "Task::Config{{name: '{}', stack_size_bytes: {}, priority: {}, core_id: {}}}", + config.name, config.stack_size_bytes, config.priority, config.core_id); + } +}; + +// for printing of Task::SimpleConfig using libfmt +template <> struct fmt::formatter { + constexpr auto parse(format_parse_context &ctx) const { return ctx.begin(); } + + template + auto format(const espp::Task::SimpleConfig &config, FormatContext &ctx) const { + return fmt::format_to(ctx.out(), + "Task::SimpleConfig{{callback: '{}', task_config: {}, log_level: {}}}", + config.callback, config.task_config, config.log_level); + } +}; + +// for printing of Task::AdvancedConfig using libfmt +template <> struct fmt::formatter { + constexpr auto parse(format_parse_context &ctx) const { return ctx.begin(); } + + template + auto format(const espp::Task::AdvancedConfig &config, FormatContext &ctx) const { + return fmt::format_to(ctx.out(), + "Task::AdvancedConfig{{callback: '{}', task_config: {}, log_level: {}}}", + config.callback, config.task_config, config.log_level); + } +}; diff --git a/components/task/src/task.cpp b/components/task/src/task.cpp new file mode 100644 index 000000000..c013ce5f0 --- /dev/null +++ b/components/task/src/task.cpp @@ -0,0 +1,150 @@ +#include "task.hpp" + +using namespace espp; + +Task::Task(const Task::Config &config) + : BaseComponent(config.name, config.log_level) + , name_(config.name) + , callback_(config.callback) + , config_({config.name, config.stack_size_bytes, config.priority, config.core_id}) {} + +Task::Task(const Task::SimpleConfig &config) + : BaseComponent(config.task_config.name, config.log_level) + , name_(config.task_config.name) + , simple_callback_(config.callback) + , config_(config.task_config) {} + +Task::Task(const Task::AdvancedConfig &config) + : BaseComponent(config.task_config.name, config.log_level) + , name_(config.task_config.name) + , callback_(config.callback) + , config_(config.task_config) {} + +std::unique_ptr Task::make_unique(const Task::Config &config) { + return std::make_unique(config); +} + +std::unique_ptr Task::make_unique(const Task::SimpleConfig &config) { + return std::make_unique(config); +} + +std::unique_ptr Task::make_unique(const Task::AdvancedConfig &config) { + return std::make_unique(config); +} + +Task::~Task() { + logger_.debug("Destroying task"); + // stop the task if it was started + stop(); + // ensure we stop the thread if it's still around + if (thread_.joinable()) { + thread_.join(); + } + logger_.debug("Task destroyed"); +} + +bool Task::start() { + logger_.debug("Starting task"); + if (started_) { + logger_.warn("Task already started!"); + return false; + } + +#if defined(ESP_PLATFORM) + auto thread_config = esp_pthread_get_default_config(); + thread_config.thread_name = name_.c_str(); + auto core_id = config_.core_id; + if (core_id >= 0) + thread_config.pin_to_core = core_id; + if (core_id >= portNUM_PROCESSORS) { + logger_.error("core_id ({}) is larger than portNUM_PROCESSORS ({}), cannot create Task '{}'", + core_id, portNUM_PROCESSORS, name_); + return false; + } + thread_config.stack_size = config_.stack_size_bytes; + thread_config.prio = config_.priority; + // this will set the config for the next created thread + auto err = esp_pthread_set_cfg(&thread_config); + if (err == ESP_ERR_NO_MEM) { + logger_.error("Out of memory, cannot create Task '{}'", name_); + return false; + } + if (err == ESP_ERR_INVALID_ARG) { + // see + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/pthread.html?highlight=esp_pthread_set_cfg#_CPPv419esp_pthread_set_cfgPK17esp_pthread_cfg_t + logger_.error( + "Configured stack size ({}) is less than PTHREAD_STACK_MIN ({}), cannot create Task '{}'", + config_.stack_size_bytes, PTHREAD_STACK_MIN, name_); + return false; + } +#endif + + if (thread_.joinable()) { + thread_.join(); + } + + // set the atomic so that when the thread starts it won't immediately + // exit. + started_ = true; + // create and start the std::thread + thread_ = std::thread(&Task::thread_function, this); + + logger_.debug("Task started"); + return true; +} + +bool Task::stop() { + if (started_) { + logger_.debug("Stopping task"); + started_ = false; + cv_.notify_all(); + if (thread_.joinable()) { + thread_.join(); + } + logger_.debug("Task stopped"); + } + return true; +} + +bool Task::is_started() const { return started_; } + +bool Task::is_running() const { return is_started(); } + +#if defined(ESP_PLATFORM) || defined(_DOXYGEN_) +std::string Task::get_info() { + return fmt::format("[T] '{}',{},{},{}\n", pcTaskGetName(nullptr), xPortGetCoreID(), + uxTaskPriorityGet(nullptr), uxTaskGetStackHighWaterMark(nullptr)); +} + +std::string Task::get_info(const Task &task) { + TaskHandle_t freertos_handle = xTaskGetHandle(task.name_.c_str()); + return fmt::format("[T] '{}',{},{},{}\n", pcTaskGetName(freertos_handle), xPortGetCoreID(), + uxTaskPriorityGet(freertos_handle), + uxTaskGetStackHighWaterMark(freertos_handle)); +} +#endif + +void Task::thread_function() { + while (started_) { + if (callback_) { + bool should_stop = callback_(cv_m_, cv_); + if (should_stop) { + // callback returned true, so stop running the thread function + logger_.debug("Callback requested stop, thread_function exiting"); + started_ = false; + break; + } + } else if (simple_callback_) { + bool should_stop = simple_callback_(); + if (should_stop) { + // callback returned true, so stop running the thread function + logger_.debug("Callback requested stop, thread_function exiting"); + started_ = false; + break; + } + } else { + started_ = false; + break; + } + } +} diff --git a/components/timer/CMakeLists.txt b/components/timer/CMakeLists.txt index ee7475d16..84fa80e11 100644 --- a/components/timer/CMakeLists.txt +++ b/components/timer/CMakeLists.txt @@ -1,3 +1,4 @@ idf_component_register( INCLUDE_DIRS "include" + SRC_DIRS "src" REQUIRES esp_timer base_component task) diff --git a/components/timer/include/timer.hpp b/components/timer/include/timer.hpp index be898b8dd..d18780809 100644 --- a/components/timer/include/timer.hpp +++ b/components/timer/include/timer.hpp @@ -57,9 +57,9 @@ class Timer : public BaseComponent { std::string_view name; ///< The name of the timer. std::chrono::duration period; ///< The period of the timer. If 0, the timer callback will only be called once. - std::chrono::duration delay{ - 0}; ///< The delay before the first execution of the timer callback after start() is called. - callback_fn callback; ///< The callback function to call when the timer expires. + std::chrono::duration delay = std::chrono::duration( + 0); ///< The delay before the first execution of the timer callback after start() is called. + espp::Timer::callback_fn callback; ///< The callback function to call when the timer expires. bool auto_start{true}; ///< If true, the timer will start automatically when constructed. size_t stack_size_bytes{4096}; ///< The stack size of the task that runs the timer. size_t priority{0}; ///< Priority of the timer, 0 is lowest priority on ESP / FreeRTOS. @@ -70,42 +70,15 @@ class Timer : public BaseComponent { /// @brief Construct a new Timer object /// @param config The configuration for the timer. - explicit Timer(const Config &config) - : BaseComponent(config.name, config.log_level) - , period_(std::chrono::duration_cast(config.period)) - , delay_(std::chrono::duration_cast(config.delay)) - , callback_(config.callback) { - // set the logger rate limit - logger_.set_rate_limit(std::chrono::milliseconds(100)); - // make the task - task_ = espp::Task::make_unique({ - .name = std::string(config.name) + "_task", - .callback = std::bind(&Timer::timer_callback_fn, this, std::placeholders::_1, - std::placeholders::_2), - .stack_size_bytes = config.stack_size_bytes, - .priority = config.priority, - .core_id = config.core_id, - .log_level = config.log_level, - }); - period_float = std::chrono::duration(period_).count(); - delay_float = std::chrono::duration(delay_).count(); - if (config.auto_start) { - start(); - } - } + explicit Timer(const Config &config); /// @brief Destroy the Timer object /// @details Cancels the timer if it is running. - ~Timer() { cancel(); } + ~Timer(); /// @brief Start the timer. /// @details Starts the timer. Does nothing if the timer is already running. - void start() { - logger_.info("starting with period {:.3f} s and delay {:.3f} s", period_float, delay_float); - running_ = true; - // start the task - task_->start(); - } + void start(); /// @brief Start the timer with a delay. /// @details Starts the timer with a delay. If the timer is already running, @@ -114,32 +87,15 @@ class Timer : public BaseComponent { /// with the delay. Overwrites any previous delay that might have /// been set. /// @param delay The delay before the first execution of the timer callback. - void start(std::chrono::duration delay) { - if (delay.count() < 0) { - logger_.warn("delay cannot be negative, not starting"); - return; - } - if (is_running()) { - logger_.info("restarting with delay {:.3f} s", delay.count()); - cancel(); - } - delay_ = std::chrono::duration_cast(delay); - delay_float = std::chrono::duration(delay_).count(); - start(); - } + void start(const std::chrono::duration &delay); /// @brief Stop the timer, same as cancel(). /// @details Stops the timer, same as cancel(). - void stop() { cancel(); } + void stop(); /// @brief Cancel the timer. /// @details Cancels the timer. - void cancel() { - logger_.info("canceling"); - running_ = false; - // cancel the task - task_->stop(); - } + void cancel(); /// @brief Set the period of the timer. /// @details Sets the period of the timer. @@ -148,81 +104,15 @@ class Timer : public BaseComponent { /// @note If the period is negative, the period will not be set / updated. /// @note If the timer is running, the period will be updated after the /// current period has elapsed. - void set_period(std::chrono::duration period) { - if (period.count() < 0) { - logger_.warn("period cannot be negative, not setting"); - return; - } - period_ = std::chrono::duration_cast(period); - period_float = std::chrono::duration(period_).count(); - logger_.info("setting period to {:.3f} s", period_float); - } + void set_period(const std::chrono::duration &period); /// @brief Check if the timer is running. /// @details Checks if the timer is running. /// @return true if the timer is running, false otherwise. - bool is_running() const { return running_ && task_->is_running(); } + bool is_running() const; protected: - bool timer_callback_fn(std::mutex &m, std::condition_variable &cv) { - logger_.debug("callback entered"); - if (!running_) { - // stop the timer, the timer was canceled - logger_.debug("timer was canceled, stopping"); - return true; - } - if (!callback_) { - // stop the timer, the callback is null - logger_.debug("callback is null, stopping"); - running_ = false; - return true; - } - // initial delay, if any - this is only used the first time the timer - // runs - if (delay_float > 0) { - auto start_time = std::chrono::steady_clock::now(); - logger_.debug("waiting for delay {:.3f} s", delay_float); - std::unique_lock lock(m); - cv.wait_until(lock, start_time + delay_); - if (!running_) { - logger_.debug("delay canceled, stopping"); - return true; - } - // now set the delay to 0 - delay_ = std::chrono::microseconds(0); - delay_float = 0; - } - // now run the callback - auto start_time = std::chrono::steady_clock::now(); - logger_.debug("running callback"); - bool requested_stop = callback_(); - if (requested_stop || period_float <= 0) { - // stop the timer if requested or if the period is <= 0 - logger_.debug("callback requested stop or period is <= 0, stopping"); - running_ = false; - return true; - } - auto end = std::chrono::steady_clock::now(); - float elapsed = std::chrono::duration(end - start_time).count(); - if (elapsed > period_float) { - // if the callback took longer than the period, then we should just - // return and run the callback again immediately - logger_.warn_rate_limited("callback took longer ({:.3f} s) than period ({:.3f} s)", elapsed, - period_float); - return false; - } - // now wait for the period (taking into account the time it took to run - // the callback) - { - std::unique_lock lock(m); - cv.wait_until(lock, start_time + period_); - // Note: we don't care about cv_retval here because we are going to - // return from the function anyway. If the timer was canceled, then - // the task will be stopped and the callback will not be called again. - } - // keep the timer running - return false; - } + bool timer_callback_fn(std::mutex &m, std::condition_variable &cv); std::chrono::microseconds period_{0}; ///< The period of the timer. If 0, the timer will run once. std::chrono::microseconds delay_{0}; ///< The delay before the timer starts. diff --git a/components/timer/src/timer.cpp b/components/timer/src/timer.cpp new file mode 100644 index 000000000..3db709352 --- /dev/null +++ b/components/timer/src/timer.cpp @@ -0,0 +1,131 @@ +#include "timer.hpp" + +using namespace espp; + +Timer::Timer(const Timer::Config &config) + : BaseComponent(config.name, config.log_level) + , period_(std::chrono::duration_cast(config.period)) + , delay_(std::chrono::duration_cast(config.delay)) + , callback_(config.callback) { + // set the logger rate limit + logger_.set_rate_limit(std::chrono::milliseconds(100)); + // make the task + task_ = espp::Task::make_unique({ + .name = std::string(config.name) + "_task", + .callback = + std::bind(&Timer::timer_callback_fn, this, std::placeholders::_1, std::placeholders::_2), + .stack_size_bytes = config.stack_size_bytes, + .priority = config.priority, + .core_id = config.core_id, + .log_level = config.log_level, + }); + period_float = std::chrono::duration(period_).count(); + delay_float = std::chrono::duration(delay_).count(); + if (config.auto_start) { + start(); + } +} + +Timer::~Timer() { cancel(); } + +void Timer::start() { + logger_.info("starting with period {:.3f} s and delay {:.3f} s", period_float, delay_float); + running_ = true; + // start the task + task_->start(); +} + +void Timer::start(const std::chrono::duration &delay) { + if (delay.count() < 0) { + logger_.warn("delay cannot be negative, not starting"); + return; + } + if (is_running()) { + logger_.info("restarting with delay {:.3f} s", delay.count()); + cancel(); + } + delay_ = std::chrono::duration_cast(delay); + delay_float = std::chrono::duration(delay_).count(); + start(); +} + +void Timer::stop() { cancel(); } + +void Timer::cancel() { + logger_.info("canceling"); + running_ = false; + // cancel the task + task_->stop(); +} + +void Timer::set_period(const std::chrono::duration &period) { + if (period.count() < 0) { + logger_.warn("period cannot be negative, not setting"); + return; + } + period_ = std::chrono::duration_cast(period); + period_float = std::chrono::duration(period_).count(); + logger_.info("setting period to {:.3f} s", period_float); +} + +bool Timer::is_running() const { return running_ && task_->is_running(); } + +bool Timer::timer_callback_fn(std::mutex &m, std::condition_variable &cv) { + logger_.debug("callback entered"); + if (!running_) { + // stop the timer, the timer was canceled + logger_.debug("timer was canceled, stopping"); + return true; + } + if (!callback_) { + // stop the timer, the callback is null + logger_.debug("callback is null, stopping"); + running_ = false; + return true; + } + // initial delay, if any - this is only used the first time the timer + // runs + if (delay_float > 0) { + auto start_time = std::chrono::steady_clock::now(); + logger_.debug("waiting for delay {:.3f} s", delay_float); + std::unique_lock lock(m); + cv.wait_until(lock, start_time + delay_); + if (!running_) { + logger_.debug("delay canceled, stopping"); + return true; + } + // now set the delay to 0 + delay_ = std::chrono::microseconds(0); + delay_float = 0; + } + // now run the callback + auto start_time = std::chrono::steady_clock::now(); + logger_.debug("running callback"); + bool requested_stop = callback_(); + if (requested_stop || period_float <= 0) { + // stop the timer if requested or if the period is <= 0 + logger_.debug("callback requested stop or period is <= 0, stopping"); + running_ = false; + return true; + } + auto end = std::chrono::steady_clock::now(); + float elapsed = std::chrono::duration(end - start_time).count(); + if (elapsed > period_float) { + // if the callback took longer than the period, then we should just + // return and run the callback again immediately + logger_.warn_rate_limited("callback took longer ({:.3f} s) than period ({:.3f} s)", elapsed, + period_float); + return false; + } + // now wait for the period (taking into account the time it took to run + // the callback) + { + std::unique_lock lock(m); + cv.wait_until(lock, start_time + period_); + // Note: we don't care about cv_retval here because we are going to + // return from the function anyway. If the timer was canceled, then + // the task will be stopped and the callback will not be called again. + } + // keep the timer running + return false; +} diff --git a/doc/Doxyfile b/doc/Doxyfile index 5294401a9..b9f65c920 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -198,6 +198,7 @@ INPUT += $(PROJECT_PATH)/components/t-dongle-s3/include/t-dongle-s3.hpp INPUT += $(PROJECT_PATH)/components/t_keyboard/include/t_keyboard.hpp INPUT += $(PROJECT_PATH)/components/tabulate/include/tabulate.hpp INPUT += $(PROJECT_PATH)/components/task/include/task.hpp +INPUT += $(PROJECT_PATH)/components/task/include/run_on_core.hpp INPUT += $(PROJECT_PATH)/components/thermistor/include/thermistor.hpp INPUT += $(PROJECT_PATH)/components/timer/include/high_resolution_timer.hpp INPUT += $(PROJECT_PATH)/components/timer/include/timer.hpp diff --git a/doc/en/task.rst b/doc/en/task.rst index 71578f2fd..e6cc7aa2f 100644 --- a/doc/en/task.rst +++ b/doc/en/task.rst @@ -23,3 +23,4 @@ API Reference ------------- .. include-build-file:: inc/task.inc +.. include-build-file:: inc/run_on_core.inc diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 000000000..bdaab25d5 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +env/ diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index fa47405c0..3df7336c8 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -14,37 +14,81 @@ set(CMAKE_COLOR_MAKEFILE ON) set(COMPONENTS "../components") set(EXTERNAL "../external") -set(ESPP_INCLUDES - ${EXTERNAL}/fmt/include +set(EXTERNAL_INCLUDES ${EXTERNAL}/alpaca/include + ${EXTERNAL}/cli/include + ${EXTERNAL}/csv2/include + ${EXTERNAL}/hid-rp/hid-rp/ + ${EXTERNAL}/fmt/include + ${EXTERNAL}/magic_enum/include/magic_enum/ + ${EXTERNAL}/tabulate/include +) + +set(ESPP_INCLUDES ${COMPONENTS}/base_component/include ${COMPONENTS}/base_peripheral/include + ${COMPONENTS}/color/include + ${COMPONENTS}/csv/include + ${COMPONENTS}/event_manager/include + ${COMPONENTS}/file_system/include ${COMPONENTS}/ftp/include ${COMPONENTS}/format/include + ${COMPONENTS}/hid-rp/include ${COMPONENTS}/logger/include + ${COMPONENTS}/math/include ${COMPONENTS}/rtsp/include ${COMPONENTS}/serialization/include + ${COMPONENTS}/tabulate/include ${COMPONENTS}/task/include ${COMPONENTS}/timer/include ${COMPONENTS}/socket/include + ${COMPONENTS}/state_machine/include ) set(ESPP_SOURCES + ${COMPONENTS}/color/src/color.cpp + ${COMPONENTS}/event_manager/src/event_manager.cpp ${COMPONENTS}/logger/src/logger.cpp - lib.cpp + ${COMPONENTS}/file_system/src/file_system.cpp + ${COMPONENTS}/rtsp/src/rtcp_packet.cpp + ${COMPONENTS}/rtsp/src/rtp_packet.cpp + ${COMPONENTS}/rtsp/src/rtsp_client.cpp + ${COMPONENTS}/rtsp/src/rtsp_server.cpp + ${COMPONENTS}/rtsp/src/rtsp_session.cpp + ${COMPONENTS}/task/src/task.cpp + ${COMPONENTS}/timer/src/timer.cpp + ${COMPONENTS}/socket/src/socket.cpp + ${COMPONENTS}/socket/src/tcp_socket.cpp + ${COMPONENTS}/socket/src/udp_socket.cpp + espp.cpp ) -include_directories(${ESPP_INCLUDES}) +# if we're on windows, we need to add wcswidth.c to the sources +if(MSVC) + list(APPEND ESPP_SOURCES wcswidth.c) +endif() -set(TARGET_NAME "espp_pc") -# to build for python we need to have the python developer libraries installed -find_package (Python3 COMPONENTS Interpreter Development) +# if we're on Windows, we need to link against ws2_32 +if(WIN32) + set(EXTERNAL_LIBS ws2_32) +endif() + +include_directories(. ${EXTERNAL_INCLUDES} ${ESPP_INCLUDES}) + +set(LINK_ARG "--whole-archive") + +# settings for Windows / MSVC +if(MSVC) + add_compile_options(/utf-8 /D_USE_MATH_DEFINES /bigobj) +endif() + +# settings for MacOS if(APPLE) set(LINK_ARG "-all_load") -else() - set(LINK_ARG "--whole-archive") endif() +set(TARGET_NAME "espp_pc") + # main library (which can be built for pc, android, and iOS) add_library( # Specifies the name of the library. ${TARGET_NAME} @@ -54,27 +98,32 @@ add_library( # Specifies the name of the library. ${ESPP_SOURCES} ) set_property(TARGET ${TARGET_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) target_link_options(${TARGET_NAME} PRIVATE "${LINK_ARG}") +target_link_libraries(${TARGET_NAME} ${EXTERNAL_LIBS}) target_compile_features(${TARGET_NAME} PRIVATE cxx_std_20) # and install it into the hardware_tests/pc folder for use by the python scripts there install(TARGETS ${TARGET_NAME} ARCHIVE DESTINATION ${PROJECT_SOURCE_DIR}/pc) # and install the headers for use by the c++ code there -install(DIRECTORY ${ESPP_INCLUDES} DESTINATION ${PROJECT_SOURCE_DIR}/pc) +install(DIRECTORY ${EXTERNAL_INCLUDES} DESTINATION ${PROJECT_SOURCE_DIR}/pc) +install(DIRECTORY ${ESPP_INCLUDES} DESTINATION ${PROJECT_SOURCE_DIR}/pc/) +# we have to make sure to install the magic_enum includes differently, since they're in different folders +install(DIRECTORY ${EXTERNAL}/magic_enum/include/magic_enum/ DESTINATION ${PROJECT_SOURCE_DIR}/pc/include/) +# and install the espp.hpp file for use by the python scripts there +install(FILES espp.hpp wcswidth.h DESTINATION ${PROJECT_SOURCE_DIR}/pc/include/) + +# and we need to include the pybind11 library +add_subdirectory(pybind11) + +# NOTE: this is an alternate WIP way +# python_add_library(_core MODULE +# ./python_bindings/module.cpp ./python_bindings/pybind_espp.cpp ${ESPP_SOURCES} WITH_SOABI) +# target_link_libraries(_core PRIVATE pybind11::headers) +# install(TARGETS _core DESTINATION ${PROJECT_SOURCE_DIR}/pc/) + # Python binding -add_library( # Specifies the name of the library. - espp - # Sets the library as a shared (.so) library. - MODULE - # Provides a relative path to your source file(s). - ./bind.cpp ) -set_property(TARGET espp PROPERTY POSITION_INDEPENDENT_CODE ON) -target_link_libraries(espp espp_pc pthread ${Python3_LIBRARIES}) -target_include_directories(espp PRIVATE pybind11/include ${Python3_INCLUDE_DIRS}) -target_link_options(espp PRIVATE ${Python3_LINK_OPTIONS}) +pybind11_add_module(espp + ./python_bindings/module.cpp ./python_bindings/pybind_espp.cpp ${ESPP_SOURCES}) target_compile_features(espp PRIVATE cxx_std_20) -# This changes the filename to `espp.so` -set_target_properties(espp PROPERTIES PREFIX "") -# and install it into the hardware_tests/pc folder for use by the python scripts there install(TARGETS espp LIBRARY DESTINATION ${PROJECT_SOURCE_DIR}/pc/) diff --git a/lib/README.md b/lib/README.md index c9ba646e4..bb9646e2a 100644 --- a/lib/README.md +++ b/lib/README.md @@ -3,7 +3,7 @@ This folder contains the configuration needed to cross-compile the central (cross-platform) components of espp for the following platforms: -* PC (Linux, MacOS) +* PC (Linux, MacOS, Windows) * C++ * Python (through pybind 11) @@ -16,13 +16,15 @@ cmake: mkdir build cd build cmake .. -make -make install +cmake --build . --config Release --target install ``` +This is conveniently scripted up for you into [./build.sh](./build.sh) and +[./build.ps1](./build.ps1) scripts you can simply run from your terminal. + This will build and install the following files: -* `./pc/libespp_pc.a` - C++ static library for use with other C++ code. +* `./pc/libespp_pc` - C++ static library for use with other C++ code. * `./pc/include` - All the header files need for using the library from C++ code. * `./pc/espp.so` - C++ shared library for python binding for use with python code. diff --git a/lib/autogenerate_bindings.py b/lib/autogenerate_bindings.py new file mode 100644 index 000000000..57eeccbfb --- /dev/null +++ b/lib/autogenerate_bindings.py @@ -0,0 +1,154 @@ +import litgen +from srcmlcpp import SrcmlcppOptions +import os + + +def my_litgen_options() -> litgen.LitgenOptions: + # configure your options here + options = litgen.LitgenOptions() + + # /////////////////////////////////////////////////////////////////// + # Root namespace + # /////////////////////////////////////////////////////////////////// + # The namespace espp is the C++ root namespace for the generated bindings + # (i.e. no submodule will be generated for it in the python bindings) + options.namespaces_root = ["espp"] + # we don't actualy want to exclude the detail namespace + options.namespace_exclude__regex = r"[Ii]nternal" # default was r"[Ii]nternal|[Dd]etail" + + # ////////////////////////////////////////////////////////////////// + # Basic functions bindings + # //////////////////////////////////////////////////////////////////// + # No specific option is needed for these basic bindings + # litgen will add the docstrings automatically in the python bindings + + # ////////////////////////////////////////////////////////////////// + # Classes and structs bindings + # ////////////////////////////////////////////////////////////////// + # No specific option is needed for these bindings. + # - Litgen will automatically add a default constructor with named parameters + # for structs that have no constructor defined in C++. + # - A class will publish only its public methods and members + # To prevent the generation of the default constructor with named parameters + # for a specific struct, you can use the following option: + options.struct_create_default_named_ctor__regex = r".*" # default + options.class_create_default_named_ctor__regex = r".*" + + # /////////////////////////////////////////////////////////////////// + # Exclude functions and/or parameters from the bindings + # /////////////////////////////////////////////////////////////////// + # We want to exclude `inline void priv_SetOptions(bool v) {}` from the bindings + # priv_ is a prefix for private functions that we don't want to expose + # options.fn_exclude_by_name__regex = "run_on_core" # NOTE: this doesn't work since it seems to be parsing that fails for Task::run_on_core + + # we'd like the following classes to be able to pick up new attributes + # dynamically (within python): + # - + options.class_dynamic_attributes__regex = r".*" # expose all classes to support dynamic attributes + + # Inside `inline void SetOptions(bool v, bool priv_param = false) {}`, + # we don't want to expose the private parameter priv_param + # (it is possible since it has a default value) + # options.fn_params_exclude_names__regex = "^priv_" + + # //////////////////////////////////////////////////////////////////// + # Override virtual methods in python + # //////////////////////////////////////////////////////////////////// + # The virtual methods of this class can be overriden in python + options.class_template_options.add_specialization(r"WeightedConfig", ["espp::Vector2f"]) # NOTE: this doesn't seem to work + options.class_template_options.add_specialization(r"Bezier", ["espp::Vector2f"]) + options.class_template_options.add_specialization(r"Bezier::Config", ["espp::Vector2f"]) # NOTE: this doesn't seem to work + options.class_template_options.add_specialization(r"RangeMapper", ["int", "float"]) + options.class_template_options.add_specialization(r"RangeMapper::Config", ["int", "float"]) # NOTE: this doesn't seem to work + options.class_template_options.add_specialization(r"Vector2d", ["int", "float"]) # NOTE: this still generates some bindings which are not specialized for some reason + + # //////////////////////////////////////////////////////////////////// + # Publish bindings for template functions + # //////////////////////////////////////////////////////////////////// + options.fn_template_options.add_specialization(r"^sgn$", ["int", "float"], add_suffix_to_function_name=False) + + # //////////////////////////////////////////////////////////////////// + # Return values policy + # //////////////////////////////////////////////////////////////////// + # `FileSystem& get()` and `EventManager& get()` return references, that + # python should not free, so we force the reference policy to be 'reference' + # instead of 'automatic' + options.fn_return_force_policy_reference_for_references__regex = "^get$" + + # //////////////////////////////////////////////////////////////////// + # Boxed types + # //////////////////////////////////////////////////////////////////// + # Adaptation for `inline void SwitchBoolValue(bool &v) { v = !v; }` + # SwitchBoolValue is a C++ function that takes a bool parameter by reference and changes its value + # Since bool are immutable in python, we can to use a BoxedBool instead + options.fn_params_replace_modifiable_immutable_by_boxed__regex = "^SwitchBoolValue$" + + # //////////////////////////////////////////////////////////////////// + # Published vectorized math functions and namespaces + # //////////////////////////////////////////////////////////////////// + # The functions in the MathFunctions namespace will be also published as vectorized functions + # options.fn_namespace_vectorize__regex = r"^espp::MathFunctions$" # Do it in this namespace only + options.fn_vectorize__regex = r".*" # For all functions + + # //////////////////////////////////////////////////////////////////// + # Format the python stubs with black + # //////////////////////////////////////////////////////////////////// + # Set to True if you want the stub file to be formatted with black + options.python_run_black_formatter = False + + return options + + +def autogenerate() -> None: + repository_dir = os.path.realpath(os.path.dirname(__file__) + "/../") + output_dir = repository_dir + "/lib/python_bindings" + + include_dir = repository_dir + "/components/" + header_files = [include_dir + "base_component/include/base_component.hpp", + include_dir + "color/include/color.hpp", + include_dir + "event_manager/include/event_manager.hpp", + # include_dir + "file_system/include/file_system.hpp", # can't deal with singleton that does not support constructor / destructor + include_dir + "ftp/include/ftp_server.hpp", + # include_dir + "ftp/include/ftp_client_session.hpp", can't deal with tcpsocket unique ptr in constructor + include_dir + "logger/include/logger.hpp", + include_dir + "math/include/bezier.hpp", # have to set class template options + include_dir + "math/include/fast_math.hpp", + include_dir + "math/include/gaussian.hpp", + include_dir + "math/include/range_mapper.hpp", # have to set class template options + include_dir + "math/include/vector2d.hpp", # have to set class template options + # include_dir + "rtsp/include/jpeg_frame.hpp", + # include_dir + "rtsp/include/jpeg_header.hpp", + # include_dir + "rtsp/include/rtsp_client.hpp", + # include_dir + "rtsp/include/rtsp_server.hpp", + include_dir + "socket/include/socket.hpp", + include_dir + "socket/include/tcp_socket.hpp", + include_dir + "socket/include/udp_socket.hpp", + include_dir + "task/include/task.hpp", + include_dir + "timer/include/timer.hpp", + + # state machine: + # include_dir + "state_machine/include/deep_history_state.hpp", + # include_dir + "state_machine/include/shallow_history_state.hpp", + # include_dir + "state_machine/include/state_base.hpp", + # include_dir + "state_machine/include/magic_enum.hpp", + + # csv (template header): + # include_dir + "csv/include/csv.hpp", + + # tabulate (template header): + # include_dir + "tabulate/include/tabulate.hpp", + + # serialization (template header): + # include_dir + "serialization/include/serialization.hpp", + ] + + litgen.write_generated_code_for_files( + options=my_litgen_options(), + input_cpp_header_files=header_files, + output_cpp_pydef_file=output_dir + "/pybind_espp.cpp", + output_stub_pyi_file=output_dir + "/espp/__init__.pyi", + ) + + +if __name__ == "__main__": + autogenerate() diff --git a/lib/bind.cpp b/lib/bind.cpp index f9cb73af7..80d935445 100644 --- a/lib/bind.cpp +++ b/lib/bind.cpp @@ -4,15 +4,7 @@ #include #include -#include "ftp_server.hpp" -#include "logger.hpp" -#include "rtsp_client.hpp" -#include "rtsp_server.hpp" -#include "serialization.hpp" -#include "task.hpp" -#include "tcp_socket.hpp" -#include "timer.hpp" -#include "udp_socket.hpp" +#include "espp.hpp" namespace py = pybind11; using namespace espp; diff --git a/lib/build.ps1 b/lib/build.ps1 new file mode 100644 index 000000000..6a777a334 --- /dev/null +++ b/lib/build.ps1 @@ -0,0 +1,19 @@ +# powershell script to build the project using cmake + +# Create build directory if it doesn't exist +$buildDir = "build" +if (-not (Test-Path -Path $buildDir)) { + New-Item -ItemType Directory -Path $buildDir +} + +# Change to the build directory +Set-Location -Path $buildDir + +# Run cmake +cmake .. + +# Run cmake --build . --config Release --target install +cmake --build . --config Release --target install + +# Change back to the original directory +Set-Location -Path .. diff --git a/lib/build.sh b/lib/build.sh index e889084a9..37e73ef69 100755 --- a/lib/build.sh +++ b/lib/build.sh @@ -3,5 +3,4 @@ mkdir build cd build cmake .. -make -make install +cmake --build . --config Release --target install diff --git a/lib/espp.cpp b/lib/espp.cpp new file mode 100644 index 000000000..a8bc32434 --- /dev/null +++ b/lib/espp.cpp @@ -0,0 +1,5 @@ +#include "espp.hpp" + +#ifdef _MSC_VER +#pragma comment(lib, "Ws2_32.lib") +#endif diff --git a/lib/espp.hpp b/lib/espp.hpp new file mode 100644 index 000000000..11d528685 --- /dev/null +++ b/lib/espp.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "socket_msvc.hpp" + +#ifdef _MSC_VER +extern "C" { +// NOTE: needed for tabulate +#include "wcswidth.h" +} +#endif + +#include +#include + +#include "base_component.hpp" +#include "bezier.hpp" +#include "color.hpp" +#include "csv.hpp" +#include "event_manager.hpp" +#include "fast_math.hpp" +#include "file_system.hpp" +#include "ftp_client_session.hpp" +#include "ftp_server.hpp" +#include "gaussian.hpp" +// TODO: these are not working +// #include "hid-rp.hpp" +// #include "hid-rp-gamepad.hpp" +#include "logger.hpp" +#include "range_mapper.hpp" +#include "rtsp_client.hpp" +#include "rtsp_server.hpp" +#include "serialization.hpp" +#include "tabulate.hpp" +#include "task.hpp" +#include "tcp_socket.hpp" +#include "timer.hpp" +#include "udp_socket.hpp" +#include "vector2d.hpp" + +// state machine includes +#include "deep_history_state.hpp" +#include "magic_enum.hpp" +#include "shallow_history_state.hpp" +#include "state_base.hpp" + +#include diff --git a/lib/lib.cpp b/lib/lib.cpp deleted file mode 100644 index 66b23d878..000000000 --- a/lib/lib.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "ftp_server.hpp" -#include "logger.hpp" -#include "rtsp_client.hpp" -#include "rtsp_server.hpp" -#include "serialization.hpp" -#include "task.hpp" -#include "tcp_socket.hpp" -#include "timer.hpp" -#include "udp_socket.hpp" diff --git a/lib/python_bindings/espp/__init__.py b/lib/python_bindings/espp/__init__.py new file mode 100644 index 000000000..aeafc623a --- /dev/null +++ b/lib/python_bindings/espp/__init__.py @@ -0,0 +1,2 @@ +from espp import * # type: ignore # noqa: F403 +from espp import __version__ # noqa: F401 diff --git a/lib/python_bindings/espp/__init__.pyi b/lib/python_bindings/espp/__init__.pyi new file mode 100644 index 000000000..aac968159 --- /dev/null +++ b/lib/python_bindings/espp/__init__.pyi @@ -0,0 +1,2837 @@ +# If you want to use mypy or pyright, you may have to ignore some errors, like below: + +# mypy: disable-error-code="type-arg" + +from typing import overload, List + +NumberType = (int, float, np.number) + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! AUTOGENERATED CODE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# // Autogenerated code below! Do not edit! +#################### #################### + + + +class BaseComponent: + """/ Base class for all components + / Provides a logger and some basic logging configuration + """ + def get_name(self) -> str: + """/ Get the name of the component + / \return A const reference to the name of the component + / \note This is the tag of the logger + """ + pass + + def set_log_tag(self, tag: std.string_view) -> None: + """/ Set the tag for the logger + / \param tag The tag to use for the logger + """ + pass + + def get_log_level(self) -> Logger.Verbosity: + """/ Get the log level for the logger + / \return The verbosity level of the logger + / \sa Logger::Verbosity + / \sa Logger::set_verbosity + """ + pass + + def set_log_level(self, level: Logger.Verbosity) -> None: + """/ Set the log level for the logger + / \param level The verbosity level to use for the logger + / \sa Logger::Verbosity + / \sa Logger::set_verbosity + """ + pass + + def set_log_verbosity(self, level: Logger.Verbosity) -> None: + """/ Set the log verbosity for the logger + / \param level The verbosity level to use for the logger + / \note This is a convenience method that calls set_log_level + / \sa set_log_level + / \sa Logger::Verbosity + / \sa Logger::set_verbosity + """ + pass + + def get_log_verbosity(self) -> Logger.Verbosity: + """/ Get the log verbosity for the logger + / \return The verbosity level of the logger + / \note This is a convenience method that calls get_log_level + / \sa get_log_level + / \sa Logger::Verbosity + / \sa Logger::get_verbosity + """ + pass + + def set_log_rate_limit(self, rate_limit: std.chrono.duration) -> None: + """/ Set the rate limit for the logger + / \param rate_limit The rate limit to use for the logger + / \note Only calls to the logger that have _rate_limit suffix will be rate limited + / \sa Logger::set_rate_limit + """ + pass + + +#################### #################### + + +#################### #################### + + + + +class Rgb: + """* + * @brief Class representing a color using RGB color space. + + """ + r: float = float(0) #/< Red value ∈ [0, 1] + g: float = float(0) #/< Green value ∈ [0, 1] + b: float = float(0) #/< Blue value ∈ [0, 1] + + @overload + def __init__(self) -> None: + pass + + @overload + def __init__(self, r: float, g: float, b: float) -> None: + """* + * @brief Construct an Rgb object from the provided rgb values. + * @note If provided values outside the range [0,1], it will rescale them to + * be within the range [0,1] by dividing by 255. + * @param r Floating point value for the red channel, should be in range [0, + * 1] + * @param g Floating point value for the green channel, should be in range + * [0, 1] + * @param b Floating point value for the blue channel, should be in range + * [0, 1] + + """ + pass + + @overload + def __init__(self, rgb: Rgb) -> None: + """* + * @brief Copy-construct an Rgb object from the provided object. + * @note If provided values outside the range [0,1], it will rescale them to + * be within the range [0,1] by dividing by 255. + * @param rgb Rgb struct containing the values to copy. + + """ + pass + + @overload + def __init__(self, hsv: Hsv) -> None: + """* + * @brief Construct an Rgb object from the provided Hsv object. + * @note This calls hsv.rgb() on the provided object, which means that invalid + * HSV data (not in the ranges [0,360], [0,1], and [0,1]) could lead to + * bad RGB data. The Rgb constructor will automatically convert the + * values to be in the proper range, but the perceived color will be + * changed. + * @param hsv Hsv object to copy. + + """ + pass + + @overload + def __init__(self, hex: int) -> None: + """* + * @brief Construct an Rgb object from the provided hex value. + * @param hex Hex value to convert to RGB. The hex value should be in the + * format 0xRRGGBB. + + """ + pass + + + + def __add__(self, rhs: Rgb) -> Rgb: + """* + * @brief Perform additive color blending (averaging) + * @param rhs Other color to add to this color to create the resultant color + * @return Resultant color from blending this color with the \p rhs color. + + """ + pass + + def __iadd__(self, rhs: Rgb) -> Rgb: + """* + * @brief Perform additive color blending (averaging) + * @param rhs Other color to add to this color + + """ + pass + + def __eq__(self, rhs: Rgb) -> bool: + pass + + def __ne__(self, rhs: Rgb) -> bool: + pass + + def hsv(self) -> Hsv: + """* + * @brief Get a HSV representation of this RGB color. + * @return An HSV object containing the HSV representation. + + """ + pass + + def hex(self) -> int: + """* + * @brief Get the hex representation of this RGB color. + * @return The hex representation of this RGB color. + + """ + pass + +class Hsv: + """* + * @brief Class representing a color using HSV color space. + + """ + h: float = float(0) #/< Hue ∈ [0, 360] + s: float = float(0) #/< Saturation ∈ [0, 1] + v: float = float(0) #/< Value ∈ [0, 1] + + @overload + def __init__(self) -> None: + pass + + @overload + def __init__(self, h: float, s: float, v: float) -> None: + """* + * @brief Construct a Hsv object from the provided values. + * @param h Hue - will be clamped to be in range [0, 360] + * @param s Saturation - will be clamped to be in range [0, 1] + * @param v Value - will be clamped to be in range [0, 1] + + """ + pass + + @overload + def __init__(self, hsv: Hsv) -> None: + """* + * @brief Copy-construct the Hsv object + * @param hsv Object to copy from. + + """ + pass + + @overload + def __init__(self, rgb: Rgb) -> None: + """* + * @brief Construct Hsv object from Rgb object. Calls rgb.hsv() to perform + * the conversion. + * @param rgb The Rgb object to convert and copy. + + """ + pass + + + def __eq__(self, rhs: Hsv) -> bool: + pass + + def __ne__(self, rhs: Hsv) -> bool: + pass + + + def rgb(self) -> Rgb: + """* + * @brief Get a RGB representation of this HSV color. + * @return An RGB object containing the RGB representation. + + """ + pass + +@overload +def color_code(rgb: Rgb) -> Any: + """ + (C++ auto return type) + """ + pass +@overload +def color_code(hsv: Hsv) -> Any: + """ + (C++ auto return type) + """ + pass + + +# namespace espp + +#################### #################### + + +#################### #################### + + + +class EventManager: + """* + * @brief Singleton class for managing events. Provides mechanisms for + * anonymous publish / subscribe interactions - enabling one to one, + * one to many, many to one, and many to many data distribution with + * loose coupling and low overhead. Each topic runs a thread for that + * topic's subscribers, executing all the callbacks in sequence and + * then going to sleep again until new data is published. + * + * @note In c++ objects, it's recommended to call the + * add_publisher/add_subscriber functions in the class constructor and + * then to call the remove_publisher/remove_subscriber functions in the + * class destructor. + * + * @note It is recommended (unless you are only interested in events and not + * data or are only needing to transmit actual strings) to use a + * serialization library (such as espp::serialization - which wraps + * alpaca) to serialize your data structures to string when publishing + * and then deserialize your data from string in the subscriber + * callbacks. + * + * \section event_manager_ex1 Event Manager Example + * \snippet event_manager_example.cpp event manager example + + """ + + @staticmethod + def get() -> EventManager: + """* + * @brief Get the singleton instance of the EventManager. + * @return A reference to the EventManager singleton. + + """ + pass + + + def add_publisher(self, topic: str, component: str) -> bool: + """* + * @brief Register a publisher for \p component on \p topic. + * @param topic Topic name for the data being published. + * @param component Name of the component publishing data. + * @return True if the publisher was added, False if it was already + * registered for that component. + + """ + pass + + @overload + def add_subscriber( + self, + topic: str, + component: str, + callback: EventManager.event_callback_fn, + stack_size_bytes: int = 8192 + ) -> bool: + """* + * @brief Register a subscriber for \p component on \p topic. + * @param topic Topic name for the data being subscribed to. + * @param component Name of the component publishing data. + * @param callback The event_callback_fn to be called when receicing data on + * \p topic. + * @param stack_size_bytes The stack size in bytes to use for the subscriber + * @note The stack size is only used if a subscriber is not already registered + * for that topic. If a subscriber is already registered for that topic, + * the stack size is ignored. + * @return True if the subscriber was added, False if it was already + * registered for that component. + + """ + pass + + @overload + def add_subscriber( + self, + topic: str, + component: str, + callback: EventManager.event_callback_fn, + task_config: Task.BaseConfig + ) -> bool: + """* + * @brief Register a subscriber for \p component on \p topic. + * @param topic Topic name for the data being subscribed to. + * @param component Name of the component publishing data. + * @param callback The event_callback_fn to be called when receicing data on + * \p topic. + * @param task_config The task configuration to use for the subscriber. + * @note The task_config is only used if a subscriber is not already + * registered for that topic. If a subscriber is already registered for + * that topic, the task_config is ignored. + * @return True if the subscriber was added, False if it was already + * registered for that component. + + """ + pass + + def publish(self, topic: str, data: List[int]) -> bool: + """* + * @brief Publish \p data on \p topic. + * @param topic Topic to publish data on. + * @param data Data to publish, within a vector container. + * @return True if \p data was successfully published to \p topic, False + * otherwise. Publish will not occur (and will return False) if + * there are no subscribers for this topic. + + """ + pass + + def remove_publisher(self, topic: str, component: str) -> bool: + """* + * @brief Remove \p component's publisher for \p topic. + * @param topic The topic that \p component was publishing on. + * @param component The component for which the publisher was registered. + * @return True if the publisher was removed, False if it was not + * registered. + + """ + pass + + def remove_subscriber(self, topic: str, component: str) -> bool: + """* + * @brief Remove \p component's subscriber for \p topic. + * @param topic The topic that \p component was subscribing to. + * @param component The component for which the subscriber was registered. + * @return True if the subscriber was removed, False if it was not + * registered. + + """ + pass + + +#################### #################### + + +#################### #################### + + + + + +class FtpServer: + """/ \brief A class that implements a FTP server.""" + def __init__( + self, + ip_address: std.string_view, + port: int, + root: std.filesystem.path + ) -> None: + """/ \brief A class that implements a FTP server. + / \note The IP Address is not currently used to select the right + / interface, but is instead passed to the FtpClientSession so that + / it can be used in the PASV command. + / \param ip_address The IP address to listen on. + / \param port The port to listen on. + / \param root The root directory of the FTP server. + """ + pass + + + def start(self) -> bool: + """/ \brief Start the FTP server. + / Bind to the port and start accepting connections. + / \return True if the server was started, False otherwise. + """ + pass + + def stop(self) -> None: + """/ \brief Stop the FTP server.""" + pass + + +#################### #################### + + +#################### #################### + + + + + +class Logger: + """* + * @brief Logger provides a wrapper around nicer / more robust formatting than + * standard ESP_LOG* macros with the ability to change the log level at + * run-time. Logger currently is a light wrapper around libfmt (future + * std::format). + * + * To save on code size, the logger has the ability to be compiled out based on + * the log level set in the sdkconfig. This means that if the log level is set to + * ERROR, all debug, info, and warn logs will be compiled out. This is done by + * checking the log level at compile time and only compiling in the functions + * that are needed. + * + * \section logger_ex1 Basic Example + * \snippet logger_example.cpp Logger example + * \section logger_ex2 Threaded Logging and Verbosity Example + * \snippet logger_example.cpp MultiLogger example + + """ + class Verbosity(enum.Enum): + """* + * Verbosity levels for the logger, in order of increasing priority. + + """ + debug = enum.auto() # (= 0) #*< Debug level verbosity. + info = enum.auto() # (= 1) #*< Info level verbosity. + warn = enum.auto() # (= 2) #*< Warn level verbosity. + error = enum.auto() # (= 3) #*< Error level verbosity. + none = enum.auto() # (= 4) #*< No verbosity - logger will not print anything. + + class Config: + """* + * @brief Configuration struct for the logger. + + """ + tag: std.string_view #*< The TAG that will be prepended to all logs. + include_time: bool = bool(True) #*< Include the time in the log. + rate_limit: std.chrono.duration = std.chrono.duration(0) #*< The rate limit for the logger. Optional, if <= 0 no + rate limit. @note Only calls that have _rate_limited suffixed will be rate limited. + level: espp.Logger.Verbosity = espp.Logger.Verbosity.warn #*< The verbosity level for the logger. + def __init__( + self, + tag: std.string_view = std.string_view(), + include_time: bool = bool(True), + rate_limit: std.chrono.duration = std.chrono.duration(0), + level: Logger.Verbosity = Logger.Verbosity.warn + ) -> None: + """Auto-generated default constructor with named params""" + pass + + + def set_verbosity(self, level: Logger.Verbosity) -> None: + """* + * @brief Change the verbosity for the logger. \sa Logger::Verbosity + * @param level new verbosity level + + """ + pass + + def set_tag(self, tag: std.string_view) -> None: + """* + * @brief Change the tag for the logger. + * @param tag The new tag. + + """ + pass + + def get_tag(self) -> str: + """* + * @brief Get the current tag for the logger. + * @return A const reference to the current tag. + + """ + pass + + def set_include_time(self, include_time: bool) -> None: + """* + * @brief Whether to include the time in the log. + * @param include_time Whether to include the time in the log. + * @note The time is in seconds since boot and is represented as a floating + * point number with precision to the millisecond. + + """ + pass + + def set_rate_limit(self, rate_limit: std.chrono.duration) -> None: + """* + * @brief Change the rate limit for the logger. + * @param rate_limit The new rate limit. + * @note Only calls that have _rate_limited suffixed will be rate limited. + + """ + pass + + def get_rate_limit(self) -> std.chrono.duration: + """* + * @brief Get the current rate limit for the logger. + * @return The current rate limit. + + """ + pass + + + + + + + + + + + def __init__(self) -> None: + """Auto-generated default constructor""" + pass + +#################### #################### + + +#################### #################### + + +# ------------------------------------------------------------------------ +# +# ------------------------------------------------------------------------ + +#################### #################### + + +#################### #################### + + +def square(f: float) -> float: + """* + * @brief Simple square of the input. + * @param f Value to square. + * @return The square of f (f*f). + + """ + pass + +def cube(f: float) -> float: + """* + * @brief Simple cube of the input. + * @param f Value to cube. + * @return The cube of f (f*f*f). + + """ + pass + +def fast_sqrt(value: float) -> float: + """* + * @brief Fast square root approximation. + * @note Using https://reprap.org/forum/read.php?147,219210 and + * https://en.wikipedia.org/wiki/Fast_inverse_square_root + * @param value Value to take the square root of. + * @return Approximation of the square root of value. + + """ + pass + +# ------------------------------------------------------------------------ +# +# ------------------------------------------------------------------------ + +def lerp(a: float, b: float, t: float) -> float: + """* + * @brief Linear interpolation between two values. + * @param a First value. + * @param b Second value. + * @param t Interpolation factor in the range [0, 1]. + * @return Linear interpolation between a and b. + + """ + pass + +def inv_lerp(a: float, b: float, v: float) -> float: + """* + * @brief Compute the inverse lerped value. + * @param a First value (usually the lower of the two). + * @param b Second value (usually the higher of the two). + * @param v Value to inverse lerp (usually a value between a and b). + * @return Inverse lerp value, the factor of v between a and b in the range [0, + * 1] if v is between a and b, 0 if v == a, or 1 if v == b. If a == b, + * 0 is returned. If v is outside the range [a, b], the value is + * extrapolated linearly (i.e. if v < a, the value is less than 0, if v + * > b, the value is greater than 1). + + """ + pass + +def piecewise_linear(points: std.vector], x: float) -> float: + """* + * @brief Compute the piecewise linear interpolation between a set of points. + * @param points Vector of points to interpolate between. The vector should be + * sorted by the first value in the pair. The first value in the + * pair is the x value and the second value is the y value. The x + * values should be unique. The function will interpolate between + * the points using linear interpolation. If x is less than the + * first x value, the first y value is returned. If x is greater + * than the last x value, the last y value is returned. If x is + * between two x values, the y value is interpolated between the + * two y values. + * @param x Value to interpolate at. Should be a value from the first + * distribution of the points (the domain). If x is outside the domain + * of the points, the value returned will be clamped to the first or + * last y value. + * @return Interpolated value at x. + + """ + pass + +def round(x: float) -> int: + """* + * @brief Round x to the nearest integer. + * @param x Floating point value to be rounded. + * @return Nearest integer to x. + + """ + pass + +def fast_ln(x: float) -> float: + """* + * @brief fast natural log function, ln(x). + * @note This speed hack comes from: + * https://gist.github.com/LingDong-/7e4c4cae5cbbc44400a05ba650623 + * @param x Value to take the natural log of. + * @return ln(x) + + """ + pass + + +def fast_sin(angle: float) -> float: + """* + * @brief Fast approximation of sin(angle) (radians). + * @note \p Angle must be in the range [0, 2PI]. + * @param angle Angle in radians [0, 2*PI] + * @return Approximation of sin(value) + + """ + pass + +def fast_cos(angle: float) -> float: + """* + * @brief Fast approximation of cos(angle) (radians). + * @note \p Angle must be in the range [0, 2PI]. + * @param angle Angle in radians [0, 2*PI] + * @return Approximation of cos(value) + + """ + pass + +#################### #################### + + +#################### #################### + + +class Gaussian: + """* + * @brief Implements a gaussian function + * \f$y(t)=\alpha\exp(-\frac{(t-\beta)^2}{2\gamma^2})\f$. + * @details Alows you to store the alpha, beta, and gamma coefficients as well + * as update them dynamically. + * + * \section gaussian_ex1 Example + * \snippet math_example.cpp gaussian example + * \section gaussian_ex2 Fade-In/Fade-Out Example + * \snippet math_example.cpp gaussian fade in fade out example + + """ + class Config: + """* + * @brief Configuration structure for initializing the gaussian. + + """ + gamma: float #/< Slope of the gaussian, range [0, 1]. 0 is more of a thin spike from 0 up to + #/< max output (alpha), 1 is more of a small wave around the max output (alpha). + alpha: float = float(1.0) #/< Max amplitude of the gaussian output, defautls to 1.0. + beta: float = float(0.5) #/< Beta value for the gaussian, default to be symmetric at 0.5 in range [0,1]. + + def __eq__(self, rhs: Gaussian.Config) -> bool: + pass + def __init__( + self, + gamma: float = float(), + alpha: float = float(1.0), + beta: float = float(0.5) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + + def __call__(self, t: float) -> float: + """* + * @brief Evaluate the gaussian at \p t. + * @note Convienience wrapper around the at() method. + * @param t The evaluation parameter, [0, 1]. + * @return The gaussian evaluated at \p t. + + """ + pass + + def update(self, config: Gaussian.Config) -> None: + """* + * @brief Update the gaussian configuration. + * @param config The new configuration. + + """ + pass + + def set_config(self, config: Gaussian.Config) -> None: + """* + * @brief Set the configuration of the gaussian. + * @param config The new configuration. + + """ + pass + + def get_config(self) -> Gaussian.Config: + """* + * @brief Get the current configuration of the gaussian. + * @return The current configuration. + + """ + pass + + def get_gamma(self) -> float: + """* + * @brief Get the gamma value. + * @return The gamma value. + + """ + pass + + def get_alpha(self) -> float: + """* + * @brief Get the alpha value. + * @return The alpha value. + + """ + pass + + def get_beta(self) -> float: + """* + * @brief Get the beta value. + * @return The beta value. + + """ + pass + + def set_gamma(self, gamma: float) -> None: + """* + * @brief Set the gamma value. + * @param gamma The new gamma value. + + """ + pass + + def set_alpha(self, alpha: float) -> None: + """* + * @brief Set the alpha value. + * @param alpha The new alpha value. + + """ + pass + + def set_beta(self, beta: float) -> None: + """* + * @brief Set the beta value. + * @param beta The new beta value. + + """ + pass + + def __init__(self) -> None: + """Auto-generated default constructor""" + pass + +#################### #################### + + +#################### #################### + + +# ------------------------------------------------------------------------ +# +# ------------------------------------------------------------------------ + + + +# namespace espp + +#################### #################### + + +#################### #################### + + +# ------------------------------------------------------------------------ +# +# ------------------------------------------------------------------------ + + + + +# namespace espp + +#################### #################### + + +#################### #################### + + +# if we're on windows, we cannot include netinet/in.h and instead need to use +# winsock2.h + + + + +class Socket: + """* + * @brief Class for a generic socket with some helper functions for + * configuring the socket. + + """ + class Type(enum.Enum): + raw = enum.auto() # (= SOCK_RAW) #*< Only IP headers, no TCP or UDP headers as well. + dgram = enum.auto() # (= SOCK_DGRAM) #*< UDP/IP socket - datagram. + stream = enum.auto() # (= SOCK_STREAM) #*< TCP/IP socket - stream. + + class Info: + """* + * @brief Storage for socket information (address, port) with convenience + * functions to convert to/from POSIX structures. + + """ + address: str #*< IP address of the endpoint as a string. + port: int #*< Port of the endpoint as an integer. + + def init_ipv4(self, addr: str, prt: int) -> None: + """* + * @brief Initialize the struct as an ipv4 address/port combo. + * @param addr IPv4 address string + * @param prt port number + + """ + pass + + def ipv4_ptr(self) -> struct sockaddr_in: + """* + * @brief Gives access to IPv4 sockaddr structure (sockaddr_in) for use + * with low level socket calls like sendto / recvfrom. + * @return *sockaddr_in pointer to ipv4 data structure + + """ + pass + + def ipv6_ptr(self) -> struct sockaddr_in6: + """* + * @brief Gives access to IPv6 sockaddr structure (sockaddr_in6) for use + * with low level socket calls like sendto / recvfrom. + * @return *sockaddr_in6 pointer to ipv6 data structure + + """ + pass + + def update(self) -> None: + """* + * @brief Will update address and port based on the curent data in raw. + + """ + pass + + @overload + def from_sockaddr(self, source_address: struct sockaddr_storage) -> None: + """* + * @brief Fill this Info from the provided sockaddr struct. + * @param &source_address sockaddr info filled out by recvfrom. + + """ + pass + + @overload + def from_sockaddr(self, source_address: struct sockaddr_in) -> None: + """* + * @brief Fill this Info from the provided sockaddr struct. + * @param &source_address sockaddr info filled out by recvfrom. + + """ + pass + + @overload + def from_sockaddr(self, source_address: struct sockaddr_in6) -> None: + """* + * @brief Fill this Info from the provided sockaddr struct. + * @param &source_address sockaddr info filled out by recvfrom. + + """ + pass + def __init__(self, address: str = "", port: int = int()) -> None: + """Auto-generated default constructor with named params""" + pass + + + + + + + @overload + def is_valid(self) -> bool: + """* + * @brief Is the socket valid. + * @return True if the socket file descriptor is >= 0. + + """ + pass + + @staticmethod + @overload + def is_valid(socket_fd: int) -> bool: + """* + * @brief Is the socket valid. + * @param socket_fd Socket file descriptor. + * @return True if the socket file descriptor is >= 0. + + """ + pass + + def get_ipv4_info(self) -> Optional[Socket.Info]: + """* + * @brief Get the Socket::Info for the socket. + * @details This will call getsockname() on the socket to get the + * sockaddr_storage structure, and then fill out the Socket::Info + * structure. + * @return Socket::Info for the socket. + + """ + pass + + def set_receive_timeout(self, timeout: std.chrono.duration) -> bool: + """* + * @brief Set the receive timeout on the provided socket. + * @param timeout requested timeout, must be > 0. + * @return True if SO_RECVTIMEO was successfully set. + + """ + pass + + def enable_reuse(self) -> bool: + """* + * @brief Allow others to use this address/port combination after we're done + * with it. + * @return True if SO_REUSEADDR and SO_REUSEPORT were successfully set. + + """ + pass + + def make_multicast(self, time_to_live: int = 1, loopback_enabled: int = True) -> bool: + """* + * @brief Configure the socket to be multicast (if time_to_live > 0). + * Sets the IP_MULTICAST_TTL (number of multicast hops allowed) and + * optionally configures whether this node should receive its own + * multicast packets (IP_MULTICAST_LOOP). + * @param time_to_live number of multicast hops allowed (TTL). + * @param loopback_enabled Whether to receive our own multicast packets. + * @return True if IP_MULTICAST_TTL and IP_MULTICAST_LOOP were set. + + """ + pass + + def add_multicast_group(self, multicast_group: str) -> bool: + """* + * @brief If this is a server socket, add it to the provided the multicast + * group. + * + * @note Multicast groups must be Class D addresses (224.0.0.0 to + * 239.255.255.255) + * + * See https://en.wikipedia.org/wiki/Multicast_address for more + * information. + * @param multicast_group multicast group to join. + * @return True if IP_ADD_MEMBERSHIP was successfully set. + + """ + pass + + def select(self, timeout: std.chrono.microseconds) -> int: + """* + * @brief Select on the socket for read events. + * @param timeout how long to wait for an event. + * @return number of events that occurred. + + """ + pass + + def __init__(self) -> None: + """Auto-generated default constructor""" + pass + +# namespace espp + +#################### #################### + + +#################### #################### + + +# if we're on windows, we cannot include netinet/in.h and instead need to use +# winsock2.h + + +class TcpSocket: + """* + * @brief Class for managing sending and receiving data using TCP/IP. Can be + * used to create client or server sockets. + * + * \section tcp_ex1 TCP Client Example + * \snippet socket_example.cpp TCP Client example + * \section tcp_ex2 TCP Server Example + * \snippet socket_example.cpp TCP Server example + * + * \section tcp_ex3 TCP Client Response Example + * \snippet socket_example.cpp TCP Client Response example + * \section tcp_ex4 TCP Server Response Example + * \snippet socket_example.cpp TCP Server Response example + * + + """ + class Config: + """* + * @brief Config struct for the TCP socket. + + """ + log_level: espp.Logger.Verbosity = espp.Logger.Verbosity(espp.Logger.Verbosity.warn) #*< Verbosity level for the TCP socket logger. + def __init__( + self, + log_level: Logger.Verbosity = Logger.Verbosity(Logger.Verbosity.warn) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + class ConnectConfig: + """* + * @brief Config struct for connecting to a remote TCP server. + + """ + ip_address: str #*< Address to send data to. + port: int #*< Port number to send data to. + def __init__(self, ip_address: str = "", port: int = int()) -> None: + """Auto-generated default constructor with named params""" + pass + + class TransmitConfig: + """* + * @brief Config struct for sending data to a remote TCP socket. + * @note This is only used when waiting for a response from the remote. + + """ + wait_for_response: bool = False #*< Whether to wait for a response from the remote or not. + response_size: int = 0 #*< If waiting for a response, this is the maximum size response we will receive. + on_response_callback: espp.Socket.response_callback_fn = None #*< If waiting for a + response, this is an optional handler which is provided the response data. + response_timeout: std.chrono.duration = std.chrono.duration( + 0.5) #*< If waiting for a response, this is the maximum timeout to wait. + + @staticmethod + def default() -> TcpSocket.TransmitConfig: + pass + def __init__( + self, + wait_for_response: bool = False, + response_size: int = 0, + on_response_callback: Socket.response_callback_fn = None, + response_timeout: std.chrono.duration = std.chrono.duration( + 0.5) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + + + def reinit(self) -> None: + """* + * @brief Reinitialize the socket, cleaning it up if first it is already + * initalized. + + """ + pass + + def close(self) -> None: + """* + * @brief Close the socket. + + """ + pass + + def is_connected(self) -> bool: + """* + * @brief Check if the socket is connected to a remote endpoint. + * @return True if the socket is connected to a remote endpoint. + + """ + pass + + def connect(self, connect_config: TcpSocket.ConnectConfig) -> bool: + """* + * @brief Open a connection to the remote TCP server. + * @param connect_config ConnectConfig struct describing the server endpoint. + * @return True if the client successfully connected to the server. + + """ + pass + + def get_remote_info(self) -> Socket.Info: + """* + * @brief Get the remote endpoint info. + * @return The remote endpoint info. + + """ + pass + + @overload + def transmit( + self, + data: List[int], + transmit_config: TcpSocket.TransmitConfig = TcpSocket.TransmitConfig.Default() + ) -> bool: + """* + * @brief Send data to the endpoint already connected to by TcpSocket::connect. + * Can be configured to block waiting for a response from the remote. + * + * If response is requested, a callback can be provided in + * send_config which will be provided the response data for + * processing. + * @param data vector of bytes to send to the remote endpoint. + * @param transmit_config TransmitConfig struct indicating whether to wait for a + * response. + * @return True if the data was sent, False otherwise. + + """ + pass + + @overload + def transmit( + self, + data: List[char], + transmit_config: TcpSocket.TransmitConfig = TcpSocket.TransmitConfig.Default() + ) -> bool: + """* + * @brief Send data to the endpoint already connected to by TcpSocket::connect. + * Can be configured to block waiting for a response from the remote. + * + * If response is requested, a callback can be provided in + * send_config which will be provided the response data for + * processing. + * @param data vector of bytes to send to the remote endpoint. + * @param transmit_config TransmitConfig struct indicating whether to wait for a + * response. + * @return True if the data was sent, False otherwise. + + """ + pass + + @overload + def transmit( + self, + data: std.string_view, + transmit_config: TcpSocket.TransmitConfig = TcpSocket.TransmitConfig.Default() + ) -> bool: + """* + * @brief Send data to the endpoint already connected to by TcpSocket::connect. + * Can be configured to block waiting for a response from the remote. + * + * If response is requested, a callback can be provided in + * send_config which will be provided the response data for + * processing. + * @param data string view of bytes to send to the remote endpoint. + * @param transmit_config TransmitConfig struct indicating whether to wait for a + * response. + * @return True if the data was sent, False otherwise. + + """ + pass + + @overload + def receive(self, data: List[int], max_num_bytes: int) -> bool: + """* + * @brief Call read on the socket, assuming it has already been configured + * appropriately. + * + * @param data Vector of bytes of received data. + * @param max_num_bytes Maximum number of bytes to receive. + * @return True if successfully received, False otherwise. + + """ + pass + + @overload + def receive(self, data: int, max_num_bytes: int) -> int: + """* + * @brief Call read on the socket, assuming it has already been configured + * appropriately. + * @note This function will block until max_num_bytes are received or the + * receive timeout is reached. + * @note The data pointed to by data must be at least max_num_bytes in size. + * @param data Pointer to buffer to receive data. + * @param max_num_bytes Maximum number of bytes to receive. + * @return Number of bytes received. + + """ + pass + + def bind(self, port: int) -> bool: + """* + * @brief Bind the socket as a server on \p port. + * @param port The port to which to bind the socket. + * @return True if the socket was bound. + + """ + pass + + def listen(self, max_pending_connections: int) -> bool: + """* + * @brief Listen for incoming client connections. + * @note Must be called after bind and before accept. + * @see bind + * @see accept + * @param max_pending_connections Max number of allowed pending connections. + * @return True if socket was able to start listening. + + """ + pass + + def accept(self) -> TcpSocket: + """* + * @brief Accept an incoming connection. + * @note Blocks until a connection is accepted. + * @note Must be called after listen. + * @note This function will block until a connection is accepted. + * @return A unique pointer to a TcpClientSession if a connection was + * accepted, None otherwise. + + """ + pass + + def __init__(self) -> None: + """Auto-generated default constructor""" + pass + +#################### #################### + + +#################### #################### + + + +# TODO: should this class _contain_ a socket or just create sockets within each +# call? + +class UdpSocket: + """* + * @brief Class for managing sending and receiving data using UDP/IP. Can be + * used to create client or server sockets. + * + * See + * https://github.com/espressif/esp-idf/tree/master/examples/protocols/sockets/udp_multicast + * for more information on udp multicast sockets. + * + * \section udp_ex1 UDP Client Example + * \snippet socket_example.cpp UDP Client example + * \section udp_ex2 UDP Server Example + * \snippet socket_example.cpp UDP Server example + * + * \section udp_ex3 UDP Client Response Example + * \snippet socket_example.cpp UDP Client Response example + * \section udp_ex4 UDP Server Response Example + * \snippet socket_example.cpp UDP Server Response example + * + * \section udp_ex5 UDP Multicast Client Example + * \snippet socket_example.cpp UDP Multicast Client example + * \section udp_ex6 UDP Multicast Server Example + * \snippet socket_example.cpp UDP Multicast Server example + * + + """ + class ReceiveConfig: + port: int #*< Port number to bind to / receive from. + buffer_size: int #*< Max size of data we can receive at one time. + is_multicast_endpoint: bool = bool(False) #*< Whether this should be a multicast endpoint. + multicast_group: str = str("") #*< If this is a multicast endpoint, this is the group it belongs to. + on_receive_callback: espp.Socket.receive_callback_fn = espp.Socket.receive_callback_fn(None) #*< Function containing business logic to handle data received. + def __init__( + self, + port: int = int(), + buffer_size: int = int(), + is_multicast_endpoint: bool = bool(False), + multicast_group: str = str(""), + on_receive_callback: Socket.receive_callback_fn = Socket.receive_callback_fn(None) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + class SendConfig: + ip_address: str #*< Address to send data to. + port: int #*< Port number to send data to. + is_multicast_endpoint: bool = bool(False) #*< Whether this should be a multicast endpoint. + wait_for_response: bool = bool(False) #*< Whether to wait for a response from the remote or not. + response_size: int = int(0) #*< If waiting for a response, this is the maximum size response we will receive. + on_response_callback: espp.Socket.response_callback_fn = espp.Socket.response_callback_fn(None) #*< If waiting for a response, this is an optional handler which is provided the + response data. + response_timeout: std.chrono.duration = std.chrono.duration( + 0.5) #*< If waiting for a response, this is the maximum timeout to wait. + def __init__( + self, + ip_address: str = "", + port: int = int(), + is_multicast_endpoint: bool = bool(False), + wait_for_response: bool = bool(False), + response_size: int = int(0), + on_response_callback: Socket.response_callback_fn = Socket.response_callback_fn(None), + response_timeout: std.chrono.duration = std.chrono.duration( + 0.5) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + class Config: + log_level: espp.Logger.Verbosity = espp.Logger.Verbosity(espp.Logger.Verbosity.warn) #*< Verbosity level for the UDP socket logger. + def __init__( + self, + log_level: Logger.Verbosity = Logger.Verbosity(Logger.Verbosity.warn) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + + + @overload + def send(self, data: List[int], send_config: UdpSocket.SendConfig) -> bool: + """* + * @brief Send data to the endpoint specified by the send_config. + * Can be configured to multicast (within send_config) and can be + * configured to block waiting for a response from the remote. + * + * @note in the case of multicast, it will block only until the first + * response. + * + * If response is requested, a callback can be provided in + * send_config which will be provided the response data for + * processing. + * @param data vector of bytes to send to the remote endpoint. + * @param send_config SendConfig struct indicating where to send and whether + * to wait for a response. + * @return True if the data was sent, False otherwise. + + """ + pass + + @overload + def send(self, data: std.string_view, send_config: UdpSocket.SendConfig) -> bool: + """* + * @brief Send data to the endpoint specified by the send_config. + * Can be configured to multicast (within send_config) and can be + * configured to block waiting for a response from the remote. + * + * @note in the case of multicast, it will block only until the first + * response. + * + * If response is requested, a callback can be provided in + * send_config which will be provided the response data for + * processing. + * @param data String view of bytes to send to the remote endpoint. + * @param send_config SendConfig struct indicating where to send and whether + * to wait for a response. + * @return True if the data was sent, False otherwise. + + """ + pass + + def receive( + self, + max_num_bytes: int, + data: List[int], + remote_info: Socket.Info + ) -> bool: + """* + * @brief Call recvfrom on the socket, assuming it has already been + * configured appropriately. + * + * @param max_num_bytes Maximum number of bytes to receive. + * @param data Vector of bytes of received data. + * @param remote_info Socket::Info containing the sender's information. This + * will be populated with the information about the sender. + * @return True if successfully received, False otherwise. + + """ + pass + + def start_receiving( + self, + task_config: Task.Config, + receive_config: UdpSocket.ReceiveConfig + ) -> bool: + """* + * @brief Configure a server socket and start a thread to continuously + * receive and handle data coming in on that socket. + * + * @param task_config Task::Config struct for configuring the receive task. + * @param receive_config ReceiveConfig struct with socket and callback info. + * @return True if the socket was created and task was started, False otherwise. + + """ + pass + + def __init__(self) -> None: + """Auto-generated default constructor""" + pass + +#################### #################### + + +#################### #################### + + + + + +class Task: + """* + * @brief Task provides an abstraction over std::thread which optionally + * includes memory / priority configuration on ESP systems. It allows users to + * easily stop the task, and will automatically stop itself if destroyed. + * + * There is also a utility function which can be used to get the info for the + * task of the current context, or for a provided Task object. + * + * There is also a helper function to run a lambda on a specific core, which can + * be used to run a specific function on a specific core, as you might want to + * do when registering an interrupt driver on a specific core. + * + * \section task_ex1 Basic Task Example + * \snippet task_example.cpp Task example + * \section task_ex2 Many Task Example + * \snippet task_example.cpp ManyTask example + * \section task_ex3 Long Running Task Example + * \snippet task_example.cpp LongRunningTask example + * \section task_ex4 Task Info Example + * \snippet task_example.cpp Task Info example + * \section task_ex5 Task Request Stop Example + * \snippet task_example.cpp Task Request Stop example + * + * \section run_on_core_ex1 Run on Core Example + * \snippet task_example.cpp run on core example + + """ + + + class BaseConfig: + """* + * @brief Base configuration struct for the Task. + * @note This is designed to be used as a configuration struct in other classes + * that may have a Task as a member. + + """ + name: str #*< Name of the task + stack_size_bytes: int = int(4096) #*< Stack Size (B) allocated to the task. + priority: int = int(0) #*< Priority of the task, 0 is lowest priority on ESP / FreeRTOS. + core_id: int = int(-1) #*< Core ID of the task, -1 means it is not pinned to any core. + def __init__( + self, + name: str = "", + stack_size_bytes: int = int(4096), + priority: int = int(0), + core_id: int = int(-1) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + class Config: + """* + * @brief Configuration struct for the Task. + * @note This is the recommended way to configure the Task, and allows you to + * use the condition variable and mutex from the task to wait_for and + * wait_until. + * @note This is an older configuration struct, and is kept for backwards + * compatibility. It is recommended to use the AdvancedConfig struct + * instead. + + """ + name: str #*< Name of the task + callback: espp.Task.callback_fn #*< Callback function + stack_size_bytes: int = int(4096) #*< Stack Size (B) allocated to the task. + priority: int = int(0) #*< Priority of the task, 0 is lowest priority on ESP / FreeRTOS. + core_id: int = int(-1) #*< Core ID of the task, -1 means it is not pinned to any core. + log_level: espp.Logger.Verbosity = espp.Logger.Verbosity(espp.Logger.Verbosity.warn) #*< Log verbosity for the task. + def __init__( + self, + name: str = "", + callback: Task.callback_fn = Task.callback_fn(), + stack_size_bytes: int = int(4096), + priority: int = int(0), + core_id: int = int(-1), + log_level: Logger.Verbosity = Logger.Verbosity(Logger.Verbosity.warn) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + class SimpleConfig: + """* + * @brief Simple configuration struct for the Task. + * @note This is useful for when you don't need to use the condition variable + * or mutex in the callback. + + """ + callback: espp.Task.simple_callback_fn #*< Callback function + task_config: espp.Task.BaseConfig #*< Base configuration for the task. + log_level: espp.Logger.Verbosity = espp.Logger.Verbosity(espp.Logger.Verbosity.warn) #*< Log verbosity for the task. + def __init__( + self, + callback: Task.simple_callback_fn = Task.simple_callback_fn(), + task_config: Task.BaseConfig = Task.BaseConfig(), + log_level: Logger.Verbosity = Logger.Verbosity(Logger.Verbosity.warn) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + class AdvancedConfig: + """* + * @brief Advanced configuration struct for the Task. + * @note This is the recommended way to configure the Task, and allows you to + * use the condition variable and mutex from the task to wait_for and + * wait_until. + + """ + callback: espp.Task.callback_fn #*< Callback function + task_config: espp.Task.BaseConfig #*< Base configuration for the task. + log_level: espp.Logger.Verbosity = espp.Logger.Verbosity(espp.Logger.Verbosity.warn) #*< Log verbosity for the task. + def __init__( + self, + callback: Task.callback_fn = Task.callback_fn(), + task_config: Task.BaseConfig = Task.BaseConfig(), + log_level: Logger.Verbosity = Logger.Verbosity(Logger.Verbosity.warn) + ) -> None: + """Auto-generated default constructor with named params""" + pass + + + + + @staticmethod + @overload + def make_unique(config: Task.Config) -> Task: + """* + * @brief Get a unique pointer to a new task created with \p config. + * Useful to not have to use templated std::make_unique (less typing). + * @param config Config struct to initialize the Task with. + * @return std::unique_ptr pointer to the newly created task. + + """ + pass + + @staticmethod + @overload + def make_unique(config: Task.SimpleConfig) -> Task: + """* + * @brief Get a unique pointer to a new task created with \p config. + * Useful to not have to use templated std::make_unique (less typing). + * @param config SimpleConfig struct to initialize the Task with. + * @return std::unique_ptr pointer to the newly created task. + + """ + pass + + @staticmethod + @overload + def make_unique(config: Task.AdvancedConfig) -> Task: + """* + * @brief Get a unique pointer to a new task created with \p config. + * Useful to not have to use templated std::make_unique (less typing). + * @param config AdvancedConfig struct to initialize the Task with. + * @return std::unique_ptr pointer to the newly created task. + + """ + pass + + + def start(self) -> bool: + """* + * @brief Start executing the task. + * + * @return True if the task started, False if it was already started. + + """ + pass + + def stop(self) -> bool: + """* + * @brief Stop the task execution, blocking until it stops. + * + * @return True if the task stopped, False if it was not started / already + * stopped. + + """ + pass + + def is_started(self) -> bool: + """* + * @brief Has the task been started or not? + * + * @return True if the task is started / running, False otherwise. + + """ + pass + + def is_running(self) -> bool: + """* + * @brief Is the task running? + * + * @return True if the task is running, False otherwise. + + """ + pass + + + def __init__(self) -> None: + """Auto-generated default constructor""" + pass + +# namespace espp + +#################### #################### + + +#################### #################### + + + +class Timer: + """/ @brief A timer that can be used to schedule tasks to run at a later time. + / @details A timer can be used to schedule a task to run at a later time. + / The timer will run in the background and will call the task when + / the time is up. The timer can be canceled at any time. A timer + / can be configured to run once or to repeat. + / + / The timer uses a task to run in the background. The task will + / sleep until the timer is ready to run. When the timer is ready to + / run, the task will call the callback function. The callback + / function can return True to cancel the timer or False to keep the + / timer running. If the timer is configured to repeat, then the + / callback function will be called again after the period has + / elapsed. If the timer is configured to run once, then the + / callback function will only be called once. + / + / The timer can be configured to start automatically when it is + / constructed. If the timer is not configured to start + / automatically, then the timer can be started by calling start(). + / The timer can be canceled at any time by calling cancel(). + / + / @note The timer uses a task to run in the background, so the timer + / callback function will be called in the context of the task. The + / timer callback function should not block for a long time because it + / will block the task. If the timer callback function blocks for a + / long time, then the timer will not be able to keep up with the + / period. + / + / \section timer_ex1 Timer Example 1 + / \snippet timer_example.cpp timer example + / \section timer_ex2 Timer Delay Example + / \snippet timer_example.cpp timer delay example + / \section timer_ex3 Oneshot Timer Example + / \snippet timer_example.cpp timer oneshot example + / \section timer_ex4 Timer Cancel Itself Example + / \snippet timer_example.cpp timer cancel itself example + / \section timer_ex5 Oneshot Timer Cancel Itself Then Start again with Delay Example + / \snippet timer_example.cpp timer oneshot restart example + / \section timer_ex6 Timer Update Period Example + / \snippet timer_example.cpp timer update period example + """ + + class Config: + """/ @brief The configuration for the timer.""" + name: std.string_view #/< The name of the timer. + period: std.chrono.duration #/< The period of the timer. If 0, the timer callback will only be called once. + delay: std.chrono.duration = std.chrono.duration( + 0) #/< The delay before the first execution of the timer callback after start() is called. + callback: espp.Timer.callback_fn #/< The callback function to call when the timer expires. + auto_start: bool = bool(True) #/< If True, the timer will start automatically when constructed. + stack_size_bytes: int = int(4096) #/< The stack size of the task that runs the timer. + priority: int = int(0) #/< Priority of the timer, 0 is lowest priority on ESP / FreeRTOS. + core_id: int = int(-1) #/< Core ID of the timer, -1 means it is not pinned to any core. + log_level: espp.Logger.Verbosity = espp.Logger.Verbosity.warn #/< The log level for the timer. + def __init__( + self, + name: std.string_view = std.string_view(), + period: std.chrono.duration = std.chrono.duration(), + delay: std.chrono.duration = std.chrono.duration( + 0), + callback: Timer.callback_fn = Timer.callback_fn(), + auto_start: bool = bool(True), + stack_size_bytes: int = int(4096), + priority: int = int(0), + core_id: int = int(-1), + log_level: Logger.Verbosity = Logger.Verbosity.warn + ) -> None: + """Auto-generated default constructor with named params""" + pass + + + + @overload + def start(self) -> None: + """/ @brief Start the timer. + / @details Starts the timer. Does nothing if the timer is already running. + """ + pass + + @overload + def start(self, delay: std.chrono.duration) -> None: + """/ @brief Start the timer with a delay. + / @details Starts the timer with a delay. If the timer is already running, + / this will cancel the timer and start it again with the new + / delay. If the timer is not running, this will start the timer + / with the delay. Overwrites any previous delay that might have + / been set. + / @param delay The delay before the first execution of the timer callback. + """ + pass + + def stop(self) -> None: + """/ @brief Stop the timer, same as cancel(). + / @details Stops the timer, same as cancel(). + """ + pass + + def cancel(self) -> None: + """/ @brief Cancel the timer. + / @details Cancels the timer. + """ + pass + + def set_period(self, period: std.chrono.duration) -> None: + """/ @brief Set the period of the timer. + / @details Sets the period of the timer. + / @param period The period of the timer. + / @note If the period is 0, the timer will run once. + / @note If the period is negative, the period will not be set / updated. + / @note If the timer is running, the period will be updated after the + / current period has elapsed. + """ + pass + + def is_running(self) -> bool: + """/ @brief Check if the timer is running. + / @details Checks if the timer is running. + / @return True if the timer is running, False otherwise. + """ + pass + + def __init__(self) -> None: + """Auto-generated default constructor""" + pass + +#################### #################### + +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! AUTOGENERATED CODE END !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! diff --git a/lib/python_bindings/espp/py.typed b/lib/python_bindings/espp/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/lib/python_bindings/module.cpp b/lib/python_bindings/module.cpp new file mode 100644 index 000000000..efc2be1ff --- /dev/null +++ b/lib/python_bindings/module.cpp @@ -0,0 +1,20 @@ +#include + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +namespace py = pybind11; + +void py_init_module_espp(py::module &m); + +// This builds the native python module `espp` +// it will be wrapped in a standard python module `espp` +PYBIND11_MODULE(espp, m) { +#ifdef VERSION_INFO + m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); +#else + m.attr("__version__") = "dev"; +#endif + + py_init_module_espp(m); +} diff --git a/lib/python_bindings/pybind_espp.cpp b/lib/python_bindings/pybind_espp.cpp new file mode 100644 index 000000000..768d43353 --- /dev/null +++ b/lib/python_bindings/pybind_espp.cpp @@ -0,0 +1,1710 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "espp.hpp" + +namespace py = pybind11; + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! AUTOGENERATED CODE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// // Autogenerated code below! Do not edit! + +// // Autogenerated code end +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! AUTOGENERATED CODE END !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +void py_init_module_espp(py::module &m) { + // using namespace espp; // NON! + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! AUTOGENERATED CODE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // // Autogenerated code below! Do not edit! + //////////////////// //////////////////// + auto pyClassBaseComponent = + py::class_(m, "BaseComponent", py::dynamic_attr(), + "/ Base class for all components\n/ Provides a logger and " + "some basic logging configuration") + .def("get_name", &espp::BaseComponent::get_name, + "/ Get the name of the component\n/ \\return A const reference to the name of the " + "component\n/ \note This is the tag of the logger") + .def("set_log_tag", &espp::BaseComponent::set_log_tag, py::arg("tag"), + "/ Set the tag for the logger\n/ \\param tag The tag to use for the logger") + .def("get_log_level", &espp::BaseComponent::get_log_level, + "/ Get the log level for the logger\n/ \\return The verbosity level of the " + "logger\n/ \\sa Logger::Verbosity\n/ \\sa Logger::set_verbosity") + .def("set_log_level", &espp::BaseComponent::set_log_level, py::arg("level"), + "/ Set the log level for the logger\n/ \\param level The verbosity level to use for " + "the logger\n/ \\sa Logger::Verbosity\n/ \\sa Logger::set_verbosity") + .def("set_log_verbosity", &espp::BaseComponent::set_log_verbosity, py::arg("level"), + "/ Set the log verbosity for the logger\n/ \\param level The verbosity level to use " + "for the logger\n/ \note This is a convenience method that calls set_log_level\n/ " + "\\sa set_log_level\n/ \\sa Logger::Verbosity\n/ \\sa Logger::set_verbosity") + .def("get_log_verbosity", &espp::BaseComponent::get_log_verbosity, + "/ Get the log verbosity for the logger\n/ \\return The verbosity level of the " + "logger\n/ \note This is a convenience method that calls get_log_level\n/ \\sa " + "get_log_level\n/ \\sa Logger::Verbosity\n/ \\sa Logger::get_verbosity") + .def("set_log_rate_limit", &espp::BaseComponent::set_log_rate_limit, + py::arg("rate_limit"), + "/ Set the rate limit for the logger\n/ \\param rate_limit The rate limit to use " + "for the logger\n/ \note Only calls to the logger that have _rate_limit suffix will " + "be rate limited\n/ \\sa Logger::set_rate_limit"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassRgb = + py::class_(m, "Rgb", py::dynamic_attr(), + "*\n * @brief Class representing a color using RGB color space.\n") + .def_readwrite("r", &espp::Rgb::r, "/< Red value ∈ [0, 1]") + .def_readwrite("g", &espp::Rgb::g, "/< Green value ∈ [0, 1]") + .def_readwrite("b", &espp::Rgb::b, "/< Blue value ∈ [0, 1]") + .def(py::init<>()) + .def(py::init(), py::arg("r"), py::arg("g"), + py::arg("b"), + "*\n * @brief Construct an Rgb object from the provided rgb values.\n * @note " + "If provided values outside the range [0,1], it will rescale them to\n * be " + "within the range [0,1] by dividing by 255.\n * @param r Floating point value for " + "the red channel, should be in range [0,\n * 1]\n * @param g Floating " + "point value for the green channel, should be in range\n * [0, 1]\n * " + "@param b Floating point value for the blue channel, should be in range\n * " + " [0, 1]\n") + .def(py::init(), py::arg("rgb"), + "*\n * @brief Copy-construct an Rgb object from the provided object.\n * @note " + "If provided values outside the range [0,1], it will rescale them to\n * be " + "within the range [0,1] by dividing by 255.\n * @param rgb Rgb struct containing " + "the values to copy.\n") + .def(py::init(), py::arg("hsv"), + "*\n * @brief Construct an Rgb object from the provided Hsv object.\n * @note " + "This calls hsv.rgb() on the provided object, which means that invalid\n * " + "HSV data (not in the ranges [0,360], [0,1], and [0,1]) could lead to\n * " + "bad RGB data. The Rgb constructor will automatically convert the\n * " + "values to be in the proper range, but the perceived color will be\n * " + "changed.\n * @param hsv Hsv object to copy.\n") + .def(py::init(), py::arg("hex"), + "*\n * @brief Construct an Rgb object from the provided hex value.\n * @param " + "hex Hex value to convert to RGB. The hex value should be in the\n * " + "format 0xRRGGBB.\n") + .def("__add__", &espp::Rgb::operator+, py::arg("rhs"), + "*\n * @brief Perform additive color blending (averaging)\n * @param rhs Other " + "color to add to this color to create the resultant color\n * @return Resultant " + "color from blending this color with the \\p rhs color.\n") + .def("__iadd__", &espp::Rgb::operator+=, py::arg("rhs"), + "*\n * @brief Perform additive color blending (averaging)\n * @param rhs Other " + "color to add to this color\n") + .def("__eq__", &espp::Rgb::operator==, py::arg("rhs")) + .def("__ne__", &espp::Rgb::operator!=, py::arg("rhs")) + .def("hsv", &espp::Rgb::hsv, + "*\n * @brief Get a HSV representation of this RGB color.\n * @return An HSV " + "object containing the HSV representation.\n") + .def("hex", &espp::Rgb::hex, + "*\n * @brief Get the hex representation of this RGB color.\n * @return The hex " + "representation of this RGB color.\n"); + + auto pyClassHsv = + py::class_(m, "Hsv", py::dynamic_attr(), + "*\n * @brief Class representing a color using HSV color space.\n") + .def_readwrite("h", &espp::Hsv::h, "/< Hue ∈ [0, 360]") + .def_readwrite("s", &espp::Hsv::s, "/< Saturation ∈ [0, 1]") + .def_readwrite("v", &espp::Hsv::v, "/< Value ∈ [0, 1]") + .def(py::init<>()) + .def(py::init(), py::arg("h"), py::arg("s"), + py::arg("v"), + "*\n * @brief Construct a Hsv object from the provided values.\n * @param h Hue " + "- will be clamped to be in range [0, 360]\n * @param s Saturation - will be " + "clamped to be in range [0, 1]\n * @param v Value - will be clamped to be in " + "range [0, 1]\n") + .def(py::init(), py::arg("hsv"), + "*\n * @brief Copy-construct the Hsv object\n * @param hsv Object to copy " + "from.\n") + .def(py::init(), py::arg("rgb"), + "*\n * @brief Construct Hsv object from Rgb object. Calls rgb.hsv() to perform\n " + " * the conversion.\n * @param rgb The Rgb object to convert and copy.\n") + .def("__eq__", &espp::Hsv::operator==, py::arg("rhs")) + .def("__ne__", &espp::Hsv::operator!=, py::arg("rhs")) + .def("rgb", &espp::Hsv::rgb, + "*\n * @brief Get a RGB representation of this HSV color.\n * @return An RGB " + "object containing the RGB representation.\n"); + + m.def("color_code", py::overload_cast(espp::color_code), py::arg("rgb"), + "\n(C++ auto return type)"); + + m.def("color_code", py::overload_cast(espp::color_code), py::arg("hsv"), + "\n(C++ auto return type)"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassEventManager = + py::class_( + m, "EventManager", py::dynamic_attr(), + "*\n * @brief Singleton class for managing events. Provides mechanisms for\n * " + "anonymous publish / subscribe interactions - enabling one to one,\n * one to " + "many, many to one, and many to many data distribution with\n * loose coupling " + "and low overhead. Each topic runs a thread for that\n * topic's subscribers, " + "executing all the callbacks in sequence and\n * then going to sleep again until " + "new data is published.\n *\n * @note In c++ objects, it's recommended to call the\n * " + " add_publisher/add_subscriber functions in the class constructor and\n * then " + "to call the remove_publisher/remove_subscriber functions in the\n * class " + "destructor.\n *\n * @note It is recommended (unless you are only interested in events " + "and not\n * data or are only needing to transmit actual strings) to use a\n * " + " serialization library (such as espp::serialization - which wraps\n * alpaca) to " + "serialize your data structures to string when publishing\n * and then deserialize " + "your data from string in the subscriber\n * callbacks.\n *\n * \\section " + "event_manager_ex1 Event Manager Example\n * \\snippet event_manager_example.cpp event " + "manager example\n") + .def_static("get", &espp::EventManager::get, + "*\n * @brief Get the singleton instance of the EventManager.\n * " + "@return A reference to the EventManager singleton.\n", + pybind11::return_value_policy::reference) + .def("add_publisher", &espp::EventManager::add_publisher, py::arg("topic"), + py::arg("component"), + "*\n * @brief Register a publisher for \\p component on \\p topic.\n * @param " + "topic Topic name for the data being published.\n * @param component Name of the " + "component publishing data.\n * @return True if the publisher was added, False if " + "it was already\n * registered for that component.\n") + .def("add_subscriber", + py::overload_cast( + &espp::EventManager::add_subscriber), + py::arg("topic"), py::arg("component"), py::arg("callback"), + py::arg("stack_size_bytes") = 8192, + "*\n * @brief Register a subscriber for \\p component on \\p topic.\n * @param " + "topic Topic name for the data being subscribed to.\n * @param component Name of " + "the component publishing data.\n * @param callback The event_callback_fn to be " + "called when receicing data on\n * \\p topic.\n * @param " + "stack_size_bytes The stack size in bytes to use for the subscriber\n * @note The " + "stack size is only used if a subscriber is not already registered\n * for " + "that topic. If a subscriber is already registered for that topic,\n * the " + "stack size is ignored.\n * @return True if the subscriber was added, False if it " + "was already\n * registered for that component.\n") + .def("add_subscriber", + py::overload_cast( + &espp::EventManager::add_subscriber), + py::arg("topic"), py::arg("component"), py::arg("callback"), py::arg("task_config"), + "*\n * @brief Register a subscriber for \\p component on \\p topic.\n * @param " + "topic Topic name for the data being subscribed to.\n * @param component Name of " + "the component publishing data.\n * @param callback The event_callback_fn to be " + "called when receicing data on\n * \\p topic.\n * @param task_config The " + "task configuration to use for the subscriber.\n * @note The task_config is only " + "used if a subscriber is not already\n * registered for that topic. If a " + "subscriber is already registered for\n * that topic, the task_config is " + "ignored.\n * @return True if the subscriber was added, False if it was already\n " + " * registered for that component.\n") + .def("publish", &espp::EventManager::publish, py::arg("topic"), py::arg("data"), + "*\n * @brief Publish \\p data on \\p topic.\n * @param topic Topic to publish " + "data on.\n * @param data Data to publish, within a vector container.\n * " + "@return True if \\p data was successfully published to \\p topic, False\n * " + " otherwise. Publish will not occur (and will return False) if\n * " + "there are no subscribers for this topic.\n") + .def("remove_publisher", &espp::EventManager::remove_publisher, py::arg("topic"), + py::arg("component"), + "*\n * @brief Remove \\p component's publisher for \\p topic.\n * @param topic " + "The topic that \\p component was publishing on.\n * @param component The " + "component for which the publisher was registered.\n * @return True if the " + "publisher was removed, False if it was not\n * registered.\n") + .def("remove_subscriber", &espp::EventManager::remove_subscriber, py::arg("topic"), + py::arg("component"), + "*\n * @brief Remove \\p component's subscriber for \\p topic.\n * @param topic " + "The topic that \\p component was subscribing to.\n * @param component The " + "component for which the subscriber was registered.\n * @return True if the " + "subscriber was removed, False if it was not\n * registered.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassFtpServer = + py::class_(m, "FtpServer", py::dynamic_attr(), + "/ \\brief A class that implements a FTP server.") + .def(py::init(), + py::arg("ip_address"), py::arg("port"), py::arg("root"), + "/ \\brief A class that implements a FTP server.\n/ \note The IP Address is not " + "currently used to select the right\n/ interface, but is instead passed to " + "the FtpClientSession so that\n/ it can be used in the PASV command.\n/ " + "\\param ip_address The IP address to listen on.\n/ \\param port The port to listen " + "on.\n/ \\param root The root directory of the FTP server.") + .def("start", &espp::FtpServer::start, + "/ \\brief Start the FTP server.\n/ Bind to the port and start accepting " + "connections.\n/ \\return True if the server was started, False otherwise.") + .def("stop", &espp::FtpServer::stop, "/ \\brief Stop the FTP server."); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassLogger = py::class_( + m, "Logger", py::dynamic_attr(), + "*\n * @brief Logger provides a wrapper around nicer / more robust formatting than\n * " + "standard ESP_LOG* macros with the ability to change the log level at\n * run-time. Logger " + "currently is a light wrapper around libfmt (future\n * std::format).\n *\n * To save on " + "code size, the logger has the ability to be compiled out based on\n * the log level set in " + "the sdkconfig. This means that if the log level is set to\n * ERROR, all debug, info, and " + "warn logs will be compiled out. This is done by\n * checking the log level at compile time " + "and only compiling in the functions\n * that are needed.\n *\n * \\section logger_ex1 Basic " + "Example\n * \\snippet logger_example.cpp Logger example\n * \\section logger_ex2 Threaded " + "Logging and Verbosity Example\n * \\snippet logger_example.cpp MultiLogger example\n"); + + { // inner classes & enums of Logger + py::enum_( + pyClassLogger, "Verbosity", py::arithmetic(), + "*\n * Verbosity levels for the logger, in order of increasing priority.\n") + .value("debug", espp::Logger::Verbosity::DEBUG, "*< Debug level verbosity.") + .value("info", espp::Logger::Verbosity::INFO, "*< Info level verbosity.") + .value("warn", espp::Logger::Verbosity::WARN, "*< Warn level verbosity.") + .value("error", espp::Logger::Verbosity::ERROR, "*< Error level verbosity.") + .value("none", espp::Logger::Verbosity::NONE, + "*< No verbosity - logger will not print anything."); + auto pyClassLogger_ClassConfig = + py::class_(pyClassLogger, "Config", py::dynamic_attr(), + "*\n * @brief Configuration struct for the logger.\n") + .def(py::init<>( + [](std::string_view tag = std::string_view(), bool include_time = {true}, + std::chrono::duration rate_limit = std::chrono::duration(0), + espp::Logger::Verbosity level = espp::Logger::Verbosity::WARN) { + auto r = std::make_unique(); + r->tag = tag; + r->include_time = include_time; + r->rate_limit = rate_limit; + r->level = level; + return r; + }), + py::arg("tag") = std::string_view(), py::arg("include_time") = bool{true}, + py::arg("rate_limit") = std::chrono::duration(0), + py::arg("level") = espp::Logger::Verbosity::WARN) + .def_readwrite("tag", &espp::Logger::Config::tag, + "*< The TAG that will be prepended to all logs.") + .def_readwrite("include_time", &espp::Logger::Config::include_time, + "*< Include the time in the log.") + .def_readwrite( + "rate_limit", &espp::Logger::Config::rate_limit, + "*< The rate limit for the logger. Optional, if <= 0 no\nrate limit. @note Only " + "calls that have _rate_limited suffixed will be rate limited.") + .def_readwrite("level", &espp::Logger::Config::level, + "*< The verbosity level for the logger."); + } // end of inner classes & enums of Logger + + pyClassLogger + .def("set_verbosity", &espp::Logger::set_verbosity, py::arg("level"), + "*\n * @brief Change the verbosity for the logger. \\sa Logger::Verbosity\n * " + "@param level new verbosity level\n") + .def("set_tag", &espp::Logger::set_tag, py::arg("tag"), + "*\n * @brief Change the tag for the logger.\n * @param tag The new tag.\n") + .def("get_tag", &espp::Logger::get_tag, + "*\n * @brief Get the current tag for the logger.\n * @return A const reference to " + "the current tag.\n") + .def("set_include_time", &espp::Logger::set_include_time, py::arg("include_time"), + "*\n * @brief Whether to include the time in the log.\n * @param include_time " + "Whether to include the time in the log.\n * @note The time is in seconds since boot " + "and is represented as a floating\n * point number with precision to the " + "millisecond.\n") + .def("set_rate_limit", &espp::Logger::set_rate_limit, py::arg("rate_limit"), + "*\n * @brief Change the rate limit for the logger.\n * @param rate_limit The new " + "rate limit.\n * @note Only calls that have _rate_limited suffixed will be rate " + "limited.\n") + .def("get_rate_limit", &espp::Logger::get_rate_limit, + "*\n * @brief Get the current rate limit for the logger.\n * @return The current " + "rate limit.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassBezier_espp_Vector2f = py::class_>( + m, "Bezier_espp_Vector2f", py::dynamic_attr(), + "*\n * @brief Implements rational / weighted and unweighted cubic bezier curves\n * " + "between control points.\n * @note See https://pomax.github.io/bezierinfo/ for information " + "on bezier\n * curves.\n * @note Template class which can be used individually on " + "floating point\n * values directly or on containers such as Vector2.\n * " + "@tparam T The type of the control points, e.g. float or Vector2.\n * @note The " + "bezier curve is defined by 4 control points, P0, P1, P2, P3.\n * The curve is defined " + "by the equation:\n * \\f$B(t) = (1-t)^3 * P0 + 3 * (1-t)^2 * t * P1 + 3 * (1-t) * t^2 " + "* P2 + t^3 * P3\\f$\n * where t is the evaluation parameter, [0, 1].\n *\n * @note The " + "weighted bezier curve is defined by 4 control points, P0, P1, P2, P3\n * and 4 " + "weights, W0, W1, W2, W3.\n * The curve is defined by the equation:\n * \\f$B(t) = " + "(W0 * (1-t)^3 * P0 + W1 * 3 * (1-t)^2 * t * P1 + W2 * 3 * (1-t) * t^2 * P2 + W3 *\n * t^3 * " + "P3) / (W0 + W1 + W2 + W3)\\f$ where t is the evaluation parameter, [0, 1].\n *\n * " + "\\section bezier_ex1 Example\n * \\snippet math_example.cpp bezier example\n"); + + { // inner classes & enums of Bezier_espp_Vector2f + auto pyClassBezier_ClassConfig = + py::class_::Config>( + pyClassBezier_espp_Vector2f, "Config", py::dynamic_attr(), + "*\n * @brief Unweighted cubic bezier configuration for 4 control points.\n") + .def(py::init<>()) // implicit default constructor + .def_readwrite("control_points", &espp::Bezier::Config::control_points, + "/< Array of 4 control points"); + auto pyClassBezier_ClassWeightedConfig = + py::class_::WeightedConfig>( + pyClassBezier_espp_Vector2f, "WeightedConfig", py::dynamic_attr(), + "*\n * @brief Weighted cubic bezier configuration for 4 control points with\n * " + " individual weights.\n") + .def(py::init<>()) // implicit default constructor + .def_readwrite("control_points", + &espp::Bezier::WeightedConfig::control_points, + "/< Array of 4 control points") + .def_readwrite("weights", &espp::Bezier::WeightedConfig::weights, + "/< Array of 4 weights, default is array of 1.0"); + } // end of inner classes & enums of Bezier_espp_Vector2f + + pyClassBezier_espp_Vector2f.def(py::init::Config &>()) + .def(py::init::WeightedConfig &>()) + .def("__call__", &espp::Bezier::operator(), py::arg("t"), + "*\n * @brief Evaluate the bezier at \\p t.\n * @note Convienience wrapper around " + "the at() method.\n * @param t The evaluation parameter, [0, 1].\n * @return The " + "bezier evaluated at \\p t.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + m.def("square", espp::square, py::arg("f"), + "*\n * @brief Simple square of the input.\n * @param f Value to square.\n * @return The " + "square of f (f*f).\n"); + + m.def("cube", espp::cube, py::arg("f"), + "*\n * @brief Simple cube of the input.\n * @param f Value to cube.\n * @return The cube " + "of f (f*f*f).\n"); + + m.def("fast_sqrt", espp::fast_sqrt, py::arg("value"), + "*\n * @brief Fast square root approximation.\n * @note Using " + "https://reprap.org/forum/read.php?147,219210 and\n * " + "https://en.wikipedia.org/wiki/Fast_inverse_square_root\n * @param value Value to take the " + "square root of.\n * @return Approximation of the square root of value.\n"); + + m.def("sgn", py::overload_cast(espp::sgn), py::arg("x"), + "*\n * @brief Get the sign of a number (+1, 0, or -1)\n * @param x Value to get the sign " + "of\n * @return Sign of x: -1 if x < 0, 0 if x == 0, or +1 if x > 0\n"); + m.def("sgn", py::overload_cast(espp::sgn), py::arg("x"), + "*\n * @brief Get the sign of a number (+1, 0, or -1)\n * @param x Value to get the sign " + "of\n * @return Sign of x: -1 if x < 0, 0 if x == 0, or +1 if x > 0\n"); + + m.def("lerp", espp::lerp, py::arg("a"), py::arg("b"), py::arg("t"), + "*\n * @brief Linear interpolation between two values.\n * @param a First value.\n * " + "@param b Second value.\n * @param t Interpolation factor in the range [0, 1].\n * @return " + "Linear interpolation between a and b.\n"); + + m.def("inv_lerp", espp::inv_lerp, py::arg("a"), py::arg("b"), py::arg("v"), + "*\n * @brief Compute the inverse lerped value.\n * @param a First value (usually the " + "lower of the two).\n * @param b Second value (usually the higher of the two).\n * @param " + "v Value to inverse lerp (usually a value between a and b).\n * @return Inverse lerp " + "value, the factor of v between a and b in the range [0,\n * 1] if v is between a " + "and b, 0 if v == a, or 1 if v == b. If a == b,\n * 0 is returned. If v is outside " + "the range [a, b], the value is\n * extrapolated linearly (i.e. if v < a, the " + "value is less than 0, if v\n * > b, the value is greater than 1).\n"); + + m.def( + "piecewise_linear", espp::piecewise_linear, py::arg("points"), py::arg("x"), + "*\n * @brief Compute the piecewise linear interpolation between a set of points.\n * @param " + "points Vector of points to interpolate between. The vector should be\n * " + "sorted by the first value in the pair. The first value in the\n * pair is the " + "x value and the second value is the y value. The x\n * values should be " + "unique. The function will interpolate between\n * the points using linear " + "interpolation. If x is less than the\n * first x value, the first y value is " + "returned. If x is greater\n * than the last x value, the last y value is " + "returned. If x is\n * between two x values, the y value is interpolated " + "between the\n * two y values.\n * @param x Value to interpolate at. Should be " + "a value from the first\n * distribution of the points (the domain). If x is " + "outside the domain\n * of the points, the value returned will be clamped to the " + "first or\n * last y value.\n * @return Interpolated value at x.\n"); + + m.def("round", espp::round, py::arg("x"), + "*\n * @brief Round x to the nearest integer.\n * @param x Floating point value to be " + "rounded.\n * @return Nearest integer to x.\n"); + + m.def("fast_ln", espp::fast_ln, py::arg("x"), + "*\n * @brief fast natural log function, ln(x).\n * @note This speed hack comes from:\n * " + " https://gist.github.com/LingDong-/7e4c4cae5cbbc44400a05ba650623\n * @param x Value to " + "take the natural log of.\n * @return ln(x)\n"); + + m.def("fast_sin", espp::fast_sin, py::arg("angle"), + "*\n * @brief Fast approximation of sin(angle) (radians).\n * @note \\p Angle must be in " + "the range [0, 2PI].\n * @param angle Angle in radians [0, 2*PI]\n * @return Approximation " + "of sin(value)\n"); + + m.def("fast_cos", espp::fast_cos, py::arg("angle"), + "*\n * @brief Fast approximation of cos(angle) (radians).\n * @note \\p Angle must be in " + "the range [0, 2PI].\n * @param angle Angle in radians [0, 2*PI]\n * @return Approximation " + "of cos(value)\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassGaussian = py::class_( + m, "Gaussian", py::dynamic_attr(), + "*\n * @brief Implements a gaussian function\n * " + "\\f$y(t)=\\alpha\\exp(-\\frac{(t-\\beta)^2}{2\\gamma^2})\\f$.\n * @details Alows you to " + "store the alpha, beta, and gamma coefficients as well\n * as update them " + "dynamically.\n *\n * \\section gaussian_ex1 Example\n * \\snippet math_example.cpp gaussian " + "example\n * \\section gaussian_ex2 Fade-In/Fade-Out Example\n * \\snippet math_example.cpp " + "gaussian fade in fade out example\n"); + + { // inner classes & enums of Gaussian + auto pyClassGaussian_ClassConfig = + py::class_( + pyClassGaussian, "Config", py::dynamic_attr(), + "*\n * @brief Configuration structure for initializing the gaussian.\n") + .def(py::init<>([](float gamma = float(), float alpha = {1.0f}, float beta = {0.5f}) { + auto r = std::make_unique(); + r->gamma = gamma; + r->alpha = alpha; + r->beta = beta; + return r; + }), + py::arg("gamma") = float(), py::arg("alpha") = float{1.0f}, + py::arg("beta") = float{0.5f}) + .def_readwrite( + "gamma", &espp::Gaussian::Config::gamma, + "/< Slope of the gaussian, range [0, 1]. 0 is more of a thin spike from 0 up to") + .def_readwrite("alpha", &espp::Gaussian::Config::alpha, + "/< Max amplitude of the gaussian output, defautls to 1.0.") + .def_readwrite( + "beta", &espp::Gaussian::Config::beta, + "/< Beta value for the gaussian, default to be symmetric at 0.5 in range [0,1].") + .def("__eq__", &espp::Gaussian::Config::operator==, py::arg("rhs")); + } // end of inner classes & enums of Gaussian + + pyClassGaussian + .def(py::init()) // implicit default constructor + .def("__call__", &espp::Gaussian::operator(), py::arg("t"), + "*\n * @brief Evaluate the gaussian at \\p t.\n * @note Convienience wrapper around " + "the at() method.\n * @param t The evaluation parameter, [0, 1].\n * @return The " + "gaussian evaluated at \\p t.\n") + .def("update", &espp::Gaussian::update, py::arg("config"), + "*\n * @brief Update the gaussian configuration.\n * @param config The new " + "configuration.\n") + .def("set_config", &espp::Gaussian::set_config, py::arg("config"), + "*\n * @brief Set the configuration of the gaussian.\n * @param config The new " + "configuration.\n") + .def("get_config", &espp::Gaussian::get_config, + "*\n * @brief Get the current configuration of the gaussian.\n * @return The " + "current configuration.\n") + .def("get_gamma", &espp::Gaussian::get_gamma, + "*\n * @brief Get the gamma value.\n * @return The gamma value.\n") + .def("get_alpha", &espp::Gaussian::get_alpha, + "*\n * @brief Get the alpha value.\n * @return The alpha value.\n") + .def("get_beta", &espp::Gaussian::get_beta, + "*\n * @brief Get the beta value.\n * @return The beta value.\n") + .def("set_gamma", &espp::Gaussian::set_gamma, py::arg("gamma"), + "*\n * @brief Set the gamma value.\n * @param gamma The new gamma value.\n") + .def("set_alpha", &espp::Gaussian::set_alpha, py::arg("alpha"), + "*\n * @brief Set the alpha value.\n * @param alpha The new alpha value.\n") + .def("set_beta", &espp::Gaussian::set_beta, py::arg("beta"), + "*\n * @brief Set the beta value.\n * @param beta The new beta value.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassRangeMapper_int = py::class_>( + m, "RangeMapper_int", py::dynamic_attr(), + "*\n * @brief Template class for converting a value from an uncentered [minimum,\n * " + "maximum] range into a centered output range (default [-1,1]). If\n * provided a " + "non-zero deadband, it will convert all values within\n * [center-deadband, " + "center+deadband] to be the configured\n * output_center (default 0).\n *\n * " + "The RangeMapper can be optionally configured to invert the input,\n * so that it " + "will compute the input w.r.t. the configured min/max of\n * the input range when " + "mapping to the output range - this will mean\n * that a values within the ranges " + "[minimum, minimum+deadband] and\n * [maximum-deadband, maximum] will all map to the " + "output_center and\n * the input center will map to both output_max and output_min\n " + "* depending on the sign of the input.\n *\n * @note When inverting the input range, " + "you are introducing a discontinuity\n * between the input distribution and the output " + "distribution at the\n * input center. Noise around the input's center value will " + "create\n * oscillations in the output which will jump between output maximum\n * " + " and output minimum. Therefore it is advised to use \\p invert_input\n * sparignly, " + "and to set the values robustly.\n *\n * The RangeMapper can be optionally configured " + "to invert the output,\n * so that after converting from the input range to the " + "output range,\n * it will flip the sign on the output.\n *\n * \\section " + "range_mapper_ex1 Example\n * \\snippet math_example.cpp range_mapper example\n"); + + { // inner classes & enums of RangeMapper_int + auto pyClassRangeMapper_ClassConfig = + py::class_::Config>( + pyClassRangeMapper_int, "Config", py::dynamic_attr(), + "*\n * @brief Configuration for the input uncentered range with optional\n * " + "values for the centered output range, default values of 0 output center\n * and 1 " + "output range provide a default output range between [-1, 1].\n") + .def(py::init<>([](int center = int(), int center_deadband = 0, int minimum = int(), + int maximum = int(), int range_deadband = 0, int output_center = 0, + int output_range = 1, bool invert_output = false) { + auto r = std::make_unique::Config>(); + r->center = center; + r->center_deadband = center_deadband; + r->minimum = minimum; + r->maximum = maximum; + r->range_deadband = range_deadband; + r->output_center = output_center; + r->output_range = output_range; + r->invert_output = invert_output; + return r; + }), + py::arg("center") = int(), py::arg("center_deadband") = 0, + py::arg("minimum") = int(), py::arg("maximum") = int(), + py::arg("range_deadband") = 0, py::arg("output_center") = 0, + py::arg("output_range") = 1, py::arg("invert_output") = false) + .def_readwrite("center", &espp::RangeMapper::Config::center, + "*< Center value for the input range.") + .def_readwrite("center_deadband", &espp::RangeMapper::Config::center_deadband, + "*< Deadband amount around (+-) the center for which output will be 0.") + .def_readwrite("minimum", &espp::RangeMapper::Config::minimum, + "*< Minimum value for the input range.") + .def_readwrite("maximum", &espp::RangeMapper::Config::maximum, + "*< Maximum value for the input range.") + .def_readwrite("range_deadband", &espp::RangeMapper::Config::range_deadband, + "*< Deadband amount around the minimum and maximum for which output " + "will\n be min/max output.") + .def_readwrite("output_center", &espp::RangeMapper::Config::output_center, + "*< The center for the output. Default 0.") + .def_readwrite( + "output_range", &espp::RangeMapper::Config::output_range, + "*< The range (+/-) from the center for the output. Default 1. @note Will\n " + " be passed through std::abs() to ensure it is positive.") + .def_readwrite("invert_output", &espp::RangeMapper::Config::invert_output, + "*< Whether to invert the output (default False). @note If True will " + "flip the sign\n of the output after converting from " + "the input distribution."); + } // end of inner classes & enums of RangeMapper_int + + pyClassRangeMapper_int.def(py::init::Config>()) + .def("get_center_deadband", &espp::RangeMapper::get_center_deadband, + "*\n * @brief Return the configured deadband around the center of the input\n * " + " distribution\n * @return Deadband around the center of the input distribution for " + "this\n * range mapper.\n") + .def("get_minimum", &espp::RangeMapper::get_minimum, + "*\n * @brief Return the configured minimum of the input distribution\n * @return " + "Minimum of the input distribution for this range mapper.\n") + .def("get_maximum", &espp::RangeMapper::get_maximum, + "*\n * @brief Return the configured maximum of the input distribution\n * @return " + "Maximum of the input distribution for this range mapper.\n") + .def( + "get_range", &espp::RangeMapper::get_range, + "*\n * @brief Return the configured range of the input distribution\n * @note Always " + "positive.\n * @return Range of the input distribution for this range mapper.\n") + .def("get_range_deadband", &espp::RangeMapper::get_range_deadband, + "*\n * @brief Return the configured deadband around the min/max of the input\n * " + " distribution\n * @return Deadband around the min/max of the input distribution " + "for this\n * range mapper.\n") + .def("get_output_center", &espp::RangeMapper::get_output_center, + "*\n * @brief Return the configured center of the output distribution\n * @return " + "Center of the output distribution for this range mapper.\n") + .def("get_output_range", &espp::RangeMapper::get_output_range, + "*\n * @brief Return the configured range of the output distribution\n * @note " + "Always positive.\n * @return Range of the output distribution for this range " + "mapper.\n") + .def("get_output_min", &espp::RangeMapper::get_output_min, + "*\n * @brief Return the configured minimum of the output distribution\n * @return " + "Minimum of the output distribution for this range mapper.\n") + .def("get_output_max", &espp::RangeMapper::get_output_max, + "*\n * @brief Return the configured maximum of the output distribution\n * @return " + "Maximum of the output distribution for this range mapper.\n") + .def("set_center_deadband", &espp::RangeMapper::set_center_deadband, py::arg("deadband"), + "*\n * @brief Set the deadband around the center of the input distribution.\n * " + "@param deadband The deadband to use around the center of the input\n * " + "distribution.\n * @note The deadband must be non-negative.\n * @note The deadband " + "is applied around the center value of the input\n * distribution.\n") + .def("set_range_deadband", &espp::RangeMapper::set_range_deadband, py::arg("deadband"), + "*\n * @brief Set the deadband around the min/max of the input distribution.\n * " + "@param deadband The deadband to use around the min/max of the input\n * " + "distribution.\n * @note The deadband must be non-negative.\n * @note The deadband " + "is applied around the min/max values of the input\n * distribution.\n") + .def("map", &espp::RangeMapper::map, py::arg("v"), + "*\n * @brief Map a value \\p v from the input distribution into the configured\n * " + " output range (centered, default [-1,1]).\n * @param v Value from the " + "(possibly uncentered and possibly inverted -\n * defined by the previously " + "configured Config) input distribution\n * @return Value within the centered output " + "distribution.\n") + .def("unmap", &espp::RangeMapper::unmap, py::arg("v"), + "*\n * @brief Unmap a value \\p v from the configured output range (centered,\n * " + " default [-1,1]) back into the input distribution.\n * @param T&v Value from the " + "centered output distribution.\n * @return Value within the input distribution.\n"); + auto pyClassRangeMapper_float = py::class_>( + m, "RangeMapper_float", py::dynamic_attr(), + "*\n * @brief Template class for converting a value from an uncentered [minimum,\n * " + "maximum] range into a centered output range (default [-1,1]). If\n * provided a " + "non-zero deadband, it will convert all values within\n * [center-deadband, " + "center+deadband] to be the configured\n * output_center (default 0).\n *\n * " + "The RangeMapper can be optionally configured to invert the input,\n * so that it " + "will compute the input w.r.t. the configured min/max of\n * the input range when " + "mapping to the output range - this will mean\n * that a values within the ranges " + "[minimum, minimum+deadband] and\n * [maximum-deadband, maximum] will all map to the " + "output_center and\n * the input center will map to both output_max and output_min\n " + "* depending on the sign of the input.\n *\n * @note When inverting the input range, " + "you are introducing a discontinuity\n * between the input distribution and the output " + "distribution at the\n * input center. Noise around the input's center value will " + "create\n * oscillations in the output which will jump between output maximum\n * " + " and output minimum. Therefore it is advised to use \\p invert_input\n * sparignly, " + "and to set the values robustly.\n *\n * The RangeMapper can be optionally configured " + "to invert the output,\n * so that after converting from the input range to the " + "output range,\n * it will flip the sign on the output.\n *\n * \\section " + "range_mapper_ex1 Example\n * \\snippet math_example.cpp range_mapper example\n"); + + { // inner classes & enums of RangeMapper_float + auto pyClassRangeMapper_ClassConfig = + py::class_::Config>( + pyClassRangeMapper_float, "Config", py::dynamic_attr(), + "*\n * @brief Configuration for the input uncentered range with optional\n * " + "values for the centered output range, default values of 0 output center\n * and 1 " + "output range provide a default output range between [-1, 1].\n") + .def(py::init<>([](float center = float(), float center_deadband = 0, + float minimum = float(), float maximum = float(), + float range_deadband = 0, float output_center = 0, + float output_range = 1, bool invert_output = false) { + auto r = std::make_unique::Config>(); + r->center = center; + r->center_deadband = center_deadband; + r->minimum = minimum; + r->maximum = maximum; + r->range_deadband = range_deadband; + r->output_center = output_center; + r->output_range = output_range; + r->invert_output = invert_output; + return r; + }), + py::arg("center") = float(), py::arg("center_deadband") = 0, + py::arg("minimum") = float(), py::arg("maximum") = float(), + py::arg("range_deadband") = 0, py::arg("output_center") = 0, + py::arg("output_range") = 1, py::arg("invert_output") = false) + .def_readwrite("center", &espp::RangeMapper::Config::center, + "*< Center value for the input range.") + .def_readwrite("center_deadband", &espp::RangeMapper::Config::center_deadband, + "*< Deadband amount around (+-) the center for which output will be 0.") + .def_readwrite("minimum", &espp::RangeMapper::Config::minimum, + "*< Minimum value for the input range.") + .def_readwrite("maximum", &espp::RangeMapper::Config::maximum, + "*< Maximum value for the input range.") + .def_readwrite("range_deadband", &espp::RangeMapper::Config::range_deadband, + "*< Deadband amount around the minimum and maximum for which output " + "will\n be min/max output.") + .def_readwrite("output_center", &espp::RangeMapper::Config::output_center, + "*< The center for the output. Default 0.") + .def_readwrite( + "output_range", &espp::RangeMapper::Config::output_range, + "*< The range (+/-) from the center for the output. Default 1. @note Will\n " + " be passed through std::abs() to ensure it is positive.") + .def_readwrite("invert_output", &espp::RangeMapper::Config::invert_output, + "*< Whether to invert the output (default False). @note If True will " + "flip the sign\n of the output after converting from " + "the input distribution."); + } // end of inner classes & enums of RangeMapper_float + + pyClassRangeMapper_float.def(py::init::Config>()) + .def("get_center_deadband", &espp::RangeMapper::get_center_deadband, + "*\n * @brief Return the configured deadband around the center of the input\n * " + " distribution\n * @return Deadband around the center of the input distribution for " + "this\n * range mapper.\n") + .def("get_minimum", &espp::RangeMapper::get_minimum, + "*\n * @brief Return the configured minimum of the input distribution\n * @return " + "Minimum of the input distribution for this range mapper.\n") + .def("get_maximum", &espp::RangeMapper::get_maximum, + "*\n * @brief Return the configured maximum of the input distribution\n * @return " + "Maximum of the input distribution for this range mapper.\n") + .def( + "get_range", &espp::RangeMapper::get_range, + "*\n * @brief Return the configured range of the input distribution\n * @note Always " + "positive.\n * @return Range of the input distribution for this range mapper.\n") + .def("get_range_deadband", &espp::RangeMapper::get_range_deadband, + "*\n * @brief Return the configured deadband around the min/max of the input\n * " + " distribution\n * @return Deadband around the min/max of the input distribution " + "for this\n * range mapper.\n") + .def("get_output_center", &espp::RangeMapper::get_output_center, + "*\n * @brief Return the configured center of the output distribution\n * @return " + "Center of the output distribution for this range mapper.\n") + .def("get_output_range", &espp::RangeMapper::get_output_range, + "*\n * @brief Return the configured range of the output distribution\n * @note " + "Always positive.\n * @return Range of the output distribution for this range " + "mapper.\n") + .def("get_output_min", &espp::RangeMapper::get_output_min, + "*\n * @brief Return the configured minimum of the output distribution\n * @return " + "Minimum of the output distribution for this range mapper.\n") + .def("get_output_max", &espp::RangeMapper::get_output_max, + "*\n * @brief Return the configured maximum of the output distribution\n * @return " + "Maximum of the output distribution for this range mapper.\n") + .def("set_center_deadband", &espp::RangeMapper::set_center_deadband, + py::arg("deadband"), + "*\n * @brief Set the deadband around the center of the input distribution.\n * " + "@param deadband The deadband to use around the center of the input\n * " + "distribution.\n * @note The deadband must be non-negative.\n * @note The deadband " + "is applied around the center value of the input\n * distribution.\n") + .def("set_range_deadband", &espp::RangeMapper::set_range_deadband, py::arg("deadband"), + "*\n * @brief Set the deadband around the min/max of the input distribution.\n * " + "@param deadband The deadband to use around the min/max of the input\n * " + "distribution.\n * @note The deadband must be non-negative.\n * @note The deadband " + "is applied around the min/max values of the input\n * distribution.\n") + .def("map", &espp::RangeMapper::map, py::arg("v"), + "*\n * @brief Map a value \\p v from the input distribution into the configured\n * " + " output range (centered, default [-1,1]).\n * @param v Value from the " + "(possibly uncentered and possibly inverted -\n * defined by the previously " + "configured Config) input distribution\n * @return Value within the centered output " + "distribution.\n") + .def("unmap", &espp::RangeMapper::unmap, py::arg("v"), + "*\n * @brief Unmap a value \\p v from the configured output range (centered,\n * " + " default [-1,1]) back into the input distribution.\n * @param T&v Value from the " + "centered output distribution.\n * @return Value within the input distribution.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassVector2d_int = + py::class_>( + m, "Vector2d_int", py::dynamic_attr(), + "*\n * @brief Container representing a 2 dimensional vector.\n *\n * Provides " + "getters/setters, index operator, and vector / scalar math\n * utilities.\n *\n * " + "\\section vector_ex1 Example\n * \\snippet math_example.cpp vector2 example\n") + .def(py::init(), py::arg("x") = 0, py::arg("y") = 0, + "*\n * @brief Constructor for the vector, defaults to 0,0.\n * @param x The " + "starting X value.\n * @param y The starting Y value.\n") + .def(py::init &>(), py::arg("other"), + "*\n * @brief Vector copy constructor.\n * @param other Vector to copy.\n") + .def("magnitude", &espp::Vector2d::magnitude, + "*\n * @brief Returns vector magnitude: ||v||.\n * @return The magnitude.\n") + .def("magnitude_squared", &espp::Vector2d::magnitude_squared, + "*\n * @brief Returns vector magnitude squared: ||v||^2.\n * @return The " + "magnitude squared.\n") + .def( + "x", [](espp::Vector2d &self) { return self.x(); }, + "*\n * @brief Getter for the x value.\n * @return The current x value.\n") + .def("x", py::overload_cast(&espp::Vector2d::x), py::arg("v"), + "*\n * @brief Setter for the x value.\n * @param v New value for \\c x.\n") + .def( + "y", [](espp::Vector2d &self) { return self.y(); }, + "*\n * @brief Getter for the y value.\n * @return The current y value.\n") + .def("y", py::overload_cast(&espp::Vector2d::y), py::arg("v"), + "*\n * @brief Setter for the y value.\n * @param v New value for \\c y.\n") + .def( + "__lt__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) < 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def( + "__le__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) <= 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def( + "__eq__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) == 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def( + "__ge__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) >= 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def( + "__gt__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) > 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def("__eq__", &espp::Vector2d::operator==, py::arg("other"), + "*\n * @brief Equality operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return True if the vectors are equal, False " + "otherwise.\n") + .def("__getitem__", &espp::Vector2d::operator[], py::arg("index"), + "*\n * @brief Index operator for vector elements.\n * @note Returns a mutable " + "reference to the element.\n * @param index The index to return.\n * @return " + "Mutable reference to the element at \\p index.\n") + .def( + "__neg__", [](espp::Vector2d &self) { return self.operator-(); }, + "*\n * @brief Negate the vector.\n * @return The new vector which is the " + "negative.\n") + .def("__sub__", + py::overload_cast &>(&espp::Vector2d::operator-, + py::const_), + py::arg("rhs"), + "*\n * @brief Return a new vector which is the provided vector subtracted from\n " + " * this vector.\n * @param rhs The vector to subtract from this vector.\n " + " * @return Resultant vector subtraction.\n") + .def("__isub__", &espp::Vector2d::operator-=, py::arg("rhs"), + "*\n * @brief Return the provided vector subtracted from this vector.\n * " + "@param rhs The vector to subtract from this vector.\n * @return Resultant vector " + "subtraction.\n") + .def("__add__", &espp::Vector2d::operator+, py::arg("rhs"), + "*\n * @brief Return a new vector, which is the addition of this vector and the\n " + " * provided vector.\n * @param rhs The vector to add to this vector.\n " + "* @return Resultant vector addition.\n") + .def("__iadd__", &espp::Vector2d::operator+=, py::arg("rhs"), + "*\n * @brief Return the vector added with the provided vector.\n * @param rhs " + "The vector to add to this vector.\n * @return Resultant vector addition.\n") + .def("__mul__", &espp::Vector2d::operator*, py::arg("v"), + "*\n * @brief Return a scaled version of the vector, multiplied by the provided\n " + " * value.\n * @param v Value the vector should be multiplied by.\n * " + "@return Resultant scaled vector.\n") + .def("__imul__", &espp::Vector2d::operator*=, py::arg("v"), + "*\n * @brief Return the vector multiplied by the provided value.\n * @param v " + "Value the vector should be scaled by.\n * @return Resultant scaled vector.\n") + .def("__truediv__", + py::overload_cast(&espp::Vector2d::operator/, py::const_), + py::arg("v"), + "*\n * @brief Return a scaled version of the vector, divided by the provided\n " + "* value.\n * @param v Value the vector should be divided by.\n * " + "@return Resultant scaled vector.\n") + .def("__itruediv__", py::overload_cast(&espp::Vector2d::operator/=), + py::arg("v"), + "*\n * @brief Return the vector divided by the provided value.\n * @param v " + "Value the vector should be divided by.\n * @return Resultant scaled vector.\n") + .def("__truediv__", + py::overload_cast &>(&espp::Vector2d::operator/, + py::const_), + py::arg("v"), + "*\n * @brief Return a scaled version of the vector, divided by the provided\n " + "* vector value. Scales x and y independently.\n * @param v Vector values " + "the vector should be divided by.\n * @return Resultant scaled vector.\n") + .def("__itruediv__", + py::overload_cast &>(&espp::Vector2d::operator/=), + py::arg("v"), + "*\n * @brief Return the vector divided by the provided vector values.\n * " + "@param v Vector of values the vector should be divided by.\n * @return Resultant " + "scaled vector.\n") + .def("dot", &espp::Vector2d::dot, py::arg("other"), + "*\n * @brief Dot product of this vector with another vector.\n * @param other " + "The second vector\n * @return The dot product (x1*x2 + y1*y2)\n") + .def("normalized", &espp::Vector2d::normalized, + "*\n * @brief Return normalized (unit length) version of the vector.\n * " + "@return The normalized vector.\n"); + auto pyClassVector2d_float = + py::class_>( + m, "Vector2d_float", py::dynamic_attr(), + "*\n * @brief Container representing a 2 dimensional vector.\n *\n * Provides " + "getters/setters, index operator, and vector / scalar math\n * utilities.\n *\n * " + "\\section vector_ex1 Example\n * \\snippet math_example.cpp vector2 example\n") + .def(py::init(), py::arg("x") = 0, py::arg("y") = 0, + "*\n * @brief Constructor for the vector, defaults to 0,0.\n * @param x The " + "starting X value.\n * @param y The starting Y value.\n") + .def(py::init &>(), py::arg("other"), + "*\n * @brief Vector copy constructor.\n * @param other Vector to copy.\n") + .def("magnitude", &espp::Vector2d::magnitude, + "*\n * @brief Returns vector magnitude: ||v||.\n * @return The magnitude.\n") + .def("magnitude_squared", &espp::Vector2d::magnitude_squared, + "*\n * @brief Returns vector magnitude squared: ||v||^2.\n * @return The " + "magnitude squared.\n") + .def( + "x", [](espp::Vector2d &self) { return self.x(); }, + "*\n * @brief Getter for the x value.\n * @return The current x value.\n") + .def("x", py::overload_cast(&espp::Vector2d::x), py::arg("v"), + "*\n * @brief Setter for the x value.\n * @param v New value for \\c x.\n") + .def( + "y", [](espp::Vector2d &self) { return self.y(); }, + "*\n * @brief Getter for the y value.\n * @return The current y value.\n") + .def("y", py::overload_cast(&espp::Vector2d::y), py::arg("v"), + "*\n * @brief Setter for the y value.\n * @param v New value for \\c y.\n") + .def( + "__lt__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) < 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def( + "__le__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) <= 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def( + "__eq__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) == 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def( + "__ge__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) >= 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def( + "__gt__", + [](const espp::Vector2d &self, const espp::Vector2d &other) -> bool { + auto cmp = [&self](auto &&other) -> bool { return self.operator<=>(other) > 0; }; + + return cmp(other); + }, + py::arg("other"), + "*\n * @brief Spaceship operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return -1 if this vector is less than \\p other, " + "0 if they are equal, 1 if\n * this vector is greater than \\p other.\n") + .def("__eq__", &espp::Vector2d::operator==, py::arg("other"), + "*\n * @brief Equality operator for comparing two vectors.\n * @param other The " + "vector to compare against.\n * @return True if the vectors are equal, False " + "otherwise.\n") + .def("__getitem__", &espp::Vector2d::operator[], py::arg("index"), + "*\n * @brief Index operator for vector elements.\n * @note Returns a mutable " + "reference to the element.\n * @param index The index to return.\n * @return " + "Mutable reference to the element at \\p index.\n") + .def( + "__neg__", [](espp::Vector2d &self) { return self.operator-(); }, + "*\n * @brief Negate the vector.\n * @return The new vector which is the " + "negative.\n") + .def("__sub__", + py::overload_cast &>(&espp::Vector2d::operator-, + py::const_), + py::arg("rhs"), + "*\n * @brief Return a new vector which is the provided vector subtracted from\n " + " * this vector.\n * @param rhs The vector to subtract from this vector.\n " + " * @return Resultant vector subtraction.\n") + .def("__isub__", &espp::Vector2d::operator-=, py::arg("rhs"), + "*\n * @brief Return the provided vector subtracted from this vector.\n * " + "@param rhs The vector to subtract from this vector.\n * @return Resultant vector " + "subtraction.\n") + .def("__add__", &espp::Vector2d::operator+, py::arg("rhs"), + "*\n * @brief Return a new vector, which is the addition of this vector and the\n " + " * provided vector.\n * @param rhs The vector to add to this vector.\n " + "* @return Resultant vector addition.\n") + .def("__iadd__", &espp::Vector2d::operator+=, py::arg("rhs"), + "*\n * @brief Return the vector added with the provided vector.\n * @param rhs " + "The vector to add to this vector.\n * @return Resultant vector addition.\n") + .def("__mul__", &espp::Vector2d::operator*, py::arg("v"), + "*\n * @brief Return a scaled version of the vector, multiplied by the provided\n " + " * value.\n * @param v Value the vector should be multiplied by.\n * " + "@return Resultant scaled vector.\n") + .def("__imul__", &espp::Vector2d::operator*=, py::arg("v"), + "*\n * @brief Return the vector multiplied by the provided value.\n * @param v " + "Value the vector should be scaled by.\n * @return Resultant scaled vector.\n") + .def("__truediv__", + py::overload_cast(&espp::Vector2d::operator/, py::const_), + py::arg("v"), + "*\n * @brief Return a scaled version of the vector, divided by the provided\n " + "* value.\n * @param v Value the vector should be divided by.\n * " + "@return Resultant scaled vector.\n") + .def("__itruediv__", py::overload_cast(&espp::Vector2d::operator/=), + py::arg("v"), + "*\n * @brief Return the vector divided by the provided value.\n * @param v " + "Value the vector should be divided by.\n * @return Resultant scaled vector.\n") + .def("__truediv__", + py::overload_cast &>(&espp::Vector2d::operator/, + py::const_), + py::arg("v"), + "*\n * @brief Return a scaled version of the vector, divided by the provided\n " + "* vector value. Scales x and y independently.\n * @param v Vector values " + "the vector should be divided by.\n * @return Resultant scaled vector.\n") + .def("__itruediv__", + py::overload_cast &>(&espp::Vector2d::operator/=), + py::arg("v"), + "*\n * @brief Return the vector divided by the provided vector values.\n * " + "@param v Vector of values the vector should be divided by.\n * @return Resultant " + "scaled vector.\n") + .def("dot", &espp::Vector2d::dot, py::arg("other"), + "*\n * @brief Dot product of this vector with another vector.\n * @param other " + "The second vector\n * @return The dot product (x1*x2 + y1*y2)\n") + .def("normalized", &espp::Vector2d::normalized, + "*\n * @brief Return normalized (unit length) version of the vector.\n * " + "@return The normalized vector.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassSocket = + py::class_(m, "Socket", py::dynamic_attr(), + "*\n * @brief Class for a generic socket with some helper " + "functions for\n * configuring the socket.\n"); + + { // inner classes & enums of Socket + py::enum_(pyClassSocket, "Type", py::arithmetic(), "") + .value("raw", espp::Socket::Type::RAW, "*< Only IP headers, no TCP or UDP headers as well.") + .value("dgram", espp::Socket::Type::DGRAM, "*< UDP/IP socket - datagram.") + .value("stream", espp::Socket::Type::STREAM, "*< TCP/IP socket - stream."); + auto pyClassSocket_ClassInfo = + py::class_( + pyClassSocket, "Info", py::dynamic_attr(), + "*\n * @brief Storage for socket information (address, port) with convenience\n * " + " functions to convert to/from POSIX structures.\n") + .def(py::init<>([](std::string address = std::string(), size_t port = size_t()) { + auto r = std::make_unique(); + r->address = address; + r->port = port; + return r; + }), + py::arg("address") = std::string(), py::arg("port") = size_t()) + .def_readwrite("address", &espp::Socket::Info::address, + "*< IP address of the endpoint as a string.") + .def_readwrite("port", &espp::Socket::Info::port, + "*< Port of the endpoint as an integer.") + .def("init_ipv4", &espp::Socket::Info::init_ipv4, py::arg("addr"), py::arg("prt"), + "*\n * @brief Initialize the struct as an ipv4 address/port combo.\n * " + "@param addr IPv4 address string\n * @param prt port number\n") + .def("ipv4_ptr", &espp::Socket::Info::ipv4_ptr, + "*\n * @brief Gives access to IPv4 sockaddr structure (sockaddr_in) for use\n " + " * with low level socket calls like sendto / recvfrom.\n * @return " + "*sockaddr_in pointer to ipv4 data structure\n") + .def("ipv6_ptr", &espp::Socket::Info::ipv6_ptr, + "*\n * @brief Gives access to IPv6 sockaddr structure (sockaddr_in6) for " + "use\n * with low level socket calls like sendto / recvfrom.\n * " + "@return *sockaddr_in6 pointer to ipv6 data structure\n") + .def("update", &espp::Socket::Info::update, + "*\n * @brief Will update address and port based on the curent data in raw.\n") + .def("from_sockaddr", + py::overload_cast( + &espp::Socket::Info::from_sockaddr), + py::arg("source_address"), + "*\n * @brief Fill this Info from the provided sockaddr struct.\n * " + "@param &source_address sockaddr info filled out by recvfrom.\n") + .def("from_sockaddr", + py::overload_cast(&espp::Socket::Info::from_sockaddr), + py::arg("source_address"), + "*\n * @brief Fill this Info from the provided sockaddr struct.\n * " + "@param &source_address sockaddr info filled out by recvfrom.\n") + .def("from_sockaddr", + py::overload_cast(&espp::Socket::Info::from_sockaddr), + py::arg("source_address"), + "*\n * @brief Fill this Info from the provided sockaddr struct.\n * " + "@param &source_address sockaddr info filled out by recvfrom.\n"); + } // end of inner classes & enums of Socket + + pyClassSocket + .def( + "is_valid", [](espp::Socket &self) { return self.is_valid(); }, + "*\n * @brief Is the socket valid.\n * @return True if the socket file descriptor is " + ">= 0.\n") + .def_static("is_valid_fd", py::overload_cast(&espp::Socket::is_valid_fd), + py::arg("socket_fd"), + "*\n * @brief Is the socket valid.\n * @param socket_fd Socket file " + "descriptor.\n * @return True if the socket file descriptor is >= 0.\n") + .def("get_ipv4_info", &espp::Socket::get_ipv4_info, + "*\n * @brief Get the Socket::Info for the socket.\n * @details This will call " + "getsockname() on the socket to get the\n * sockaddr_storage structure, and " + "then fill out the Socket::Info\n * structure.\n * @return Socket::Info " + "for the socket.\n") + .def("set_receive_timeout", &espp::Socket::set_receive_timeout, py::arg("timeout"), + "*\n * @brief Set the receive timeout on the provided socket.\n * @param timeout " + "requested timeout, must be > 0.\n * @return True if SO_RECVTIMEO was successfully " + "set.\n") + .def("enable_reuse", &espp::Socket::enable_reuse, + "*\n * @brief Allow others to use this address/port combination after we're done\n " + "* with it.\n * @return True if SO_REUSEADDR and SO_REUSEPORT were " + "successfully set.\n") + .def("make_multicast", &espp::Socket::make_multicast, py::arg("time_to_live") = 1, + py::arg("loopback_enabled") = true, + "*\n * @brief Configure the socket to be multicast (if time_to_live > 0).\n * " + " Sets the IP_MULTICAST_TTL (number of multicast hops allowed) and\n * " + "optionally configures whether this node should receive its own\n * multicast " + "packets (IP_MULTICAST_LOOP).\n * @param time_to_live number of multicast hops " + "allowed (TTL).\n * @param loopback_enabled Whether to receive our own multicast " + "packets.\n * @return True if IP_MULTICAST_TTL and IP_MULTICAST_LOOP were set.\n") + .def("add_multicast_group", &espp::Socket::add_multicast_group, py::arg("multicast_group"), + "*\n * @brief If this is a server socket, add it to the provided the multicast\n * " + " group.\n *\n * @note Multicast groups must be Class D addresses " + "(224.0.0.0 to\n * 239.255.255.255)\n *\n * See " + "https://en.wikipedia.org/wiki/Multicast_address for more\n * information.\n " + "* @param multicast_group multicast group to join.\n * @return True if " + "IP_ADD_MEMBERSHIP was successfully set.\n") + .def("select", &espp::Socket::select, py::arg("timeout"), + "*\n * @brief Select on the socket for read events.\n * @param timeout how long to " + "wait for an event.\n * @return number of events that occurred.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassTcpSocket = py::class_( + m, "TcpSocket", py::dynamic_attr(), + "*\n * @brief Class for managing sending and receiving data using TCP/IP. Can be\n * " + " used to create client or server sockets.\n *\n * \\section tcp_ex1 TCP Client Example\n " + "* \\snippet socket_example.cpp TCP Client example\n * \\section tcp_ex2 TCP Server " + "Example\n * \\snippet socket_example.cpp TCP Server example\n *\n * \\section tcp_ex3 TCP " + "Client Response Example\n * \\snippet socket_example.cpp TCP Client Response example\n * " + "\\section tcp_ex4 TCP Server Response Example\n * \\snippet socket_example.cpp TCP Server " + "Response example\n *\n"); + + { // inner classes & enums of TcpSocket + auto pyClassTcpSocket_ClassConfig = + py::class_(pyClassTcpSocket, "Config", py::dynamic_attr(), + "*\n * @brief Config struct for the TCP socket.\n") + .def( + py::init<>([](espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}) { + auto r = std::make_unique(); + r->log_level = log_level; + return r; + }), + py::arg("log_level") = espp::Logger::Verbosity{espp::Logger::Verbosity::WARN}) + .def_readwrite("log_level", &espp::TcpSocket::Config::log_level, + "*< Verbosity level for the TCP socket logger."); + auto pyClassTcpSocket_ClassConnectConfig = + py::class_( + pyClassTcpSocket, "ConnectConfig", py::dynamic_attr(), + "*\n * @brief Config struct for connecting to a remote TCP server.\n") + .def(py::init<>([](std::string ip_address = std::string(), size_t port = size_t()) { + auto r = std::make_unique(); + r->ip_address = ip_address; + r->port = port; + return r; + }), + py::arg("ip_address") = std::string(), py::arg("port") = size_t()) + .def_readwrite("ip_address", &espp::TcpSocket::ConnectConfig::ip_address, + "*< Address to send data to.") + .def_readwrite("port", &espp::TcpSocket::ConnectConfig::port, + "*< Port number to send data to."); + auto pyClassTcpSocket_ClassTransmitConfig = + py::class_( + pyClassTcpSocket, "TransmitConfig", py::dynamic_attr(), + "*\n * @brief Config struct for sending data to a remote TCP socket.\n * @note " + "This is only used when waiting for a response from the remote.\n") + .def(py::init<>([](bool wait_for_response = false, size_t response_size = 0, + espp::Socket::response_callback_fn on_response_callback = nullptr, + std::chrono::duration response_timeout = + std::chrono::duration(0.5f)) { + auto r = std::make_unique(); + r->wait_for_response = wait_for_response; + r->response_size = response_size; + r->on_response_callback = on_response_callback; + r->response_timeout = response_timeout; + return r; + }), + py::arg("wait_for_response") = false, py::arg("response_size") = 0, + py::arg("on_response_callback") = py::none(), + py::arg("response_timeout") = std::chrono::duration(0.5f)) + .def_readwrite("wait_for_response", &espp::TcpSocket::TransmitConfig::wait_for_response, + "*< Whether to wait for a response from the remote or not.") + .def_readwrite( + "response_size", &espp::TcpSocket::TransmitConfig::response_size, + "*< If waiting for a response, this is the maximum size response we will receive.") + .def_readwrite("on_response_callback", + &espp::TcpSocket::TransmitConfig::on_response_callback, + "*< If waiting for a\n response, this is an optional " + "handler which is provided the response data.") + .def_readwrite("response_timeout", &espp::TcpSocket::TransmitConfig::response_timeout, + "*< If waiting for a response, this is the maximum timeout to wait.") + .def_static("default", &espp::TcpSocket::TransmitConfig::Default); + } // end of inner classes & enums of TcpSocket + + pyClassTcpSocket.def(py::init()) + .def("reinit", &espp::TcpSocket::reinit, + "*\n * @brief Reinitialize the socket, cleaning it up if first it is already\n * " + " initalized.\n") + .def("close", &espp::TcpSocket::close, "*\n * @brief Close the socket.\n") + .def("is_connected", &espp::TcpSocket::is_connected, + "*\n * @brief Check if the socket is connected to a remote endpoint.\n * @return " + "True if the socket is connected to a remote endpoint.\n") + .def("connect", &espp::TcpSocket::connect, py::arg("connect_config"), + "*\n * @brief Open a connection to the remote TCP server.\n * @param connect_config " + "ConnectConfig struct describing the server endpoint.\n * @return True if the client " + "successfully connected to the server.\n") + .def("get_remote_info", &espp::TcpSocket::get_remote_info, + "*\n * @brief Get the remote endpoint info.\n * @return The remote endpoint info.\n") + .def("transmit", + py::overload_cast &, const espp::TcpSocket::TransmitConfig &>( + &espp::TcpSocket::transmit), + py::arg("data"), py::arg("transmit_config") = espp::TcpSocket::TransmitConfig::Default(), + "*\n * @brief Send data to the endpoint already connected to by TcpSocket::connect.\n " + " * Can be configured to block waiting for a response from the remote.\n *\n " + " * If response is requested, a callback can be provided in\n * " + "send_config which will be provided the response data for\n * processing.\n " + "* @param data vector of bytes to send to the remote endpoint.\n * @param " + "transmit_config TransmitConfig struct indicating whether to wait for a\n * " + "response.\n * @return True if the data was sent, False otherwise.\n") + .def("transmit", + py::overload_cast &, const espp::TcpSocket::TransmitConfig &>( + &espp::TcpSocket::transmit), + py::arg("data"), py::arg("transmit_config") = espp::TcpSocket::TransmitConfig::Default(), + "*\n * @brief Send data to the endpoint already connected to by TcpSocket::connect.\n " + " * Can be configured to block waiting for a response from the remote.\n *\n " + " * If response is requested, a callback can be provided in\n * " + "send_config which will be provided the response data for\n * processing.\n " + "* @param data vector of bytes to send to the remote endpoint.\n * @param " + "transmit_config TransmitConfig struct indicating whether to wait for a\n * " + "response.\n * @return True if the data was sent, False otherwise.\n") + .def("transmit", + py::overload_cast( + &espp::TcpSocket::transmit), + py::arg("data"), py::arg("transmit_config") = espp::TcpSocket::TransmitConfig::Default(), + "*\n * @brief Send data to the endpoint already connected to by TcpSocket::connect.\n " + " * Can be configured to block waiting for a response from the remote.\n *\n " + " * If response is requested, a callback can be provided in\n * " + "send_config which will be provided the response data for\n * processing.\n " + "* @param data string view of bytes to send to the remote endpoint.\n * @param " + "transmit_config TransmitConfig struct indicating whether to wait for a\n * " + "response.\n * @return True if the data was sent, False otherwise.\n") + .def("receive", py::overload_cast &, size_t>(&espp::TcpSocket::receive), + py::arg("data"), py::arg("max_num_bytes"), + "*\n * @brief Call read on the socket, assuming it has already been configured\n * " + " appropriately.\n *\n * @param data Vector of bytes of received data.\n * " + "@param max_num_bytes Maximum number of bytes to receive.\n * @return True if " + "successfully received, False otherwise.\n") + .def("receive", py::overload_cast(&espp::TcpSocket::receive), + py::arg("data"), py::arg("max_num_bytes"), + "*\n * @brief Call read on the socket, assuming it has already been configured\n * " + " appropriately.\n * @note This function will block until max_num_bytes are " + "received or the\n * receive timeout is reached.\n * @note The data pointed " + "to by data must be at least max_num_bytes in size.\n * @param data Pointer to buffer " + "to receive data.\n * @param max_num_bytes Maximum number of bytes to receive.\n * " + "@return Number of bytes received.\n") + .def("bind", &espp::TcpSocket::bind, py::arg("port"), + "*\n * @brief Bind the socket as a server on \\p port.\n * @param port The port to " + "which to bind the socket.\n * @return True if the socket was bound.\n") + .def("listen", &espp::TcpSocket::listen, py::arg("max_pending_connections"), + "*\n * @brief Listen for incoming client connections.\n * @note Must be called " + "after bind and before accept.\n * @see bind\n * @see accept\n * @param " + "max_pending_connections Max number of allowed pending connections.\n * @return True " + "if socket was able to start listening.\n") + .def("accept", &espp::TcpSocket::accept, + "*\n * @brief Accept an incoming connection.\n * @note Blocks until a connection is " + "accepted.\n * @note Must be called after listen.\n * @note This function will " + "block until a connection is accepted.\n * @return A unique pointer to a " + "TcpClientSession if a connection was\n * accepted, None otherwise.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassUdpSocket = py::class_( + m, "UdpSocket", py::dynamic_attr(), + "*\n * @brief Class for managing sending and receiving data using UDP/IP. Can be\n * " + " used to create client or server sockets.\n *\n * See\n * " + "https://github.com/espressif/esp-idf/tree/master/examples/protocols/sockets/udp_multicast\n " + "* for more information on udp multicast sockets.\n *\n * \\section udp_ex1 UDP Client " + "Example\n * \\snippet socket_example.cpp UDP Client example\n * \\section udp_ex2 UDP " + "Server Example\n * \\snippet socket_example.cpp UDP Server example\n *\n * \\section " + "udp_ex3 UDP Client Response Example\n * \\snippet socket_example.cpp UDP Client Response " + "example\n * \\section udp_ex4 UDP Server Response Example\n * \\snippet socket_example.cpp " + "UDP Server Response example\n *\n * \\section udp_ex5 UDP Multicast Client Example\n * " + "\\snippet socket_example.cpp UDP Multicast Client example\n * \\section udp_ex6 UDP " + "Multicast Server Example\n * \\snippet socket_example.cpp UDP Multicast Server example\n " + "*\n"); + + { // inner classes & enums of UdpSocket + auto pyClassUdpSocket_ClassReceiveConfig = + py::class_(pyClassUdpSocket, "ReceiveConfig", + py::dynamic_attr(), "") + .def(py::init<>([](size_t port = size_t(), size_t buffer_size = size_t(), + bool is_multicast_endpoint = {false}, + std::string multicast_group = {""}, + espp::Socket::receive_callback_fn on_receive_callback = {nullptr}) { + auto r = std::make_unique(); + r->port = port; + r->buffer_size = buffer_size; + r->is_multicast_endpoint = is_multicast_endpoint; + r->multicast_group = multicast_group; + r->on_receive_callback = on_receive_callback; + return r; + }), + py::arg("port") = size_t(), py::arg("buffer_size") = size_t(), + py::arg("is_multicast_endpoint") = bool{false}, + py::arg("multicast_group") = std::string{""}, + py::arg("on_receive_callback") = espp::Socket::receive_callback_fn{nullptr}) + .def_readwrite("port", &espp::UdpSocket::ReceiveConfig::port, + "*< Port number to bind to / receive from.") + .def_readwrite("buffer_size", &espp::UdpSocket::ReceiveConfig::buffer_size, + "*< Max size of data we can receive at one time.") + .def_readwrite("is_multicast_endpoint", + &espp::UdpSocket::ReceiveConfig::is_multicast_endpoint, + "*< Whether this should be a multicast endpoint.") + .def_readwrite("multicast_group", &espp::UdpSocket::ReceiveConfig::multicast_group, + "*< If this is a multicast endpoint, this is the group it belongs to.") + .def_readwrite("on_receive_callback", + &espp::UdpSocket::ReceiveConfig::on_receive_callback, + "*< Function containing business logic to handle data received."); + auto pyClassUdpSocket_ClassSendConfig = + py::class_(pyClassUdpSocket, "SendConfig", py::dynamic_attr(), + "") + .def(py::init<>([](std::string ip_address = std::string(), size_t port = size_t(), + bool is_multicast_endpoint = {false}, + bool wait_for_response = {false}, size_t response_size = {0}, + espp::Socket::response_callback_fn on_response_callback = {nullptr}, + std::chrono::duration response_timeout = + std::chrono::duration(0.5f)) { + auto r = std::make_unique(); + r->ip_address = ip_address; + r->port = port; + r->is_multicast_endpoint = is_multicast_endpoint; + r->wait_for_response = wait_for_response; + r->response_size = response_size; + r->on_response_callback = on_response_callback; + r->response_timeout = response_timeout; + return r; + }), + py::arg("ip_address") = std::string(), py::arg("port") = size_t(), + py::arg("is_multicast_endpoint") = bool{false}, + py::arg("wait_for_response") = bool{false}, py::arg("response_size") = size_t{0}, + py::arg("on_response_callback") = espp::Socket::response_callback_fn{nullptr}, + py::arg("response_timeout") = std::chrono::duration(0.5f)) + .def_readwrite("ip_address", &espp::UdpSocket::SendConfig::ip_address, + "*< Address to send data to.") + .def_readwrite("port", &espp::UdpSocket::SendConfig::port, + "*< Port number to send data to.") + .def_readwrite("is_multicast_endpoint", + &espp::UdpSocket::SendConfig::is_multicast_endpoint, + "*< Whether this should be a multicast endpoint.") + .def_readwrite("wait_for_response", &espp::UdpSocket::SendConfig::wait_for_response, + "*< Whether to wait for a response from the remote or not.") + .def_readwrite( + "response_size", &espp::UdpSocket::SendConfig::response_size, + "*< If waiting for a response, this is the maximum size response we will receive.") + .def_readwrite("on_response_callback", + &espp::UdpSocket::SendConfig::on_response_callback, + "*< If waiting for a response, this is an optional handler which is " + "provided the\n response data.") + .def_readwrite("response_timeout", &espp::UdpSocket::SendConfig::response_timeout, + "*< If waiting for a response, this is the maximum timeout to wait."); + auto pyClassUdpSocket_ClassConfig = + py::class_(pyClassUdpSocket, "Config", py::dynamic_attr(), "") + .def( + py::init<>([](espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}) { + auto r = std::make_unique(); + r->log_level = log_level; + return r; + }), + py::arg("log_level") = espp::Logger::Verbosity{espp::Logger::Verbosity::WARN}) + .def_readwrite("log_level", &espp::UdpSocket::Config::log_level, + "*< Verbosity level for the UDP socket logger."); + } // end of inner classes & enums of UdpSocket + + pyClassUdpSocket.def(py::init()) + .def("send", + py::overload_cast &, const espp::UdpSocket::SendConfig &>( + &espp::UdpSocket::send), + py::arg("data"), py::arg("send_config"), + "*\n * @brief Send data to the endpoint specified by the send_config.\n * " + "Can be configured to multicast (within send_config) and can be\n * configured " + "to block waiting for a response from the remote.\n *\n * @note in the case " + "of multicast, it will block only until the first\n * response.\n *\n " + " * If response is requested, a callback can be provided in\n * " + "send_config which will be provided the response data for\n * processing.\n " + "* @param data vector of bytes to send to the remote endpoint.\n * @param send_config " + "SendConfig struct indicating where to send and whether\n * to wait for a " + "response.\n * @return True if the data was sent, False otherwise.\n") + .def("send", + py::overload_cast( + &espp::UdpSocket::send), + py::arg("data"), py::arg("send_config"), + "*\n * @brief Send data to the endpoint specified by the send_config.\n * " + "Can be configured to multicast (within send_config) and can be\n * configured " + "to block waiting for a response from the remote.\n *\n * @note in the case " + "of multicast, it will block only until the first\n * response.\n *\n " + " * If response is requested, a callback can be provided in\n * " + "send_config which will be provided the response data for\n * processing.\n " + "* @param data String view of bytes to send to the remote endpoint.\n * @param " + "send_config SendConfig struct indicating where to send and whether\n * to " + "wait for a response.\n * @return True if the data was sent, False otherwise.\n") + .def("receive", &espp::UdpSocket::receive, py::arg("max_num_bytes"), py::arg("data"), + py::arg("remote_info"), + "*\n * @brief Call recvfrom on the socket, assuming it has already been\n * " + "configured appropriately.\n *\n * @param max_num_bytes Maximum number of bytes to " + "receive.\n * @param data Vector of bytes of received data.\n * @param remote_info " + "Socket::Info containing the sender's information. This\n * will be populated " + "with the information about the sender.\n * @return True if successfully received, " + "False otherwise.\n") + .def("start_receiving", &espp::UdpSocket::start_receiving, py::arg("task_config"), + py::arg("receive_config"), + "*\n * @brief Configure a server socket and start a thread to continuously\n * " + " receive and handle data coming in on that socket.\n *\n * @param task_config " + "Task::Config struct for configuring the receive task.\n * @param receive_config " + "ReceiveConfig struct with socket and callback info.\n * @return True if the socket " + "was created and task was started, False otherwise.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassTask = py::class_( + m, "Task", py::dynamic_attr(), + "*\n * @brief Task provides an abstraction over std::thread which optionally\n * includes " + "memory / priority configuration on ESP systems. It allows users to\n * easily stop the " + "task, and will automatically stop itself if destroyed.\n *\n * There is also a utility " + "function which can be used to get the info for the\n * task of the current context, or for " + "a provided Task object.\n *\n * There is also a helper function to run a lambda on a " + "specific core, which can\n * be used to run a specific function on a specific core, as you " + "might want to\n * do when registering an interrupt driver on a specific core.\n *\n * " + "\\section task_ex1 Basic Task Example\n * \\snippet task_example.cpp Task example\n * " + "\\section task_ex2 Many Task Example\n * \\snippet task_example.cpp ManyTask example\n * " + "\\section task_ex3 Long Running Task Example\n * \\snippet task_example.cpp LongRunningTask " + "example\n * \\section task_ex4 Task Info Example\n * \\snippet task_example.cpp Task Info " + "example\n * \\section task_ex5 Task Request Stop Example\n * \\snippet task_example.cpp " + "Task Request Stop example\n *\n * \\section run_on_core_ex1 Run on Core Example\n * " + "\\snippet task_example.cpp run on core example\n"); + + { // inner classes & enums of Task + auto pyClassTask_ClassBaseConfig = + py::class_( + pyClassTask, "BaseConfig", py::dynamic_attr(), + "*\n * @brief Base configuration struct for the Task.\n * @note This is designed " + "to be used as a configuration struct in other classes\n * that may have a " + "Task as a member.\n") + .def(py::init<>([](std::string name = std::string(), size_t stack_size_bytes = {4096}, + size_t priority = {0}, int core_id = {-1}) { + auto r = std::make_unique(); + r->name = name; + r->stack_size_bytes = stack_size_bytes; + r->priority = priority; + r->core_id = core_id; + return r; + }), + py::arg("name") = std::string(), py::arg("stack_size_bytes") = size_t{4096}, + py::arg("priority") = size_t{0}, py::arg("core_id") = int{-1}) + .def_readwrite("name", &espp::Task::BaseConfig::name, "*< Name of the task") + .def_readwrite("stack_size_bytes", &espp::Task::BaseConfig::stack_size_bytes, + "*< Stack Size (B) allocated to the task.") + .def_readwrite("priority", &espp::Task::BaseConfig::priority, + "*< Priority of the task, 0 is lowest priority on ESP / FreeRTOS.") + .def_readwrite("core_id", &espp::Task::BaseConfig::core_id, + "*< Core ID of the task, -1 means it is not pinned to any core."); + auto pyClassTask_ClassConfig = + py::class_( + pyClassTask, "Config", py::dynamic_attr(), + "*\n * @brief Configuration struct for the Task.\n * @note This is the recommended " + "way to configure the Task, and allows you to\n * use the condition variable " + "and mutex from the task to wait_for and\n * wait_until.\n * @note This is " + "an older configuration struct, and is kept for backwards\n * compatibility. " + "It is recommended to use the AdvancedConfig struct\n * instead.\n") + .def(py::init<>( + [](std::string name = std::string(), + espp::Task::callback_fn callback = espp::Task::callback_fn(), + size_t stack_size_bytes = {4096}, size_t priority = {0}, int core_id = {-1}, + espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}) { + auto r = std::make_unique(); + r->name = name; + r->callback = callback; + r->stack_size_bytes = stack_size_bytes; + r->priority = priority; + r->core_id = core_id; + r->log_level = log_level; + return r; + }), + py::arg("name") = std::string(), py::arg("callback") = espp::Task::callback_fn(), + py::arg("stack_size_bytes") = size_t{4096}, py::arg("priority") = size_t{0}, + py::arg("core_id") = int{-1}, + py::arg("log_level") = espp::Logger::Verbosity{espp::Logger::Verbosity::WARN}) + .def_readwrite("name", &espp::Task::Config::name, "*< Name of the task") + .def_readwrite("callback", &espp::Task::Config::callback, "*< Callback function") + .def_readwrite("stack_size_bytes", &espp::Task::Config::stack_size_bytes, + "*< Stack Size (B) allocated to the task.") + .def_readwrite("priority", &espp::Task::Config::priority, + "*< Priority of the task, 0 is lowest priority on ESP / FreeRTOS.") + .def_readwrite("core_id", &espp::Task::Config::core_id, + "*< Core ID of the task, -1 means it is not pinned to any core.") + .def_readwrite("log_level", &espp::Task::Config::log_level, + "*< Log verbosity for the task."); + auto pyClassTask_ClassSimpleConfig = + py::class_( + pyClassTask, "SimpleConfig", py::dynamic_attr(), + "*\n * @brief Simple configuration struct for the Task.\n * @note This is useful " + "for when you don't need to use the condition variable\n * or mutex in the " + "callback.\n") + .def(py::init<>( + [](espp::Task::simple_callback_fn callback = espp::Task::simple_callback_fn(), + espp::Task::BaseConfig task_config = espp::Task::BaseConfig(), + espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}) { + auto r = std::make_unique(); + r->callback = callback; + r->task_config = task_config; + r->log_level = log_level; + return r; + }), + py::arg("callback") = espp::Task::simple_callback_fn(), + py::arg("task_config") = espp::Task::BaseConfig(), + py::arg("log_level") = espp::Logger::Verbosity{espp::Logger::Verbosity::WARN}) + .def_readwrite("callback", &espp::Task::SimpleConfig::callback, "*< Callback function") + .def_readwrite("task_config", &espp::Task::SimpleConfig::task_config, + "*< Base configuration for the task.") + .def_readwrite("log_level", &espp::Task::SimpleConfig::log_level, + "*< Log verbosity for the task."); + auto pyClassTask_ClassAdvancedConfig = + py::class_( + pyClassTask, "AdvancedConfig", py::dynamic_attr(), + "*\n * @brief Advanced configuration struct for the Task.\n * @note This is the " + "recommended way to configure the Task, and allows you to\n * use the " + "condition variable and mutex from the task to wait_for and\n * wait_until.\n") + .def( + py::init<>([](espp::Task::callback_fn callback = espp::Task::callback_fn(), + espp::Task::BaseConfig task_config = espp::Task::BaseConfig(), + espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}) { + auto r = std::make_unique(); + r->callback = callback; + r->task_config = task_config; + r->log_level = log_level; + return r; + }), + py::arg("callback") = espp::Task::callback_fn(), + py::arg("task_config") = espp::Task::BaseConfig(), + py::arg("log_level") = espp::Logger::Verbosity{espp::Logger::Verbosity::WARN}) + .def_readwrite("callback", &espp::Task::AdvancedConfig::callback, + "*< Callback function") + .def_readwrite("task_config", &espp::Task::AdvancedConfig::task_config, + "*< Base configuration for the task.") + .def_readwrite("log_level", &espp::Task::AdvancedConfig::log_level, + "*< Log verbosity for the task."); + } // end of inner classes & enums of Task + + pyClassTask.def(py::init()) + .def(py::init()) + .def(py::init()) + .def_static("make_unique", + py::overload_cast(&espp::Task::make_unique), + py::arg("config"), + "*\n * @brief Get a unique pointer to a new task created with \\p config.\n " + "* Useful to not have to use templated std::make_unique (less typing).\n " + " * @param config Config struct to initialize the Task with.\n * @return " + "std::unique_ptr pointer to the newly created task.\n") + .def_static("make_unique", + py::overload_cast(&espp::Task::make_unique), + py::arg("config"), + "*\n * @brief Get a unique pointer to a new task created with \\p config.\n " + "* Useful to not have to use templated std::make_unique (less typing).\n " + " * @param config SimpleConfig struct to initialize the Task with.\n * @return " + "std::unique_ptr pointer to the newly created task.\n") + .def_static("make_unique", + py::overload_cast(&espp::Task::make_unique), + py::arg("config"), + "*\n * @brief Get a unique pointer to a new task created with \\p config.\n " + "* Useful to not have to use templated std::make_unique (less typing).\n " + " * @param config AdvancedConfig struct to initialize the Task with.\n * " + "@return std::unique_ptr pointer to the newly created task.\n") + .def("start", &espp::Task::start, + "*\n * @brief Start executing the task.\n *\n * @return True if the task started, " + "False if it was already started.\n") + .def("stop", &espp::Task::stop, + "*\n * @brief Stop the task execution, blocking until it stops.\n *\n * @return " + "True if the task stopped, False if it was not started / already\n * stopped.\n") + .def("is_started", &espp::Task::is_started, + "*\n * @brief Has the task been started or not?\n *\n * @return True if the task " + "is started / running, False otherwise.\n") + .def("is_running", &espp::Task::is_running, + "*\n * @brief Is the task running?\n *\n * @return True if the task is running, " + "False otherwise.\n"); + //////////////////// //////////////////// + + //////////////////// //////////////////// + auto pyClassTimer = py::class_( + m, "Timer", py::dynamic_attr(), + "/ @brief A timer that can be used to schedule tasks to run at a later time.\n/ @details A " + "timer can be used to schedule a task to run at a later time.\n/ The timer will run " + "in the background and will call the task when\n/ the time is up. The timer can be " + "canceled at any time. A timer\n/ can be configured to run once or to repeat.\n/\n/ " + " The timer uses a task to run in the background. The task will\n/ sleep " + "until the timer is ready to run. When the timer is ready to\n/ run, the task will " + "call the callback function. The callback\n/ function can return True to cancel the " + "timer or False to keep the\n/ timer running. If the timer is configured to repeat, " + "then the\n/ callback function will be called again after the period has\n/ " + " elapsed. If the timer is configured to run once, then the\n/ callback function " + "will only be called once.\n/\n/ The timer can be configured to start automatically " + "when it is\n/ constructed. If the timer is not configured to start\n/ " + "automatically, then the timer can be started by calling start().\n/ The timer can " + "be canceled at any time by calling cancel().\n/\n/ @note The timer uses a task to run in " + "the background, so the timer\n/ callback function will be called in the context of " + "the task. The\n/ timer callback function should not block for a long time because " + "it\n/ will block the task. If the timer callback function blocks for a\n/ long " + "time, then the timer will not be able to keep up with the\n/ period.\n/\n/ \\section " + "timer_ex1 Timer Example 1\n/ \\snippet timer_example.cpp timer example\n/ \\section " + "timer_ex2 Timer Delay Example\n/ \\snippet timer_example.cpp timer delay example\n/ " + "\\section timer_ex3 Oneshot Timer Example\n/ \\snippet timer_example.cpp timer oneshot " + "example\n/ \\section timer_ex4 Timer Cancel Itself Example\n/ \\snippet timer_example.cpp " + "timer cancel itself example\n/ \\section timer_ex5 Oneshot Timer Cancel Itself Then Start " + "again with Delay Example\n/ \\snippet timer_example.cpp timer oneshot restart example\n/ " + "\\section timer_ex6 Timer Update Period Example\n/ \\snippet timer_example.cpp timer update " + "period example"); + + { // inner classes & enums of Timer + auto pyClassTimer_ClassConfig = + py::class_(pyClassTimer, "Config", py::dynamic_attr(), + "/ @brief The configuration for the timer.") + .def(py::init<>([](std::string_view name = std::string_view(), + std::chrono::duration period = std::chrono::duration(), + std::chrono::duration delay = std::chrono::duration(0), + espp::Timer::callback_fn callback = espp::Timer::callback_fn(), + bool auto_start = {true}, size_t stack_size_bytes = {4096}, + size_t priority = {0}, int core_id = {-1}, + espp::Logger::Verbosity log_level = espp::Logger::Verbosity::WARN) { + auto r = std::make_unique(); + r->name = name; + r->period = period; + r->delay = delay; + r->callback = callback; + r->auto_start = auto_start; + r->stack_size_bytes = stack_size_bytes; + r->priority = priority; + r->core_id = core_id; + r->log_level = log_level; + return r; + }), + py::arg("name") = std::string_view(), + py::arg("period") = std::chrono::duration(), + py::arg("delay") = std::chrono::duration(0), + py::arg("callback") = espp::Timer::callback_fn(), + py::arg("auto_start") = bool{true}, py::arg("stack_size_bytes") = size_t{4096}, + py::arg("priority") = size_t{0}, py::arg("core_id") = int{-1}, + py::arg("log_level") = espp::Logger::Verbosity::WARN) + .def_readwrite("name", &espp::Timer::Config::name, "/< The name of the timer.") + .def_readwrite( + "period", &espp::Timer::Config::period, + "/< The period of the timer. If 0, the timer callback will only be called once.") + .def_readwrite("delay", &espp::Timer::Config::delay, + "/< The delay before the first execution of the timer callback after " + "start() is called.") + .def_readwrite("callback", &espp::Timer::Config::callback, + "/< The callback function to call when the timer expires.") + .def_readwrite("auto_start", &espp::Timer::Config::auto_start, + "/< If True, the timer will start automatically when constructed.") + .def_readwrite("stack_size_bytes", &espp::Timer::Config::stack_size_bytes, + "/< The stack size of the task that runs the timer.") + .def_readwrite("priority", &espp::Timer::Config::priority, + "/< Priority of the timer, 0 is lowest priority on ESP / FreeRTOS.") + .def_readwrite("core_id", &espp::Timer::Config::core_id, + "/< Core ID of the timer, -1 means it is not pinned to any core.") + .def_readwrite("log_level", &espp::Timer::Config::log_level, + "/< The log level for the timer."); + } // end of inner classes & enums of Timer + + pyClassTimer.def(py::init()) + .def( + "start", [](espp::Timer &self) { return self.start(); }, + "/ @brief Start the timer.\n/ @details Starts the timer. Does nothing if the timer is " + "already running.") + .def("start", py::overload_cast &>(&espp::Timer::start), + py::arg("delay"), + "/ @brief Start the timer with a delay.\n/ @details Starts the timer with a delay. If " + "the timer is already running,\n/ this will cancel the timer and start it " + "again with the new\n/ delay. If the timer is not running, this will start the " + "timer\n/ with the delay. Overwrites any previous delay that might have\n/ " + " been set.\n/ @param delay The delay before the first execution of the timer " + "callback.") + .def("stop", &espp::Timer::stop, + "/ @brief Stop the timer, same as cancel().\n/ @details Stops the timer, same as " + "cancel().") + .def("cancel", &espp::Timer::cancel, + "/ @brief Cancel the timer.\n/ @details Cancels the timer.") + .def("set_period", &espp::Timer::set_period, py::arg("period"), + "/ @brief Set the period of the timer.\n/ @details Sets the period of the timer.\n/ " + "@param period The period of the timer.\n/ @note If the period is 0, the timer will run " + "once.\n/ @note If the period is negative, the period will not be set / updated.\n/ " + "@note If the timer is running, the period will be updated after the\n/ current " + "period has elapsed.") + .def("is_running", &espp::Timer::is_running, + "/ @brief Check if the timer is running.\n/ @details Checks if the timer is running.\n/ " + "@return True if the timer is running, False otherwise."); + //////////////////// //////////////////// + + // // Autogenerated code end + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! AUTOGENERATED CODE END !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +} diff --git a/lib/requirements.txt b/lib/requirements.txt new file mode 100644 index 000000000..01d4bbd52 --- /dev/null +++ b/lib/requirements.txt @@ -0,0 +1 @@ +litgen@git+https://github.com/pthom/litgen diff --git a/lib/wcswidth.c b/lib/wcswidth.c new file mode 100644 index 000000000..3b75839f7 --- /dev/null +++ b/lib/wcswidth.c @@ -0,0 +1,262 @@ +/* + * This is an implementation of wcwidth() and wcswidth() (defined in + * IEEE Std 1002.1-2001) for Unicode. + * + * http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html + * http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html + * + * In fixed-width output devices, Latin characters all occupy a single + * "cell" position of equal width, whereas ideographic CJK characters + * occupy two such cells. Interoperability between terminal-line + * applications and (teletype-style) character terminals using the + * UTF-8 encoding requires agreement on which character should advance + * the cursor by how many cell positions. No established formal + * standards exist at present on which Unicode character shall occupy + * how many cell positions on character terminals. These routines are + * a first attempt of defining such behavior based on simple rules + * applied to data provided by the Unicode Consortium. + * + * For some graphical characters, the Unicode standard explicitly + * defines a character-cell width via the definition of the East Asian + * FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes. + * In all these cases, there is no ambiguity about which width a + * terminal shall use. For characters in the East Asian Ambiguous (A) + * class, the width choice depends purely on a preference of backward + * compatibility with either historic CJK or Western practice. + * Choosing single-width for these characters is easy to justify as + * the appropriate long-term solution, as the CJK practice of + * displaying these characters as double-width comes from historic + * implementation simplicity (8-bit encoded characters were displayed + * single-width and 16-bit ones double-width, even for Greek, + * Cyrillic, etc.) and not any typographic considerations. + * + * Much less clear is the choice of width for the Not East Asian + * (Neutral) class. Existing practice does not dictate a width for any + * of these characters. It would nevertheless make sense + * typographically to allocate two character cells to characters such + * as for instance EM SPACE or VOLUME INTEGRAL, which cannot be + * represented adequately with a single-width glyph. The following + * routines at present merely assign a single-cell width to all + * neutral characters, in the interest of simplicity. This is not + * entirely satisfactory and should be reconsidered before + * establishing a formal standard in this area. At the moment, the + * decision which Not East Asian (Neutral) characters should be + * represented by double-width glyphs cannot yet be answered by + * applying a simple rule from the Unicode database content. Setting + * up a proper standard for the behavior of UTF-8 character terminals + * will require a careful analysis not only of each Unicode character, + * but also of each presentation form, something the author of these + * routines has avoided to do so far. + * + * http://www.unicode.org/unicode/reports/tr11/ + * + * Markus Kuhn -- 2007-05-26 (Unicode 5.0) + * + * Permission to use, copy, modify, and distribute this software + * for any purpose and without fee is hereby granted. The author + * disclaims all warranties with regard to this software. + * + * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c + */ + +#include + +struct interval { + int first; + int last; +}; + +/* auxiliary function for binary search in interval table */ +static int bisearch(wchar_t ucs, const struct interval *table, int max) { + int min = 0; + int mid; + + if (ucs < table[0].first || ucs > table[max].last) + return 0; + while (max >= min) { + mid = (min + max) / 2; + if (ucs > table[mid].last) + min = mid + 1; + else if (ucs < table[mid].first) + max = mid - 1; + else + return 1; + } + + return 0; +} + +/* The following two functions define the column width of an ISO 10646 + * character as follows: + * + * - The null character (U+0000) has a column width of 0. + * + * - Other C0/C1 control characters and DEL will lead to a return + * value of -1. + * + * - Non-spacing and enclosing combining characters (general + * category code Mn or Me in the Unicode database) have a + * column width of 0. + * + * - SOFT HYPHEN (U+00AD) has a column width of 1. + * + * - Other format characters (general category code Cf in the Unicode + * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0. + * + * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF) + * have a column width of 0. + * + * - Spacing characters in the East Asian Wide (W) or East Asian + * Full-width (F) category as defined in Unicode Technical + * Report #11 have a column width of 2. + * + * - All remaining characters (including all printable + * ISO 8859-1 and WGL4 characters, Unicode control characters, + * etc.) have a column width of 1. + * + * This implementation assumes that wchar_t characters are encoded + * in ISO 10646. + */ + +int mk_wcwidth(wchar_t ucs) { + /* sorted list of non-overlapping intervals of non-spacing characters */ + /* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */ + static const struct interval combining[] = { + {0x0300, 0x036F}, {0x0483, 0x0486}, {0x0488, 0x0489}, {0x0591, 0x05BD}, + {0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5}, {0x05C7, 0x05C7}, + {0x0600, 0x0603}, {0x0610, 0x0615}, {0x064B, 0x065E}, {0x0670, 0x0670}, + {0x06D6, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED}, {0x070F, 0x070F}, + {0x0711, 0x0711}, {0x0730, 0x074A}, {0x07A6, 0x07B0}, {0x07EB, 0x07F3}, + {0x0901, 0x0902}, {0x093C, 0x093C}, {0x0941, 0x0948}, {0x094D, 0x094D}, + {0x0951, 0x0954}, {0x0962, 0x0963}, {0x0981, 0x0981}, {0x09BC, 0x09BC}, + {0x09C1, 0x09C4}, {0x09CD, 0x09CD}, {0x09E2, 0x09E3}, {0x0A01, 0x0A02}, + {0x0A3C, 0x0A3C}, {0x0A41, 0x0A42}, {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, + {0x0A70, 0x0A71}, {0x0A81, 0x0A82}, {0x0ABC, 0x0ABC}, {0x0AC1, 0x0AC5}, + {0x0AC7, 0x0AC8}, {0x0ACD, 0x0ACD}, {0x0AE2, 0x0AE3}, {0x0B01, 0x0B01}, + {0x0B3C, 0x0B3C}, {0x0B3F, 0x0B3F}, {0x0B41, 0x0B43}, {0x0B4D, 0x0B4D}, + {0x0B56, 0x0B56}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0}, {0x0BCD, 0x0BCD}, + {0x0C3E, 0x0C40}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, + {0x0CBC, 0x0CBC}, {0x0CBF, 0x0CBF}, {0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, + {0x0CE2, 0x0CE3}, {0x0D41, 0x0D43}, {0x0D4D, 0x0D4D}, {0x0DCA, 0x0DCA}, + {0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6}, {0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, + {0x0E47, 0x0E4E}, {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC}, + {0x0EC8, 0x0ECD}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35}, {0x0F37, 0x0F37}, + {0x0F39, 0x0F39}, {0x0F71, 0x0F7E}, {0x0F80, 0x0F84}, {0x0F86, 0x0F87}, + {0x0F90, 0x0F97}, {0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030}, + {0x1032, 0x1032}, {0x1036, 0x1037}, {0x1039, 0x1039}, {0x1058, 0x1059}, + {0x1160, 0x11FF}, {0x135F, 0x135F}, {0x1712, 0x1714}, {0x1732, 0x1734}, + {0x1752, 0x1753}, {0x1772, 0x1773}, {0x17B4, 0x17B5}, {0x17B7, 0x17BD}, + {0x17C6, 0x17C6}, {0x17C9, 0x17D3}, {0x17DD, 0x17DD}, {0x180B, 0x180D}, + {0x18A9, 0x18A9}, {0x1920, 0x1922}, {0x1927, 0x1928}, {0x1932, 0x1932}, + {0x1939, 0x193B}, {0x1A17, 0x1A18}, {0x1B00, 0x1B03}, {0x1B34, 0x1B34}, + {0x1B36, 0x1B3A}, {0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, {0x1B6B, 0x1B73}, + {0x1DC0, 0x1DCA}, {0x1DFE, 0x1DFF}, {0x200B, 0x200F}, {0x202A, 0x202E}, + {0x2060, 0x2063}, {0x206A, 0x206F}, {0x20D0, 0x20EF}, {0x302A, 0x302F}, + {0x3099, 0x309A}, {0xA806, 0xA806}, {0xA80B, 0xA80B}, {0xA825, 0xA826}, + {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F}, {0xFE20, 0xFE23}, {0xFEFF, 0xFEFF}, + {0xFFF9, 0xFFFB}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F}, + {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x1D167, 0x1D169}, {0x1D173, 0x1D182}, + {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0xE0001, 0xE0001}, + {0xE0020, 0xE007F}, {0xE0100, 0xE01EF}}; + + /* test for 8-bit control characters */ + if (ucs == 0) + return 0; + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) + return -1; + + /* binary search in table of non-spacing characters */ + if (bisearch(ucs, combining, sizeof(combining) / sizeof(struct interval) - 1)) + return 0; + + /* if we arrive here, ucs is not a combining or C0/C1 control character */ + + return 1 + (ucs >= 0x1100 && + (ucs <= 0x115f || /* Hangul Jamo init. consonants */ + ucs == 0x2329 || ucs == 0x232a || + (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs != 0x303f) || /* CJK ... Yi */ + (ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */ + (ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */ + (ucs >= 0xfe10 && ucs <= 0xfe19) || /* Vertical forms */ + (ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */ + (ucs >= 0xff00 && ucs <= 0xff60) || /* Fullwidth Forms */ + (ucs >= 0xffe0 && ucs <= 0xffe6) || (ucs >= 0x20000 && ucs <= 0x2fffd) || + (ucs >= 0x30000 && ucs <= 0x3fffd))); +} + +int mk_wcswidth(const wchar_t *pwcs, size_t n) { + int w, width = 0; + + for (; *pwcs && n-- > 0; pwcs++) + if ((w = mk_wcwidth(*pwcs)) < 0) + return -1; + else + width += w; + + return width; +} + +/* + * The following functions are the same as mk_wcwidth() and + * mk_wcswidth(), except that spacing characters in the East Asian + * Ambiguous (A) category as defined in Unicode Technical Report #11 + * have a column width of 2. This variant might be useful for users of + * CJK legacy encodings who want to migrate to UCS without changing + * the traditional terminal character-width behaviour. It is not + * otherwise recommended for general use. + */ +int mk_wcwidth_cjk(wchar_t ucs) { + /* sorted list of non-overlapping intervals of East Asian Ambiguous + * characters, generated by "uniset +WIDTH-A -cat=Me -cat=Mn -cat=Cf c" */ + static const struct interval ambiguous[] = { + {0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8}, {0x00AA, 0x00AA}, {0x00AE, 0x00AE}, + {0x00B0, 0x00B4}, {0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6}, {0x00D0, 0x00D0}, + {0x00D7, 0x00D8}, {0x00DE, 0x00E1}, {0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED}, + {0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA}, {0x00FC, 0x00FC}, {0x00FE, 0x00FE}, + {0x0101, 0x0101}, {0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B}, {0x0126, 0x0127}, + {0x012B, 0x012B}, {0x0131, 0x0133}, {0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144}, + {0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153}, {0x0166, 0x0167}, {0x016B, 0x016B}, + {0x01CE, 0x01CE}, {0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4}, {0x01D6, 0x01D6}, + {0x01D8, 0x01D8}, {0x01DA, 0x01DA}, {0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261}, + {0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB}, {0x02CD, 0x02CD}, {0x02D0, 0x02D0}, + {0x02D8, 0x02DB}, {0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0391, 0x03A1}, {0x03A3, 0x03A9}, + {0x03B1, 0x03C1}, {0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F}, {0x0451, 0x0451}, + {0x2010, 0x2010}, {0x2013, 0x2016}, {0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022}, + {0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033}, {0x2035, 0x2035}, {0x203B, 0x203B}, + {0x203E, 0x203E}, {0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084}, {0x20AC, 0x20AC}, + {0x2103, 0x2103}, {0x2105, 0x2105}, {0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116}, + {0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B}, {0x2153, 0x2154}, {0x215B, 0x215E}, + {0x2160, 0x216B}, {0x2170, 0x2179}, {0x2190, 0x2199}, {0x21B8, 0x21B9}, {0x21D2, 0x21D2}, + {0x21D4, 0x21D4}, {0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203}, {0x2207, 0x2208}, + {0x220B, 0x220B}, {0x220F, 0x220F}, {0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A}, + {0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225}, {0x2227, 0x222C}, {0x222E, 0x222E}, + {0x2234, 0x2237}, {0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C}, {0x2252, 0x2252}, + {0x2260, 0x2261}, {0x2264, 0x2267}, {0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283}, + {0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299}, {0x22A5, 0x22A5}, {0x22BF, 0x22BF}, + {0x2312, 0x2312}, {0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573}, {0x2580, 0x258F}, + {0x2592, 0x2595}, {0x25A0, 0x25A1}, {0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7}, + {0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8}, {0x25CB, 0x25CB}, {0x25CE, 0x25D1}, + {0x25E2, 0x25E5}, {0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609}, {0x260E, 0x260F}, + {0x2614, 0x2615}, {0x261C, 0x261C}, {0x261E, 0x261E}, {0x2640, 0x2640}, {0x2642, 0x2642}, + {0x2660, 0x2661}, {0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D}, {0x266F, 0x266F}, + {0x273D, 0x273D}, {0x2776, 0x277F}, {0xE000, 0xF8FF}, {0xFFFD, 0xFFFD}, {0xF0000, 0xFFFFD}, + {0x100000, 0x10FFFD}}; + + /* binary search in table of non-spacing characters */ + if (bisearch(ucs, ambiguous, sizeof(ambiguous) / sizeof(struct interval) - 1)) + return 2; + + return mk_wcwidth(ucs); +} + +int mk_wcswidth_cjk(const wchar_t *pwcs, size_t n) { + int w, width = 0; + + for (; *pwcs && n-- > 0; pwcs++) + if ((w = mk_wcwidth_cjk(*pwcs)) < 0) + return -1; + else + width += w; + + return width; +} diff --git a/lib/wcswidth.h b/lib/wcswidth.h new file mode 100644 index 000000000..8c6d8de08 --- /dev/null +++ b/lib/wcswidth.h @@ -0,0 +1,7 @@ +#ifndef _WCWIDTH_H +#include + +int wcwidth(wchar_t wc); +int wcswidth(const wchar_t *pwcs, size_t n); + +#endif diff --git a/pc/CMakeLists.txt b/pc/CMakeLists.txt index 70f90685b..fa795955b 100644 --- a/pc/CMakeLists.txt +++ b/pc/CMakeLists.txt @@ -13,16 +13,25 @@ MACRO(GEN_TESTS curdir) LANGUAGES CXX VERSION 1.0.0 ) + # settings for Windows / MSVC + set(EXTERNAL_LIBS "pthread") + set(LINK_ARG "") + if(MSVC) + add_compile_options(/utf-8 /D_USE_MATH_DEFINES /bigobj) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + set(LINK_ARG "") + set(EXTERNAL_LIBS "ws2_32") + endif() add_executable(${TEST_NAME} ${test_file}) target_include_directories(${TEST_NAME} PRIVATE ${curdir}/../lib/pc/include) + target_link_options(${TEST_NAME} PRIVATE "${LINK_ARG}") target_link_directories(${TEST_NAME} PRIVATE ${curdir}/../lib/pc) target_link_libraries(${TEST_NAME} PRIVATE espp_pc - PRIVATE pthread + PRIVATE ${EXTERNAL_LIBS} ) - install(TARGETS ${TEST_NAME} RUNTIME DESTINATION "bin") ENDFOREACH() ENDMACRO() diff --git a/pc/README.md b/pc/README.md new file mode 100644 index 000000000..a7c71ce64 --- /dev/null +++ b/pc/README.md @@ -0,0 +1,29 @@ +# ESPP PC Code + +This folder contains some test python scripts for loading the `espp` static +library built within the `espp/lib` folder into c++ on a host system running +Linux, MacOS, or Windows. + +## Setup + +First, ensure that you have built the shared objects in the `espp/lib` folder. +If you haven't done so yet, navigate to the `espp/lib` folder and run the +following: + +```console +# if macos/linux: +./build.sh +# if windows +./build.ps1 +``` + +## Quick Start + +To run a test, you can simply run the executable from the terminal: + +```console +# if windows: +./build/Release/udp_client.exe +# if macos / linux: +./build/udp_client +``` diff --git a/pc/build.ps1 b/pc/build.ps1 new file mode 100644 index 000000000..08a8ae7cb --- /dev/null +++ b/pc/build.ps1 @@ -0,0 +1,19 @@ +# powershell script to build the project using cmake + +# Create build directory if it doesn't exist +$buildDir = "build" +if (-not (Test-Path -Path $buildDir)) { + New-Item -ItemType Directory -Path $buildDir +} + +# Change to the build directory +Set-Location -Path $buildDir + +# Run cmake +cmake .. + +# Run cmake --build . --config Release +cmake --build . --config Release + +# Change back to the original directory +Set-Location -Path .. diff --git a/pc/build.sh b/pc/build.sh index ce03e9e41..15ab2d0db 100755 --- a/pc/build.sh +++ b/pc/build.sh @@ -3,4 +3,4 @@ mkdir build cd build cmake .. -make +cmake --build . --config Release diff --git a/pc/tests/cli.cpp b/pc/tests/cli.cpp new file mode 100644 index 000000000..217ef0b59 --- /dev/null +++ b/pc/tests/cli.cpp @@ -0,0 +1,56 @@ +#include "espp.hpp" + +using namespace cli; +using namespace std; + +int main() { + // setup cli + + auto rootMenu = make_unique("cli"); + rootMenu->Insert( + "hello", [](std::ostream &out) { out << "Hello, world\n"; }, "Print hello world"); + rootMenu->Insert( + "hello_everysession", [](std::ostream &) { Cli::cout() << "Hello, everybody" << std::endl; }, + "Print hello everybody on all open sessions"); + rootMenu->Insert( + "answer", [](std::ostream &out, int x) { out << "The answer is: " << x << "\n"; }, + "Print the answer to Life, the Universe and Everything "); + rootMenu->Insert( + "color", + [](std::ostream &out) { + out << "Colors ON\n"; + SetColor(); + }, + "Enable colors in the cli"); + rootMenu->Insert( + "nocolor", + [](std::ostream &out) { + out << "Colors OFF\n"; + SetNoColor(); + }, + "Disable colors in the cli"); + + auto subMenu = make_unique("sub"); + subMenu->Insert( + "hello", [](std::ostream &out) { out << "Hello, submenu world\n"; }, + "Print hello world in the submenu"); + subMenu->Insert( + "demo", [](std::ostream &out) { out << "This is a sample!\n"; }, "Print a demo string"); + + auto subSubMenu = make_unique("subsub"); + subSubMenu->Insert( + "hello", [](std::ostream &out) { out << "Hello, subsubmenu world\n"; }, + "Print hello world in the sub-submenu"); + subMenu->Insert(std::move(subSubMenu)); + + rootMenu->Insert(std::move(subMenu)); + + Cli cli(std::move(rootMenu)); + // global exit action + cli.ExitAction([](auto &out) { out << "Goodbye and thanks for all the fish.\n"; }); + + CliFileSession input(cli); + input.Start(); + + return 0; +} diff --git a/pc/tests/udp_client.cpp b/pc/tests/udp_client.cpp new file mode 100644 index 000000000..2051ff3ac --- /dev/null +++ b/pc/tests/udp_client.cpp @@ -0,0 +1,24 @@ +#include "espp.hpp" + +using namespace std::chrono_literals; + +static auto start = std::chrono::high_resolution_clock::now(); +static auto client = espp::UdpSocket({.log_level = espp::Logger::Verbosity::DEBUG}); + +bool task_func() { + auto send_config = espp::UdpSocket::SendConfig{.ip_address = "127.0.01", .port = 5555}; + fmt::print("Sending message\n"); + client.send("Hello world\n", send_config); + std::this_thread::sleep_for(500ms); + return false; // don't want to stop the task +} + +int main() { + auto task = espp::Task(espp::Task::SimpleConfig{ + .callback = task_func, .task_config = espp::Task::BaseConfig{.name = "test_task"}}); + task.start(); + + std::this_thread::sleep_for(5s); + + return 0; +} diff --git a/pc/tests/udp_server.cpp b/pc/tests/udp_server.cpp new file mode 100644 index 000000000..47d4b8028 --- /dev/null +++ b/pc/tests/udp_server.cpp @@ -0,0 +1,34 @@ +#include "espp.hpp" + +using namespace std::chrono_literals; + +static auto start = std::chrono::high_resolution_clock::now(); + +int main() { + static auto server = espp::UdpSocket({.log_level = espp::Logger::Verbosity::DEBUG}); + auto server_task_config = espp::Task::Config{ + .name = "UdpServer", + .callback = nullptr, + .stack_size_bytes = 6 * 1024, + }; + + auto server_config = espp::UdpSocket::ReceiveConfig{ + .port = 5555, .buffer_size = 1024, .on_receive_callback = [ + ](auto &data, auto &source) -> auto{auto now = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(now - start).count(); + fmt::print("Received {} bytes from {}:{}\n", data.size(), source.address, source.port); + auto message = + fmt::format("Received {} bytes from {}:{}\n", data.size(), source.address, source.port); + // convert the message into a vector of bytes + std::vector message_bytes(message.begin(), message.end()); + // send the message back to the client + return message_bytes; +} +} +; +server.start_receiving(server_task_config, server_config); + +std::this_thread::sleep_for(10s); + +return 0; +} diff --git a/python/README.md b/python/README.md new file mode 100644 index 000000000..a23f2f61e --- /dev/null +++ b/python/README.md @@ -0,0 +1,38 @@ +# ESPP Python Library + +This folder contains some test python scripts for loading the `espp` shared +objects built within the `espp/lib` folder into Python. + +## Setup + +First, ensure that you have built the shared objects in the `espp/lib` folder. +If you haven't done so yet, navigate to the `espp/lib` folder and run the +following: + +```console +# if macos/linux: +./build.sh +# if windows +./build.ps1 +``` + +## Quick Start + +To run a test, you can use the following commands: + +```console +python3 .py +# e.g. +python3 task.py +# or +python3 udp_client.py +``` + +Note: the `udp_client.py` script requires a running instance of the +`udp_server.py` script. To run the server, use the following command from +another terminal: + +```console +python3 udp_server.py +``` + diff --git a/python/support_loader.py b/python/support_loader.py new file mode 100644 index 000000000..143c80e74 --- /dev/null +++ b/python/support_loader.py @@ -0,0 +1,12 @@ +try: + import espp +except ImportError: + import os + import sys + dirpath = os.path.dirname(os.path.realpath(__file__)) + sys.path.append(dirpath + "/../lib/pc") + import espp +else: + print("espp imported") + +print("Imported espp from: ", espp.__file__) diff --git a/python/task.py b/python/task.py index 6ec822540..e00682370 100644 --- a/python/task.py +++ b/python/task.py @@ -1,16 +1,6 @@ import time -try: - print("trying to import espp...") - import espp -except ImportError: - print("espp not found, trying to import from ../lib/pc") - print("NOTE: in general, you should add espp/lib/pc to your PYTHONPATH") - import sys - sys.path.append("../lib/pc") - import espp -else: - print("espp imported") +from support_loader import espp start = time.time() def task_func(): @@ -20,10 +10,10 @@ def task_func(): time.sleep(.5) return False # we don't want to stop the task -task = espp.Task(espp.TaskSimpleConfig( +task = espp.Task(espp.Task.SimpleConfig( task_func, #function # config - espp.TaskBaseConfig("test task") + espp.Task.BaseConfig("test task") )) task.start() diff --git a/python/timer.py b/python/timer.py index b9aec913a..517fde114 100644 --- a/python/timer.py +++ b/python/timer.py @@ -1,16 +1,6 @@ import time -try: - print("trying to import espp...") - import espp -except ImportError: - print("espp not found, trying to import from ../lib/pc") - print("NOTE: in general, you should add espp/lib/pc to your PYTHONPATH") - import sys - sys.path.append("../lib/pc") - import espp -else: - print("espp imported") +from support_loader import espp start = time.time() def timer_func(): @@ -19,7 +9,7 @@ def timer_func(): print(f"[{elapsed:.3f}] Hello from timer") return False # we don't want to stop the timer -timer = espp.Timer(espp.TimerConfig( +timer = espp.Timer(espp.Timer.Config( "test timer", #name 0.5, # period 0.0, # delay @@ -28,7 +18,7 @@ def timer_func(): 4096, # stack size (unused except on ESP) 1, # priority (unused except on ESP) -1, # core (unused except on ESP) - espp.Verbosity.NONE + espp.Logger.Verbosity.none )) time.sleep(5) diff --git a/python/udp_client.py b/python/udp_client.py index 3a36e0f6d..bfa75264f 100644 --- a/python/udp_client.py +++ b/python/udp_client.py @@ -1,18 +1,8 @@ import time -try: - print("trying to import espp...") - import espp -except ImportError: - print("espp not found, trying to import from ../lib/pc") - print("NOTE: in general, you should add espp/lib/pc to your PYTHONPATH") - import sys - sys.path.append("../lib/pc") - import espp -else: - print("espp imported") +from support_loader import espp -udp_client = espp.UdpSocket(espp.UdpSocketConfig(espp.Verbosity.DEBUG)) +udp_client = espp.UdpSocket(espp.UdpSocket.Config(espp.Logger.Verbosity.debug)) start = time.time() def task_func(): @@ -20,7 +10,7 @@ def task_func(): global udp_client port = 5555 ip = "127.0.0.1" - send_config = espp.UdpSendConfig( + send_config = espp.UdpSocket.SendConfig( ip, port ) elapsed = time.time() - start @@ -29,10 +19,10 @@ def task_func(): time.sleep(.5) return False # we don't want to stop the task -task = espp.Task(espp.TaskSimpleConfig( +task = espp.Task(espp.Task.SimpleConfig( task_func, #function # config - espp.TaskBaseConfig("test task") + espp.Task.BaseConfig("test task") )) task.start() diff --git a/python/udp_server.py b/python/udp_server.py index 6eb399829..c06033c54 100644 --- a/python/udp_server.py +++ b/python/udp_server.py @@ -1,16 +1,6 @@ import time -try: - print("trying to import espp...") - import espp -except ImportError: - print("espp not found, trying to import from ../lib/pc") - print("NOTE: in general, you should add espp/lib/pc to your PYTHONPATH") - import sys - sys.path.append("../lib/pc") - import espp -else: - print("espp imported") +from support_loader import espp start = time.time() def on_receive_data(data, sender_info): @@ -27,17 +17,17 @@ def on_receive_data(data, sender_info): ret_data = [ord(x) for x in ret_data] return ret_data -udp_client = espp.UdpSocket(espp.UdpSocketConfig(espp.Verbosity.DEBUG)) +udp_client = espp.UdpSocket(espp.UdpSocket.Config(espp.Logger.Verbosity.debug)) port = 5555 buffer_size = 1024 -receive_config = espp.UdpReceiveConfig( +receive_config = espp.UdpSocket.ReceiveConfig( port, buffer_size, False, '', on_receive_data ) -udp_client.start_receiving(espp.TaskConfig("udp_task", None), receive_config) +udp_client.start_receiving(espp.Task.Config("udp_task", None), receive_config) time.sleep(10)