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
6 changes: 6 additions & 0 deletions components/socket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ includes some initialization, cleanup, and conversion utilities.

The socket class is subclassed into UdpSocket and TcpSocket.

It provides typed option setters (e.g. `set_receive_timeout(...)`,
`set_receive_buffer_size(...)`, `set_reuse_address(...)`, and
`set_dscp(...)` / `get_dscp()` for marking transmitted packets with a
DiffServ code point such as `espp::Dscp::Ef`) plus a generic `set_option()`
wrapper, so callers never need the native handle for common configuration.

## UDP Socket

UDP sockets provide unreliable, unordered communication over IP network sockets.
Expand Down
23 changes: 23 additions & 0 deletions components/socket/include/socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ typedef int sock_type_t;
#include <math.h>

#include "base_component.hpp"
#include "dscp.hpp"
#include "format.hpp"

namespace espp {
Expand Down Expand Up @@ -239,6 +240,28 @@ class Socket : public BaseComponent {
*/
std::optional<size_t> get_receive_buffer_size();

/**
* @brief Mark this socket's TRANSMITTED packets with a DSCP code point
* (applied as IP_TOS - the DSCP occupies the upper 6 bits of the
* TOS / Traffic Class byte, RFC 2474).
* @note Best-effort: affects network / driver treatment of outgoing traffic
* (e.g. Dscp::Ef "expedited forwarding" for latency-critical flows),
* NOT local scheduling. Some platforms may ignore it.
* @param dscp the code point to apply. A custom (non-standard) value can be
* expressed with static_cast<Dscp>(0-63); values above 63 are
* rejected (returns false with a log) rather than silently masked to
* a different code point.
* @return true if IP_TOS was successfully set.
*/
bool set_dscp(espp::Dscp dscp);

/**
* @brief Get the DSCP code point this socket marks its transmitted packets
* with (read back from IP_TOS; the lower 2 ECN bits are discarded).
* @return the code point, or std::nullopt on failure.
*/
std::optional<espp::Dscp> get_dscp();

/**
* @brief Allow others to use this address/port combination after we're done
* with it.
Expand Down
31 changes: 31 additions & 0 deletions components/socket/src/socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,37 @@ std::optional<size_t> Socket::get_receive_buffer_size() {
return static_cast<size_t>(value);
}

bool Socket::set_dscp(espp::Dscp dscp) {
const auto code_point = static_cast<uint8_t>(dscp);
if (code_point > 63) {
// Named espp::Dscp values are always in range; a custom static_cast'd
// code point is documented as 0-63. Silently masking would apply a
// DIFFERENT code point (64 -> 0, 255 -> 63), so reject invalid values.
logger_.error("set_dscp: invalid DSCP {} (valid range 0-63); not applied", code_point);
return false;
}
const int tos = espp::dscp_to_tos(dscp);
return set_option(IPPROTO_IP, IP_TOS, tos);
}

std::optional<espp::Dscp> Socket::get_dscp() {
int value = 0;
#if defined(_WIN32)
// Winsock's getsockopt takes the optlen as int*, not socklen_t*.
int len = sizeof(value);
int err = getsockopt(socket_, IPPROTO_IP, IP_TOS, reinterpret_cast<char *>(&value), &len);
#else
socklen_t len = sizeof(value);
int err = getsockopt(socket_, IPPROTO_IP, IP_TOS, &value, &len);
#endif
if (err < 0) {
logger_.error("Couldn't get IP_TOS: {}", error_string());
return {};
}
// the DSCP is the upper 6 bits of the TOS byte (the lower 2 are ECN)
return static_cast<espp::Dscp>((static_cast<uint8_t>(value) >> 2) & 0x3F);
}

bool Socket::disable_reuse() {
#if !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM)
// reuse is not compiled into lwip, so it is already effectively disabled
Expand Down
24 changes: 7 additions & 17 deletions components/socket/src/socket_reactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,23 +204,13 @@ SocketReactor::add_udp_receiver(espp::UdpSocket &socket,
sock_type_t fd = socket.native_handle();
if (receive_config.dscp.has_value()) {
// Mark this socket's transmitted packets (e.g. echo responses) with the
// requested DSCP code point. The TOS byte carries the 6-bit DSCP in its
// upper bits (RFC 2474). Best-effort: network / driver treatment only, no
// effect on local scheduling (that is what `band` is for).
const uint8_t dscp = static_cast<uint8_t>(receive_config.dscp.value());
if (dscp > 63) {
// Named espp::Dscp values are always in range; a custom static_cast'd
// code point is documented as 0-63. Silently masking would apply a
// DIFFERENT code point (64 -> 0, 255 -> 63), so ignore invalid values.
logger_.warn("add_udp_receiver: invalid DSCP {} (valid range 0-63) on port {}; not applied",
dscp, receive_config.port);
} else {
const int tos = espp::dscp_to_tos(receive_config.dscp.value());
if (::setsockopt(fd, IPPROTO_IP, IP_TOS, reinterpret_cast<const char *>(&tos), sizeof(tos)) <
0) {
logger_.warn("add_udp_receiver: could not set IP_TOS (DSCP {}) on port {}", dscp,
receive_config.port);
}
// requested DSCP code point. Best-effort: network / driver treatment
// only, no effect on local scheduling (that is what `band` is for).
// Socket::set_dscp() validates the code point and logs specifics;
// registration proceeds either way.
if (!socket.set_dscp(receive_config.dscp.value())) {
logger_.warn("add_udp_receiver: could not apply DSCP {} on port {}",
static_cast<uint8_t>(receive_config.dscp.value()), receive_config.port);
}
}
auto handler = [this, &socket, callback, buffer_size]() {
Expand Down
58 changes: 58 additions & 0 deletions lib/autogenerate_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,56 @@ def _add_gil_release_guards(code: str) -> str:
return code


# litgen does not emit base classes for the socket hierarchy; declare them so the
# python-side UdpSocket/TcpSocket inherit the base Socket methods (set_dscp,
# set_receive_timeout, native_handle, ...).
_BASE_CLASS_FIX = {
"py::class_<espp::TcpSocket>(": "py::class_<espp::TcpSocket, espp::Socket>(",
"py::class_<espp::UdpSocket>(": "py::class_<espp::UdpSocket, espp::Socket>(",
}
Comment thread
finger563 marked this conversation as resolved.


def _fix_base_classes(code: str) -> str:
# Like _fix_implicit_default_ctors: each pattern must apply exactly once. A
# count of 0 means litgen's output drifted and python-side inheritance
# would silently regress; > 1 means the pattern matched something it
# should not have. Warn loudly either way so regeneration surfaces it.
for old, new in _BASE_CLASS_FIX.items():
n = code.count(old)
if n != 1:
print(f"WARNING: base-class fix `{old}` applied {n} times (expected 1); "
"python UdpSocket/TcpSocket may not inherit Socket - update _BASE_CLASS_FIX")
code = code.replace(old, new)
return code


# The stub (.pyi) is generated alongside the pydef and needs the same base-class
# treatment: litgen emits `class TcpSocket:` / `class UdpSocket:` without the
# Socket base, so type checkers / IDEs would not see the inherited Socket
# methods (set_dscp, set_receive_timeout, ...). Extend this map as further
# developer-facing stub fixes are needed.
_STUB_FIX = {
"\nclass TcpSocket:\n": "\nclass TcpSocket(Socket):\n",
"\nclass UdpSocket:\n": "\nclass UdpSocket(Socket):\n",
}


def _postprocess_generated_stub(code: str) -> str:
# Same exactly-once contract (and loud warning) as _fix_base_classes: a
# count of 0 means litgen's stub output drifted and the committed
# inheritance would silently regress on regeneration.
for old, new in _STUB_FIX.items():
n = code.count(old)
if n != 1:
print(f"WARNING: stub fix {old!r} applied {n} times (expected 1); "
"the generated .pyi may lose fixes - update _STUB_FIX")
code = code.replace(old, new)
return code


def _postprocess_generated(code: str) -> str:
code = _fix_implicit_default_ctors(code)
code = _fix_base_classes(code)
code = _fix_template_class_nested(code)
code = _remove_static_instance_dups(code)
code = _fix_class_holders(code)
Expand Down Expand Up @@ -550,6 +598,16 @@ def autogenerate() -> None:
with open(pydef_file, "w") as f:
f.write(code)
print(f"Post-processed {pydef_file}")

# The stub is a generated artifact too - reapply its fixes so regeneration
# preserves them (see _postprocess_generated_stub).
stub_file = output_dir + "/espp/__init__.pyi"
with open(stub_file, "r") as f:
stub = f.read()
stub = _postprocess_generated_stub(stub)
with open(stub_file, "w") as f:
f.write(stub)
print(f"Post-processed {stub_file}")
# NOTE: all fixups are applied statically in _postprocess_generated() above, so the generated
# file compiles with no manual edits and without a build step. fix_generated_bindings.py is kept
# as an optional diagnostic: if a future litgen version introduces *new* unqualified names, run
Expand Down
22 changes: 20 additions & 2 deletions lib/python_bindings/espp/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2916,6 +2916,24 @@ class Socket:
"""
pass

def set_dscp(self, dscp: Dscp) -> bool:
"""*
* @brief Mark this socket's TRANSMITTED packets with a DSCP code point
* (applied as IP_TOS - RFC 2474). Best-effort: network / driver
* treatment of outgoing traffic (e.g. Dscp.Ef), NOT local scheduling.
* @param dscp the espp.Dscp code point to apply.
* @return True if IP_TOS was successfully set.
"""
pass

def get_dscp(self) -> Optional[Dscp]:
"""*
* @brief Get the DSCP code point this socket marks its transmitted packets
* with (read back from IP_TOS; ECN bits discarded).
* @return The espp.Dscp code point, or None on failure.
"""
pass

def enable_reuse(self) -> bool:
"""*
* @brief Allow others to use this address/port combination after we're done
Expand Down Expand Up @@ -2996,7 +3014,7 @@ class Socket:



class TcpSocket:
class TcpSocket(Socket):
"""*
* @brief Class for managing sending and receiving data using TCP/IP. Can be
* used to create client or server sockets.
Expand Down Expand Up @@ -3266,7 +3284,7 @@ class TcpSocket:
# TODO: should this class _contain_ a socket or just create sockets within each
# call?

class UdpSocket:
class UdpSocket(Socket):
"""*
* @brief Class for managing sending and receiving data using UDP/IP. Can be
* used to create client or server sockets.
Expand Down
14 changes: 12 additions & 2 deletions lib/python_bindings/pybind_espp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,16 @@ void py_init_module_espp(py::module &m) {
"*\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("set_dscp", &espp::Socket::set_dscp, py::arg("dscp"),
"*\n * @brief Mark this socket's TRANSMITTED packets with a DSCP code point\n * "
" (applied as IP_TOS - RFC 2474). Best-effort: network / driver\n * "
"treatment of outgoing traffic (e.g. Dscp.Ef), NOT local scheduling.\n * @param "
"dscp the espp.Dscp code point to apply.\n * @return True if IP_TOS was "
"successfully set.\n")
.def("get_dscp", &espp::Socket::get_dscp,
"*\n * @brief Get the DSCP code point this socket marks its transmitted packets\n "
"* with (read back from IP_TOS; ECN bits discarded).\n * @return The "
"espp.Dscp code point, or None on failure.\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 "
Expand Down Expand Up @@ -1762,7 +1772,7 @@ void py_init_module_espp(py::module &m) {
//////////////////// </generated_from:socket.hpp> ////////////////////

//////////////////// <generated_from:tcp_socket.hpp> ////////////////////
auto pyClassTcpSocket = py::class_<espp::TcpSocket>(
auto pyClassTcpSocket = py::class_<espp::TcpSocket, espp::Socket>(
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 "
Expand Down Expand Up @@ -1929,7 +1939,7 @@ void py_init_module_espp(py::module &m) {
//////////////////// </generated_from:tcp_socket.hpp> ////////////////////

//////////////////// <generated_from:udp_socket.hpp> ////////////////////
auto pyClassUdpSocket = py::class_<espp::UdpSocket>(
auto pyClassUdpSocket = py::class_<espp::UdpSocket, espp::Socket>(
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 * "
Expand Down
33 changes: 17 additions & 16 deletions pc/tests/socket_reactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,21 +254,12 @@ int main() {
"Critical-band receiver registered (with dscp)");

#if !defined(_WIN32)
// Verify the DSCP marking actually landed on the sockets: IP_TOS carries
// the DSCP in its upper 6 bits, so read it back with getsockopt().
auto read_tos = [](espp::UdpSocket &s) {
int tos = -1;
socklen_t len = sizeof(tos);
if (::getsockopt(s.native_handle(), IPPROTO_IP, IP_TOS, reinterpret_cast<char *>(&tos),
&len) < 0) {
return -1;
}
return tos;
};
check(read_tos(low_server) == espp::dscp_to_tos(espp::Dscp::Cs1),
"IP_TOS on the Low socket reflects Dscp::Cs1");
check(read_tos(crit_server) == espp::dscp_to_tos(espp::Dscp::Ef),
"IP_TOS on the Critical socket reflects Dscp::Ef");
// Verify the DSCP marking actually landed on the sockets, read back
// through the socket's own getter.
check(low_server.get_dscp() == espp::Dscp::Cs1,
"get_dscp() on the Low socket reflects Dscp::Cs1");
check(crit_server.get_dscp() == espp::Dscp::Ef,
"get_dscp() on the Critical socket reflects Dscp::Ef");
// Out-of-range DSCP: registration must still succeed, but the invalid
// value must be ignored (TOS left at the OS default), not masked into a
// different code point.
Expand All @@ -282,7 +273,17 @@ int main() {
.dscp = static_cast<espp::Dscp>(200)});
check(bad_dscp_id != espp::SocketReactor::INVALID_ID,
"registration with an out-of-range DSCP still succeeds");
check(read_tos(bad_dscp_server) == 0, "out-of-range DSCP is ignored (TOS stays default)");
check(bad_dscp_server.get_dscp() == espp::Dscp::Cs0,
"out-of-range DSCP is ignored (TOS stays default)");
// Direct Socket::set_dscp()/get_dscp() round-trip on the same socket,
// independent of the reactor: valid value applies, invalid is rejected
// without changing the previous one.
check(bad_dscp_server.set_dscp(espp::Dscp::Af41), "set_dscp(Af41) succeeds");
check(bad_dscp_server.get_dscp() == espp::Dscp::Af41, "get_dscp() round-trips Af41");
check(!bad_dscp_server.set_dscp(static_cast<espp::Dscp>(64)),
"set_dscp(64) rejected (out of range)");
check(bad_dscp_server.get_dscp() == espp::Dscp::Af41,
"rejected set_dscp leaves the previous code point in place");
// bad_dscp_server is scoped inside the reactor's block, so make sure its
// registration is fully gone before it goes out of scope
reactor.remove(bad_dscp_id);
Expand Down
Loading