Skip to content

Align connection timing with other cores#21

Open
MitchBradley wants to merge 5 commits intopschatzmann:mainfrom
MitchBradley:AlignConnection
Open

Align connection timing with other cores#21
MitchBradley wants to merge 5 commits intopschatzmann:mainfrom
MitchBradley:AlignConnection

Conversation

@MitchBradley
Copy link
Copy Markdown
Contributor

Summary

Align WiFiClient/EthernetClient/SocketImpl connection behavior with ESP32 and Arduino-Pico client semantics.

Why

Cross-platform Arduino code expects two things that were not true in Arduino-Emulator:

  • connected() should be a fast status probe, not a multi-second wait before returning false
  • connect() should be timeout-bounded in a way that matches mainstream Arduino network client behavior

Changes

  • Make connected() fail fast on disconnect
  • Treat recv(..., MSG_PEEK) == 0 as peer close
  • Clear socket state on close
  • Add connect(..., timeout_ms) overloads as with the ESP core
  • Make default connect() use the configured connection timeout as with the Pico core
  • Return failure correctly when socket connect fails

Compatibility

This brings Arduino-Emulator closer to other cores:

  • connected() has the same fast-probe behavior as other cores
  • The connect() API is the same as for ESP, and a superset of Pico
  • The connect() timeout behavior when DNS name resolution is used follows the ESP pattern, in which the name resolution step has a separate timeout that is controlled only by the lower level networking component (lwip for ESP with a baked-in 15 second timeout, system networking for Emulator configurable at the system level through e.g. resolv.conf). The Pico core behaves a little differently, in that the supplied timeout value is passed through to the resolver, and then separately applied to the later connect step. Implementing the Pico model would require introducing a separate thread because getaddrbyname() has no adjustable-time-limit version.

* connected() no longer waits
* connect() includes a timeout
* The timeout applies to DNS resolution and connection combined
* There are extra connect( ..., timeout_ms) forms as with ESP
Copilot AI review requested due to automatic review settings April 11, 2026 20:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts Arduino-Emulator’s TCP client connection semantics to better match ESP32 and Arduino-Pico behavior, particularly around fast connected() probing and timeout-bounded connect().

Changes:

  • Adds connect(host, port, timeout_ms) overloads and routes default connect() through the configured connection timeout.
  • Makes connected() fail fast on disconnect and treats recv(..., MSG_PEEK) == 0 as peer close, clearing socket state on close.
  • Improves connection failure propagation (don’t report success when socket connect fails) and tightens socket-state handling in available()/read().

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
ArduinoCore-Linux/cores/arduino/SocketImpl.h Adds a timeout-aware connect() overload to the socket abstraction.
ArduinoCore-Linux/cores/arduino/SocketImpl.cpp Implements fast-probe connected(), timeout-bounded connect via nonblocking+select, and clears socket state on close.
ArduinoCore-Linux/cores/arduino/Ethernet.h Updates EthernetClient to use fast connected(), adds timeout-aware connect overloads, and switches hostname resolution to getaddrinfo.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +234 to +245
if (sock < 0) {
is_connected = false;
return static_cast<size_t>(-1);
}

ssize_t result = ::recv(sock, buffer, len, MSG_DONTWAIT | MSG_NOSIGNAL);
if (result == 0) {
Logger.info(SOCKET_IMPL, "read EOF");
close();
is_connected = false;
return static_cast<size_t>(-1);
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SocketImpl::read() returns static_cast<size_t>(-1) for EOF and for non-fatal nonblocking cases (EAGAIN/EWOULDBLOCK). Because the return type is size_t, this becomes a very large positive value, and call sites that store it in an int (e.g. EthernetClient::read) can misinterpret errors as successful reads. Prefer returning 0 when no bytes are available/EOF (and updating is_connected), or change the API to return a signed type (ssize_t/int) consistently through the stack.

Copilot uses AI. Check for mistakes.
@pschatzmann
Copy link
Copy Markdown
Owner

How did you test these changes ?

@pschatzmann
Copy link
Copy Markdown
Owner

I did some review and think we should change the method signature of SocketImpl virtual size_t read(uint8_t* buffer, size_t len); to return ssize_t or int.

I think returning -1 for size_t is a really bad idea. You changed the the logic of a valid 0 byte return to return -1. I think this should not be treated as an error.

Then we can improve the logic in Ethernet int read(uint8_t* buffer, size_t len) to abort the timeout loop if there was an error!

@MitchBradley
Copy link
Copy Markdown
Contributor Author

MitchBradley commented Apr 12, 2026

How did you test these changes ?

I had been testing with FluidNC but that was not comprehensive in that it did not exercise all of the timing possibilities. I created and pushed a new test examples/network-connect-timing to exercise the cases, in conjunction with a rudimentary "nc" server on localhost, plus packet filter dropping rules to simulate network delays that would result in a timeout as opposed to an instant reject. The header commentary in network-connect-timing explains how to run it and interpret the results.

I will now look at the ssize thing.

@MitchBradley
Copy link
Copy Markdown
Contributor Author

I did some review and think we should change the method signature of SocketImpl virtual size_t read(uint8_t* buffer, size_t len); to return ssize_t or int.

I think returning -1 for size_t is a really bad idea. You changed the the logic of a valid 0 byte return to return -1. I think this should not be treated as an error.

Then we can improve the logic in Ethernet int read(uint8_t* buffer, size_t len) to abort the timeout loop if there was an error!

I think int is the right answer. ssize_t is not aligned with other aspects of Arduino class conventions, and ends up having to be narrowed elsewhere. It is clearer/cleaner to do the narrowing at the lowest level instead of propagating it up to several other places. int rules in Arduino Land.

GPT-5.4 helped me find all of the fallout from the change. 4850094 purports to implement this effort.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants