From 63114126328f467d471f08dc72e73a01e724ad92 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 29 Jun 2026 16:07:54 -0500 Subject: [PATCH 1/2] feat(socket): Update socket to support setting a specific address for binding multicast --- components/socket/include/socket.hpp | 16 +++++- components/socket/include/udp_socket.hpp | 8 +++ components/socket/src/socket.cpp | 73 ++++++++++++++++++------ components/socket/src/udp_socket.cpp | 12 ++-- lib/python_bindings/espp/__init__.pyi | 71 ++++++++++++++++++++++- lib/python_bindings/pybind_espp.cpp | 71 ++++++++++++++++++++--- 6 files changed, 215 insertions(+), 36 deletions(-) diff --git a/components/socket/include/socket.hpp b/components/socket/include/socket.hpp index 2bec5fc54..2bcfaafeb 100644 --- a/components/socket/include/socket.hpp +++ b/components/socket/include/socket.hpp @@ -178,9 +178,15 @@ class Socket : public BaseComponent { * 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. + * @param interface_address Optional dotted-decimal IPv4 address of the local + * interface to use for *outgoing* multicast (IP_MULTICAST_IF). When + * empty or "0.0.0.0", the OS chooses its default multicast interface. + * Set this to the IP of the desired NIC on multi-homed hosts (e.g. to + * force multicast out a wired interface instead of Wi-Fi). * @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); + bool make_multicast(uint8_t time_to_live = 1, uint8_t loopback_enabled = true, + const std::string &interface_address = ""); /** * @brief If this is a server socket, add it to the provided the multicast @@ -192,9 +198,15 @@ class Socket : public BaseComponent { * See https://en.wikipedia.org/wiki/Multicast_address for more * information. * @param multicast_group multicast group to join. + * @param interface_address Optional dotted-decimal IPv4 address of the local + * interface on which to join the group (imr_interface) and to use for + * outgoing multicast (IP_MULTICAST_IF). When empty or "0.0.0.0", the OS + * default interface is used. Set this on multi-homed hosts so the group + * is joined on the desired NIC (e.g. wired instead of Wi-Fi). * @return true if IP_ADD_MEMBERSHIP was successfully set. */ - bool add_multicast_group(const std::string &multicast_group); + bool add_multicast_group(const std::string &multicast_group, + const std::string &interface_address = ""); /** * @brief Select on the socket for read events. diff --git a/components/socket/include/udp_socket.hpp b/components/socket/include/udp_socket.hpp index 03672fe87..f805d6f11 100644 --- a/components/socket/include/udp_socket.hpp +++ b/components/socket/include/udp_socket.hpp @@ -47,6 +47,10 @@ 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. */ + std::string multicast_interface{ + ""}; /**< Optional local IPv4 interface address on which to join the multicast group and + receive its traffic. Empty/"0.0.0.0" lets the OS pick the default interface; set it + on multi-homed hosts to bind multicast to a specific NIC (e.g. wired vs Wi-Fi). */ espp::Socket::receive_callback_fn on_receive_callback{ nullptr}; /**< Function containing business logic to handle data received. */ }; @@ -55,6 +59,10 @@ class UdpSocket : public Socket { std::string ip_address; /**< Address to send data to. */ size_t port; /**< Port number to send data to.*/ bool is_multicast_endpoint{false}; /**< Whether this should be a multicast endpoint. */ + std::string multicast_interface{ + ""}; /**< Optional local IPv4 interface address to use for outgoing multicast + (IP_MULTICAST_IF). Empty/"0.0.0.0" lets the OS pick the default interface; set it on + multi-homed hosts to send multicast out a specific NIC (e.g. wired vs Wi-Fi). */ 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. */ diff --git a/components/socket/src/socket.cpp b/components/socket/src/socket.cpp index 88163d940..3942a5f6b 100644 --- a/components/socket/src/socket.cpp +++ b/components/socket/src/socket.cpp @@ -184,7 +184,26 @@ bool Socket::enable_reuse() { #endif // !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM) } -bool Socket::make_multicast(uint8_t time_to_live, uint8_t loopback_enabled) { +// Resolve a dotted-decimal IPv4 interface address to an in_addr. An empty string or "0.0.0.0" +// resolves to INADDR_ANY (let the OS pick the default multicast interface). +static bool resolve_interface_address(const std::string &interface_address, struct in_addr &out) { + if (interface_address.empty() || interface_address == "0.0.0.0") { +#if defined(ESP_PLATFORM) + out.s_addr = IPADDR_ANY; +#else + out.s_addr = htonl(INADDR_ANY); +#endif + return true; + } +#ifdef _MSC_VER + return inet_pton(AF_INET, interface_address.c_str(), &out) == 1; +#else + return inet_aton(interface_address.c_str(), &out) == 1; +#endif +} + +bool Socket::make_multicast(uint8_t time_to_live, uint8_t loopback_enabled, + const std::string &interface_address) { int err = 0; // Assign multicast TTL - separate from normal interface TTL err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_TTL, @@ -200,21 +219,46 @@ bool Socket::make_multicast(uint8_t time_to_live, uint8_t loopback_enabled) { fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_LOOP: {}\n", error_string()); return false; } + // Pin the outgoing multicast interface (IP_MULTICAST_IF) when an interface address is given. + // Binding the socket does NOT control which NIC outgoing multicast leaves on; without this the OS + // uses its default multicast route (e.g. Wi-Fi on a multi-homed macOS host). + if (!interface_address.empty() && interface_address != "0.0.0.0") { + struct in_addr iaddr {}; + if (!resolve_interface_address(interface_address, iaddr)) { + fmt::print(fg(fmt::color::red), "Invalid multicast interface address: {}\n", + interface_address); + return false; + } + err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_IF, reinterpret_cast(&iaddr), + sizeof(struct in_addr)); + if (err < 0) { + fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_IF to {}: {}\n", interface_address, + error_string()); + return false; + } + } return true; } -bool Socket::add_multicast_group(const std::string &multicast_group) { +bool Socket::add_multicast_group(const std::string &multicast_group, + const std::string &interface_address) { struct ip_mreq imreq; int err = 0; - // Configure source interface -#if defined(ESP_PLATFORM) - imreq.imr_interface.s_addr = IPADDR_ANY; + // Resolve the local interface on which to join the group (and use for egress). Empty/"0.0.0.0" + // resolves to INADDR_ANY (OS default interface). Specifying it makes a multi-homed host join the + // group on the desired NIC instead of whichever interface the OS would pick by default. + struct in_addr iface_addr {}; + if (!resolve_interface_address(interface_address, iface_addr)) { + fmt::print(fg(fmt::color::red), "Invalid multicast interface address: {}\n", interface_address); + return false; + } + imreq.imr_interface = iface_addr; + // Configure multicast address to listen to +#if defined(ESP_PLATFORM) 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 @@ -228,18 +272,9 @@ bool Socket::add_multicast_group(const std::string &multicast_group) { return false; } - // Assign the IPv4 multicast source interface, via its IP - // (only necessary if this socket is IPV4 only). Use INADDR_ANY so the OS picks the default - // multicast interface; leaving iaddr uninitialized passes garbage to IP_MULTICAST_IF, which fails - // with EADDRNOTAVAIL ("Can't assign requested address") on some platforms (e.g. macOS). - struct in_addr iaddr {}; -#if defined(ESP_PLATFORM) - iaddr.s_addr = IPADDR_ANY; -#else - iaddr.s_addr = htonl(INADDR_ANY); -#endif - err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_IF, reinterpret_cast(&iaddr), - sizeof(struct in_addr)); + // Assign the IPv4 multicast egress interface (IP_MULTICAST_IF) to the same interface we join on. + err = setsockopt(socket_, IPPROTO_IP, IP_MULTICAST_IF, + reinterpret_cast(&iface_addr), sizeof(struct in_addr)); if (err < 0) { fmt::print(fg(fmt::color::red), "Couldn't set IP_MULTICAST_IF: {}\n", error_string()); return false; diff --git a/components/socket/src/udp_socket.cpp b/components/socket/src/udp_socket.cpp index f5c7825cf..2ff8d9217 100644 --- a/components/socket/src/udp_socket.cpp +++ b/components/socket/src/udp_socket.cpp @@ -60,8 +60,8 @@ bool UdpSocket::send(std::span data, const UdpSocket::SendConfig return false; } if (send_config.is_multicast_endpoint) { - // configure it for multicast - if (!make_multicast()) { + // configure it for multicast (pinning the egress interface if one was provided) + if (!make_multicast(1, true, send_config.multicast_interface)) { logger_.error("Cannot make multicast: {}", error_string()); return false; } @@ -194,13 +194,13 @@ bool UdpSocket::start_receiving(Task::BaseConfig &task_config, return false; } if (receive_config.is_multicast_endpoint) { - // enable multicast - if (!make_multicast()) { + // enable multicast (pinning the egress interface if one was provided) + if (!make_multicast(1, true, receive_config.multicast_interface)) { logger_.error("Unable to make bound socket multicast: {}", error_string()); return false; } - // add multicast group - if (!add_multicast_group(receive_config.multicast_group)) { + // add multicast group, joining on the requested interface (if any) + if (!add_multicast_group(receive_config.multicast_group, receive_config.multicast_interface)) { logger_.error("Unable to add multicast group to bound socket: {}", error_string()); return false; } diff --git a/lib/python_bindings/espp/__init__.pyi b/lib/python_bindings/espp/__init__.pyi index 89816b2bc..86ed499ba 100644 --- a/lib/python_bindings/espp/__init__.pyi +++ b/lib/python_bindings/espp/__init__.pyi @@ -2887,7 +2887,12 @@ class Socket: """ pass - def make_multicast(self, time_to_live: int = 1, loopback_enabled: int = True) -> bool: + def make_multicast( + self, + time_to_live: int = 1, + loopback_enabled: int = True, + interface_address: str = "" + ) -> bool: """* * @brief Configure the socket to be multicast (if time_to_live > 0). * Sets the IP_MULTICAST_TTL (number of multicast hops allowed) and @@ -2895,12 +2900,21 @@ class Socket: * 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. + * @param interface_address Optional dotted-decimal IPv4 address of the local + * interface to use for *outgoing* multicast (IP_MULTICAST_IF). When + * empty or "0.0.0.0", the OS chooses its default multicast interface. + * Set this to the IP of the desired NIC on multi-homed hosts (e.g. to + * force multicast out a wired interface instead of Wi-Fi). * @return True if IP_MULTICAST_TTL and IP_MULTICAST_LOOP were set. """ pass - def add_multicast_group(self, multicast_group: str) -> bool: + def add_multicast_group( + self, + multicast_group: str, + interface_address: str = "" + ) -> bool: """* * @brief If this is a server socket, add it to the provided the multicast * group. @@ -2911,6 +2925,11 @@ class Socket: * See https://en.wikipedia.org/wiki/Multicast_address for more * information. * @param multicast_group multicast group to join. + * @param interface_address Optional dotted-decimal IPv4 address of the local + * interface on which to join the group (imr_interface) and to use for + * outgoing multicast (IP_MULTICAST_IF). When empty or "0.0.0.0", the OS + * default interface is used. Set this on multi-homed hosts so the group + * is joined on the desired NIC (e.g. wired instead of Wi-Fi). * @return True if IP_ADD_MEMBERSHIP was successfully set. """ @@ -3240,6 +3259,9 @@ class UdpSocket: 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. + multicast_interface: str = str("") #*< Optional local IPv4 interface address on which to join the multicast group and + receive its traffic. Empty/"0.0.0.0" lets the OS pick the default interface; set it + on multi-homed hosts to bind multicast to a specific NIC (e.g. wired vs Wi-Fi). on_receive_callback: Socket.receive_callback_fn = Socket.receive_callback_fn(None) #*< Function containing business logic to handle data received. def __init__( self, @@ -3247,6 +3269,7 @@ class UdpSocket: buffer_size: int = int(), is_multicast_endpoint: bool = bool(False), multicast_group: str = str(""), + multicast_interface: str = str(""), on_receive_callback: Socket.receive_callback_fn = Socket.receive_callback_fn(None) ) -> None: """Auto-generated default constructor with named params""" @@ -3256,6 +3279,9 @@ class UdpSocket: 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. + multicast_interface: str = str("") #*< Optional local IPv4 interface address to use for outgoing multicast + (IP_MULTICAST_IF). Empty/"0.0.0.0" lets the OS pick the default interface; set it on + multi-homed hosts to send multicast out a specific NIC (e.g. wired vs Wi-Fi). 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: Socket.response_callback_fn = Socket.response_callback_fn(None) #*< If waiting for a response, this is an optional handler which is provided the @@ -3267,6 +3293,7 @@ class UdpSocket: ip_address: str = "", port: int = int(), is_multicast_endpoint: bool = bool(False), + multicast_interface: str = str(""), wait_for_response: bool = bool(False), response_size: int = int(0), on_response_callback: Socket.response_callback_fn = Socket.response_callback_fn(None), @@ -3429,6 +3456,8 @@ class Task: * \snippet task_example.cpp Task Info example * \section task_ex7 Task Request Stop Example * \snippet task_example.cpp Task Request Stop example + * \section task_ex8 Task Priority and Core Affinity Example + * \snippet task_example.cpp Task Priority and Core example * * \section run_on_core_ex1 Run on Core Example * \snippet task_example.cpp run on core example @@ -3525,6 +3554,44 @@ class Task: """ pass + def set_priority(self, priority: int) -> bool: + """* + * @brief Set the priority of the task. + * @details The new priority is always stored in the task's configuration, so + * it will be used the next time the task is started. If the task is + * currently running (ESP only), the change is also applied to the + * live task immediately via vTaskPrioritySet(). + * @param priority New FreeRTOS priority (0 is lowest priority on ESP / + * FreeRTOS). It is clamped to [0, configMAX_PRIORITIES - 1] on ESP. + * @return True if the change was applied to the currently-running task; False + * if the task is not running (the new value still takes effect the + * next time the task is started) or the platform does not support + * changing a live task's priority. + + """ + pass + + def set_core_id(self, core_id: int) -> bool: + """* + * @brief Set the core affinity (core ID) of the task. + * @details The new core ID is always stored in the task's configuration, so + * it will be used the next time the task is started. On the default + * ESP-IDF FreeRTOS port a running task's core affinity cannot be + * changed (it is fixed when the task is created), so for an + * already-started task this only takes effect after a stop()/start(). + * If the underlying FreeRTOS build does provide a runtime + * core-affinity API (configUSE_CORE_AFFINITY on a multi-core SMP + * build), the change is applied to the live task immediately. + * @param core_id Core to pin the task to (0 or 1), or -1 to leave the task + * unpinned (able to run on any core). + * @return True if the change was applied to the currently-running task; False + * if the task is not running or the platform cannot change a live + * task's core affinity (in which case the new value still takes + * effect the next time the task is started). + + """ + pass + @overload def get_id(self) -> task_id_t: diff --git a/lib/python_bindings/pybind_espp.cpp b/lib/python_bindings/pybind_espp.cpp index 9526bffba..7b465d8f5 100644 --- a/lib/python_bindings/pybind_espp.cpp +++ b/lib/python_bindings/pybind_espp.cpp @@ -1685,20 +1685,31 @@ void py_init_module_espp(py::module &m) { "* 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, + py::arg("loopback_enabled") = true, py::arg("interface_address") = "", "*\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") + "packets.\n * @param interface_address Optional dotted-decimal IPv4 address of the " + "local\n * interface to use for *outgoing* multicast (IP_MULTICAST_IF). When\n " + " * empty or \"0.0.0.0\", the OS chooses its default multicast interface.\n * " + " Set this to the IP of the desired NIC on multi-homed hosts (e.g. to\n * " + " force multicast out a wired interface instead of Wi-Fi).\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"), + py::arg("interface_address") = "", "*\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") + "* @param multicast_group multicast group to join.\n * @param interface_address " + "Optional dotted-decimal IPv4 address of the local\n * interface on which to " + "join the group (imr_interface) and to use for\n * outgoing multicast " + "(IP_MULTICAST_IF). When empty or \"0.0.0.0\", the OS\n * default interface is " + "used. Set this on multi-homed hosts so the group\n * is joined on the desired " + "NIC (e.g. wired instead of Wi-Fi).\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"); @@ -1886,18 +1897,21 @@ void py_init_module_espp(py::module &m) { .def(py::init<>([](size_t port = size_t(), size_t buffer_size = size_t(), bool is_multicast_endpoint = {false}, std::string multicast_group = {""}, + std::string multicast_interface = {""}, espp::Socket::receive_callback_fn on_receive_callback = {nullptr}) { auto r_ctor_ = std::make_unique(); r_ctor_->port = port; r_ctor_->buffer_size = buffer_size; r_ctor_->is_multicast_endpoint = is_multicast_endpoint; r_ctor_->multicast_group = multicast_group; + r_ctor_->multicast_interface = multicast_interface; r_ctor_->on_receive_callback = on_receive_callback; return r_ctor_; }), 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("multicast_interface") = 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.") @@ -1908,6 +1922,12 @@ void py_init_module_espp(py::module &m) { "*< 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( + "multicast_interface", &espp::UdpSocket::ReceiveConfig::multicast_interface, + "*< Optional local IPv4 interface address on which to join the multicast group " + "and\n receive its traffic. Empty/\"0.0.0.0\" lets the OS pick the " + "default interface; set it\n on multi-homed hosts to bind multicast " + "to a specific NIC (e.g. wired vs Wi-Fi).") .def_readwrite("on_receive_callback", &espp::UdpSocket::ReceiveConfig::on_receive_callback, "*< Function containing business logic to handle data received."); @@ -1916,6 +1936,7 @@ void py_init_module_espp(py::module &m) { "") .def(py::init<>([](std::string ip_address = std::string(), size_t port = size_t(), bool is_multicast_endpoint = {false}, + std::string multicast_interface = {""}, bool wait_for_response = {false}, size_t response_size = {0}, espp::Socket::response_callback_fn on_response_callback = {nullptr}, std::chrono::duration response_timeout = @@ -1924,6 +1945,7 @@ void py_init_module_espp(py::module &m) { r_ctor_->ip_address = ip_address; r_ctor_->port = port; r_ctor_->is_multicast_endpoint = is_multicast_endpoint; + r_ctor_->multicast_interface = multicast_interface; r_ctor_->wait_for_response = wait_for_response; r_ctor_->response_size = response_size; r_ctor_->on_response_callback = on_response_callback; @@ -1932,6 +1954,7 @@ void py_init_module_espp(py::module &m) { }), py::arg("ip_address") = std::string(), py::arg("port") = size_t(), py::arg("is_multicast_endpoint") = bool{false}, + py::arg("multicast_interface") = std::string{""}, 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)) @@ -1942,6 +1965,12 @@ void py_init_module_espp(py::module &m) { .def_readwrite("is_multicast_endpoint", &espp::UdpSocket::SendConfig::is_multicast_endpoint, "*< Whether this should be a multicast endpoint.") + .def_readwrite( + "multicast_interface", &espp::UdpSocket::SendConfig::multicast_interface, + "*< Optional local IPv4 interface address to use for outgoing multicast\n " + " (IP_MULTICAST_IF). Empty/\"0.0.0.0\" lets the OS pick the default " + "interface; set it on\n multi-homed hosts to send multicast out a " + "specific NIC (e.g. wired vs Wi-Fi).") .def_readwrite("wait_for_response", &espp::UdpSocket::SendConfig::wait_for_response, "*< Whether to wait for a response from the remote or not.") .def_readwrite( @@ -2043,9 +2072,11 @@ void py_init_module_espp(py::module &m) { "(Recommended)\n * \\snippet task_example.cpp LongRunningTaskNotified example\n * \\section " "task_ex6 Task Info Example\n * \\snippet task_example.cpp Task Info example\n * \\section " "task_ex7 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 * \\section run_on_core_ex2 Run on Core " - "(Non-Blocking) Example\n * \\snippet task_example.cpp run on core nonblocking example\n"); + "example\n * \\section task_ex8 Task Priority and Core Affinity Example\n * \\snippet " + "task_example.cpp Task Priority and Core example\n *\n * \\section run_on_core_ex1 Run on " + "Core Example\n * \\snippet task_example.cpp run on core example\n * \\section " + "run_on_core_ex2 Run on Core (Non-Blocking) Example\n * \\snippet task_example.cpp run on " + "core nonblocking example\n"); { // inner classes & enums of Task auto pyClassTask_ClassBaseConfig = @@ -2115,6 +2146,32 @@ void py_init_module_espp(py::module &m) { .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") + .def("set_priority", &espp::Task::set_priority, py::arg("priority"), + "*\n * @brief Set the priority of the task.\n * @details The new priority is always " + "stored in the task's configuration, so\n * it will be used the next time " + "the task is started. If the task is\n * currently running (ESP only), the " + "change is also applied to the\n * live task immediately via " + "vTaskPrioritySet().\n * @param priority New FreeRTOS priority (0 is lowest priority " + "on ESP /\n * FreeRTOS). It is clamped to [0, configMAX_PRIORITIES - 1] on " + "ESP.\n * @return True if the change was applied to the currently-running task; " + "False\n * if the task is not running (the new value still takes effect " + "the\n * next time the task is started) or the platform does not support\n " + "* changing a live task's priority.\n") + .def( + "set_core_id", &espp::Task::set_core_id, py::arg("core_id"), + "*\n * @brief Set the core affinity (core ID) of the task.\n * @details The new core " + "ID is always stored in the task's configuration, so\n * it will be used the " + "next time the task is started. On the default\n * ESP-IDF FreeRTOS port a " + "running task's core affinity cannot be\n * changed (it is fixed when the " + "task is created), so for an\n * already-started task this only takes effect " + "after a stop()/start().\n * If the underlying FreeRTOS build does provide a " + "runtime\n * core-affinity API (configUSE_CORE_AFFINITY on a multi-core SMP\n " + " * build), the change is applied to the live task immediately.\n * @param " + "core_id Core to pin the task to (0 or 1), or -1 to leave the task\n * " + "unpinned (able to run on any core).\n * @return True if the change was applied to the " + "currently-running task; False\n * if the task is not running or the platform " + "cannot change a live\n * task's core affinity (in which case the new value " + "still takes\n * effect the next time the task is started).\n") .def( "get_id", [](espp::Task &self) { return self.get_id(); }, "*\n * @brief Get the ID for this Task's thread / task context.\n * @return ID for " From a0f1ff99c2983d6020a6dde57295b125ff5e545d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 29 Jun 2026 16:24:08 -0500 Subject: [PATCH 2/2] fix missing ntohs for port (short) conversion --- components/socket/src/socket.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/socket/src/socket.cpp b/components/socket/src/socket.cpp index 3942a5f6b..fe27686b7 100644 --- a/components/socket/src/socket.cpp +++ b/components/socket/src/socket.cpp @@ -23,7 +23,7 @@ void Socket::Info::update() { if (raw.ss_family == PF_INET) { const auto *ipv4 = reinterpret_cast(&raw); address = inet_ntoa(ipv4->sin_addr); - port = ipv4->sin_port; + port = ntohs(ipv4->sin_port); } else if (raw.ss_family == PF_INET6) { const auto *ipv6 = reinterpret_cast(&raw); #if defined(ESP_PLATFORM) @@ -33,7 +33,7 @@ void Socket::Info::update() { inet_ntop(AF_INET6, &(ipv6->sin6_addr), str, INET6_ADDRSTRLEN); address = str; #endif - port = ipv6->sin6_port; + port = ntohs(ipv6->sin6_port); } } @@ -45,7 +45,7 @@ void Socket::Info::from_sockaddr(const struct sockaddr_storage &source_address) 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; + port = ntohs(source_address.sin_port); } void Socket::Info::from_sockaddr(const struct sockaddr_in6 &source_address) { @@ -56,7 +56,7 @@ void Socket::Info::from_sockaddr(const struct sockaddr_in6 &source_address) { inet_ntop(AF_INET6, &(source_address.sin6_addr), str, INET6_ADDRSTRLEN); address = str; #endif - port = source_address.sin6_port; + port = ntohs(source_address.sin6_port); memcpy(&raw, &source_address, sizeof(source_address)); }