Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions components/socket/include/socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions components/socket/include/udp_socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
};
Expand All @@ -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. */
Expand Down
81 changes: 58 additions & 23 deletions components/socket/src/socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ void Socket::Info::update() {
if (raw.ss_family == PF_INET) {
const auto *ipv4 = reinterpret_cast<const struct sockaddr_in *>(&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<const struct sockaddr_in6 *>(&raw);
#if defined(ESP_PLATFORM)
Expand All @@ -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);
}
}

Expand All @@ -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) {
Expand All @@ -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));
}

Expand Down Expand Up @@ -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,
Expand All @@ -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<const char *>(&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
Expand All @@ -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<const char *>(&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<const char *>(&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;
Expand Down
12 changes: 6 additions & 6 deletions components/socket/src/udp_socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ bool UdpSocket::send(std::span<const uint8_t> 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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
71 changes: 69 additions & 2 deletions lib/python_bindings/espp/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2887,20 +2887,34 @@ 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
* 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.
* @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.
Expand All @@ -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.

"""
Expand Down Expand Up @@ -3240,13 +3259,17 @@ 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,
port: int = int(),
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"""
Expand All @@ -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
Expand All @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading