New lwIP-based Ethernet library for Teensy 4.1

I just released 0.15.0. The changes:

### Added
* Added a way to enable promiscuous mode: define the
* Added a way to remove all the mDNS code: set `LWIP_MDNS_RESPONDER` to `0`
  in `lwipopts.h`.
* Added support for ".local" name lookups.
* Added the ability, in the mDNS client, to specify a service name that's
  different from the host name.
* Added `EthernetClient::abort()` for killing connections without going through
  the TCP close process.
* New sections in the README:
  * "How to change the number of sockets", and
  * "On connections that hang around after cable disconnect".
* An `EthernetServer` instance can now be created without setting the port.
  There are two new `begin()` functions for setting the port:
  * `begin(port)`
  * `begin(port, reuse)`

### Changed
* Moved CRC-32 lookup table to PROGMEM.
* Changed in `EthernetServer`:
  * `port()` returns an `int32_t` instead of a `uint16_t` so that -1 can
    represent an unset port; non-negative values are still 16-bit quantities
  * `begin(reuse)` now returns a `bool` instead of `void`, indicating success
  * The `EthernetServer` destructor now calls `end()`

### Removed
* Removed some unneeded network interfaces:
  * IEEE 802.1D MAC Bridge
  * 6LowPAN
  * PPP
  * SLIP
  * ZigBee Encapsulation Protocol
* Removed HTTPD options from `lwipopts.h`.

* Support for ".local" name lookups,
* `EthernetClient::abort()` for not having to go through the TCP close process, if necessary,
* The ability to create an `EthernetServer` without a port, and
* A couple new sections in the README.

Udp packet loss during SSD1306 display refresh.

Has anyone else detected problems with QNEthernet and a SSD1306 display?
(I did not tried this with NativeEthernet yet)

I am using a SSD1306 oled display with hw spi on the second spi port of a Teensy 4.1 (SPI1).
At the same time i am receiving Art-Net data over the native ethernet port using QNEthernet.
Checking the Art-Net sequence numbers i realized that sometimes 1 packet gets lost. This only happens during the display refresh! It doesnt matter how many universes i receive. It is the same with 1 or 64 universes.
Receiving 1 megabyte per second is absolutely no problem until the next refresh cycle. Then sometimes a packet gets lost.
In addition there are around 120000 spi write calls (just some bytes) per second to 3 dac-chips without any issues. So the problem can not be only spi related.

The execution time for a refresh cycle with 24mhz spi clock is around 990us and this happens only 4 times per second.
Yield() is original, just added the Art-Net checking via event responder. The Art-Net check runs 620000 times per second. So Ethernet.loop() respectively enet_proc_input() should run at the same rate.
My math is not the best, but there should be a call to yield() every 1.61us.
This should be enough to catch all the data, but not if anything blocks the execution.

I have changed IRQ_ENET from 128 to lower values, but with no success.
I also tried software spi for the display. But then the refresh cycle time just increases to 3800us and the issue stays the same. I also tried different libraries for the display.
Even the simple SSD1306Ascii library leads to the described packet loss.
Unfortunately my knowledge is not enough to understand what exactly happens in lwip_t41.c. But it looks complicated...
I experimented also with some parameters in lwipopts.h, but with no success. So everything there is original.

Arduino IDE is 1.8.19, Teensyduino is 1.57. QNEthernet is the latest.
If anybody could give me a hint without posting my code i would be very grateful :)
Ok it seems that all of these SSD1306 libraries are using delayMicroseconds() to generate some display related timings. I could reproduce the packet loss with a delay of 200us.
Darn :mad:
Now i have added a digitalToggleFast(13) to delayMicroseconds() in C:\Program Files (x86)\Arduino\hardware\teensy\avr\cores\teensy4\core_pins.h to check if delayMicroseconds() is called anywhere.
And it is not! I forgot that i already removed all unnecessary delays from the library i use for the display (u8g2). DelayMicroseconds() is only used for the display initialisation.
I am not an Arduino pro. So is it possible that a call to another definition of delayMicroseconds() is made by a library??
I’d need to see code. Checking for Art-Net in yield() via EventResponder doesn’t seem like the right place and the code may be overcomplicated. Instead, what happens if it’s ultimately checked from loop()?
I’d need to see code. Checking for Art-Net in yield() via EventResponder doesn’t seem like the right place and the code may be overcomplicated. Instead, what happens if it’s ultimately checked from loop()?

Hi Shawn,

if i check Art-Net directly from loop the issue stays the same...
I still need to see your code. I’ve written several Art-Net and sACN programs with QNEthernet and they work fine. I’m not sure what you’re doing differently.
I just released v0.16.0. The changes:

### Added
* `EthernetUDP::size()`: Returns the total size of the received packet data.
* Added an optional "dns" parameter to the three-parameter Ethernet.begin() that
  defaults to unset. This ensures that the DNS server IP is set before the
  address-changed callback is called.
* Added configurable packet buffering to UDP reception with the new
  `EthernetUDP(queueSize)` constructor. The default and minimum queue size is 1.
* Added configurable frame buffering to raw frame reception with the new
  `QNETHERNET_FRAME_QUEUE_SIZE` macro. Its default is 1.
* Added a new "Configuration macros" section to the README that summarizes all
  the configuration macros.

### Changed
* Changed `EthernetUDP::parsePacket()` to return zero for empty packets and -1
  if nothing is available.

* UDP and raw frame buffering for helping mitigate the problem of missing packets when they come in faster than they can be processed.
* `EthernetUDP::parsePacket()` returns zero for empty packets and -1 if nothing is available. There's lots of misuse of the return value from this function, for example treating it as a Boolean. Please check for >= 0 instead to determine if there's a packet available.

I just released v0.17.0. The changes:
### Added
* The library now, by default, puts the RX and TX buffers in RAM2 (DMAMEM).
  This behaviour can be overridden by defining the new
* Added separate `stderr` output support with the new `stderrPrint` variable.
  If set to NULL, `stderr` defaults to the usual `stdPrint`.
* Added `MDNSClass::operator bool()` for determining whether mDNS has
  been started.
* Added `MDNSClass::restart()` for when a cable has been disconnected for a
  while and then reconnected.
* Added `EthernetFrameClass::setReceiveQueueSize(size)` for setting the receive
  queue size. This replaces the `QNETHERNET_FRAME_QUEUE_SIZE` macro.
* Added a way to disable raw frame support: define the new
* Added a "Complete list of features" section to the README.
* Added `MDNSClass::hostname()` for returning the hostname of the responder,
  if running.
* Added `EthernetUDP::operator bool()`.
* Added an already-started check to `MDNSClass`.
* New section in the README: "`operator bool()` and `explicit`". It addresses
  problems that may arise with `explicit operator bool()`.
* Added `EthernetClient::connectionId()` for identifying connections across
  possibly multiple `EthernetClient` objects.
* Added `EthernetClass::isDHCPActive()`.

### Changed
* Improved error code messages in `lwip_strerr(err)`. This is used when
  `LWIP_DEBUG` is defined.
* Now shutting down mDNS too in `EthernetClass::end()`.
* Now using overloads instead of default arguments in `EthernetClass`
  and `EthernetUDP`.
* Changed `EthernetClass` and `MDNSClass` `String` functions to take
  `const char *` instead.
* Made all `operator bool()` functions `explicit`.
* `MDNSClass::removeService()` now returns `false` instead of `true` if mDNS has
  not been started.
* Enable definition of certain macros in `lwipopts.h` from the command line.
* Changed API uses of `unsigned char` to `uint8_t`, for consistency.

### Removed
* Removed the `QNETHERNET_FRAME_QUEUE_SIZE` macro and replaced it with

### Fixed
* Disallow `stdin` in `_write()`.

* Ethernet buffers are now in RAM2 (change-back-able to RAM1 if you need).
* Command line changeability of certain lwIP options.
* Ability to disable raw frame support to save some space.
* README updates.
* An `EthernetClient::connectionId()` function for uniquely identifying TCP connections.
* `Ethernet.isDHCPActive()`.
* Use of `String` to `const char *` changes.
* `EthernetFrame.setReceiveQueueSize(size)`
* Add `explicit` to `operator bool()` declarations in the API.

Thanks for your hard work on this. QNEthernet V.?? has been serving HTTP pages and UDP comm continuously for me (home network) with no issues since Mar 2022. My previous experience with other libraries over years has been dismal. I attribute this success to your library and this thread and others of yours for teaching me how to use it.

Just updated from v0.15 to v0.17.0 and only had to make one change: if(packetSize!=0){ to if(PacketSize>0){ which was easily figured out from your change notes.

FYI Arduino 2.02 just updated to 2.03 and in a YES response to updating libraries auto updated to your v0.17. awesome!
@bicycleguy Thanks for the kind words and for describing a little bit about what you’re doing! I’m really glad it was easy to find that parsePacket() fix from the changelog. :)

I’m curious, which HTTP server are you using, your own or a library?
I’m curious, which HTTP server are you using, your own or a library?
Here's a minimal version that should work with a bare T4.1 with the ethernet plug putting up a web page that will allow you to turn on and off the LED.

Please excuse my lack of taking all your advice and not updating with the latest features.

/*for use with IDE 1.0
* open serial monitor to see what the arduino receives and the server address
* requires DHCP
* for hardware: Sprinkler.kicad_pro   or bare teensy 4.1
* use the \ slash to escape the " in the html

#include <QNEthernet.h>

//for hardware:  Sprinkler Rev-  2022/05/25
#define LEDPIN13 13
#define FVEGPIN 10
#define FLWRPIN 12
#define RGRGPIN 25
#define RWALLPIN 28

using namespace qindesign::network;

EthernetServer server(80); //server port
constexpr uint32_t kDHCPTimeout = 10000;  // 10 seconds
constexpr uint16_t kServerPort = 80; //443 for TLS
constexpr int kMessageSize = 255;  // Pretend the protocol specifies 10 bytes

IPAddress ip;
String readString;
boolean gledon=false;
boolean gfvegon=false;
boolean gflwron=false;
boolean grgrgon=false;
boolean gwallon=false;
int theline=0;
uint8_t mac[6];

void setup(){

  pinMode(LEDPIN13, OUTPUT); //pin selected to control
  pinMode(FVEGPIN, OUTPUT); //pin selected to control
  pinMode(FLWRPIN, OUTPUT); //pin selected to control
  pinMode(RGRGPIN, OUTPUT); //pin selected to control
  pinMode(RWALLPIN, OUTPUT); //pin selected to control
  digitalWrite(LEDPIN13, gledon);  //turn off when starting up
  digitalWrite(FVEGPIN, gfvegon);  //turn off when starting up
  digitalWrite(FLWRPIN, gflwron);  //turn off when starting up
  digitalWrite(RGRGPIN, grgrgon);  //turn off when starting up
  digitalWrite(RWALLPIN, gwallon);  //turn off when starting up
  //enable serial data print
  while (!Serial && millis()<5000) {
    ; // wait for serial port to connect or 5 seconds
  stdPrint = &Serial;  // Make printf work (a QNEthernet feature)
  Serial.println("QsprinklerServerMinimal"); // so I can keep track of what is loaded
  Serial.printf("\nMAC: %02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  if(mac[5]==0xf3){   //host name stuff
  }else if(mac[5]==0xb6){
  } //will use default 'teensy-lwip'
  printf("Starting Ethernet with DHCP...\n");
  if (!Ethernet.begin()) {
    printf("Failed to start Ethernet\n");
  if (!Ethernet.waitForLocalIP(kDHCPTimeout)) {
    printf("Failed to get IP address from DHCP\n");
  } else {
    ip = Ethernet.subnetMask();
    printf("    Subnet mask = %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);
    ip = Ethernet.gatewayIP();
    printf("    Gateway     = %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);
    ip = Ethernet.dnsServerIP();
    printf("    DNS         = %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);

    // Start the server
    ip = Ethernet.localIP();
    printf("Listening for clients on '%s'.  Copy this to your browser:  http://%u.%u.%u.%u   \n\n", Ethernet.hostname().c_str(), ip[0], ip[1], ip[2], ip[3]);
  digitalWrite(LEDPIN13, gledon);  //turn off when starting up

void loop(){
  // Create a client connection
  EthernetClient client = server.available();
  if (client) {
    while (client.connected()) {
      if (client.available()) {
        char c =;

        //read char by char HTTP request
        if (readString.length() < 100) {

          //store characters to string
          readString += c;
          //Serial.print(c); //uncomment to see in serial monitor

        //if HTTP request has ended
        if (c == '\n') {
          Serial.println(theline); //separate responses with a line #
          Serial.print(readString); // \n already included
          //set the status of items before sending the HTML in case this is a response to previous change
          //this will prevent having to update again as in formServer.ino
          if(readString.indexOf("LED+ON") >0){
            digitalWrite(LEDPIN13, gledon);   
          }else if(readString.indexOf("LED+OFF") >0){ 
            digitalWrite(LEDPIN13, gledon);  

          }else if(readString.indexOf("BLE+ON") >0){
            digitalWrite(FVEGPIN, gfvegon);   
          }else if(readString.indexOf("BLE+OFF") >0){ 
            digitalWrite(FVEGPIN, gfvegon);   
          }else if(readString.indexOf("WER+ON") >0){ 
            digitalWrite(FLWRPIN, gflwron);   
          }else if(readString.indexOf("WER+OFF") >0){ 
            digitalWrite(FLWRPIN, gflwron);   
          }else if(readString.indexOf("AGE+ON") >0){ 
            digitalWrite(RGRGPIN, grgrgon);   
          }else if(readString.indexOf("AGE+OFF") >0){ 
            digitalWrite(RGRGPIN, grgrgon);   
          }else if(readString.indexOf("WALL+ON") >0){ 
            digitalWrite(FVEGPIN, gwallon);  //RWALLPIN 
          }else if(readString.indexOf("WALL+OFF") >0){ 
            digitalWrite(FVEGPIN, gwallon);   //RWALLPIN
            Serial.println("Didn't see on or off");

          //now output HTML data header
          client.writeFully("HTTP/1.1 200 OK\n");
          client.writeFully("Content-Type: text/html\n");

          //client.println("<meta http-equiv=\"refresh\" content=\"3\" />");  //this is lame with updates every 3 seconds
          client.writeFully("<font size=\"10\">");


          //client.printf("<FORM ACTION=\"http://%u.%u.%u.%u:80\" method=get >",  ip[0], ip[1], ip[2], ip[3]);
          char buff[60];
          sprintf(buff, "<FORM ACTION=\"http://%u.%u.%u.%u:80\" method=get >",  ip[0], ip[1], ip[2], ip[3]);
          //String ths=buff;  //to check the bubber length
          //Serial.printf("\nths.length() is = %d\n", ths.length());
            client.writeFully("The LED is ON");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN LED OFF\" style=\"font-size: 70px\">");
            client.writeFully("The LED is OFF");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN LED ON\" style=\"font-size: 70px\">");
            client.writeFully("The front vegetable sprinkler is ON");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN VEGETABLE OFF\" style=\"font-size: 70px\">");
            client.writeFully("The front vegetable sprinkler is OFF");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN VEGETABLE ON\" style=\"font-size: 70px\">");
            client.writeFully("The front flower sprinkler is ON");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN FLOWER OFF\" style=\"font-size: 70px\">");
            client.writeFully("The front flower sprinkler is OFF");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN FLOWER ON\" style=\"font-size: 70px\">");
            client.writeFully("The rear sprinkler by garage is ON");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN BY GARAGE OFF\" style=\"font-size: 70px\">");
            client.writeFully("The rear sprinkler by garage is OFF");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN BY GARAGE ON\" style=\"font-size: 70px\">");
            client.writeFully("The rear sprinkler by wall is ON");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN BY WALL OFF\" style=\"font-size: 70px\">");
            client.writeFully("The rear sprinkler by wall is OFF");
            client.writeFully("<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"TURN BY WALL ON\" style=\"font-size: 70px\">");

          //client.println("Changes immediately, but page <BR> update requires another press");

          //client.printf("file: %s", __FILE__ ); //this works but prints to internet: /Users/michaelrunyan/Doc... not good
          //client.printf("temperature: %0.2f &#186C", tempmonGetTemp());


          //stopping client

          //clearing string for next read
I just released v0.18.0. The changes:

### Added
* Added a "Notes on ordering and timing" section to the README.
* Added a README section that discusses `EthernetClient::connect()` and its
  return values.
* Added non-blocking TCP connection functions, `connectNoWait()`, that are
  equivalent to the `connect()` functions but don't wait for the connection
  to complete.
* Added `EthernetUDP::beginWithReuse()` and `beginMulticastWithReuse()`
  functions to replace the corresponding begin-with-_reuse_-parameter versions.
* Added printing the link speed and duplex in the IPerfServer example.
* Added an "Asynchronous use is not supported" section to the README.
* New `EthernetClass::onInterfaceStatus(callback)` and `interfaceStatus()`
  functions for tracking the network interface status.
* Added a check to ensure lwIP isn't called from an interrupt context.

### Changed
* Wrapped `LWIP_MDNS_RESPONDER` option in `lwipopts.h` with `#ifndef` and added
  it to the README.
* Made `EthernetClass::loop()` non-static.
* Changed serial output in examples to use CRLF line endings.
* Changed `EthernetClient::connect()` internals to call `close()` instead of
  `stop()` so that any cleanup doesn't block.
* Updated `EthernetClient::connect()` to return some of the error codes defined
  at [Ethernet - client.connect()](
* Changed `EthernetServer::begin()`-with-Boolean-_reuse_-parameters to be named
  `beginWithReuse()`. This avoids too many overloads with mysterious
  Boolean arguments.
* Changed `EthernetServer::operator bool()` to be `const`.
* Changed `EthernetServer::end()` to return `void` instead of `bool`.
* Changed `MDNSClass::begin(hostname)` and `DNSClient::getHostByName()` to treat
  a NULL hostname as an error; they now explicitly return false in this case.
* Changed `MDNSClass::end()` to return `void` instead of `bool`.
* Changed examples that use `unsigned char` to use `uint8_t` in
  appropriate places.
* `EthernetUDP::begin` functions now call `stop()` if the socket is listening
  and the parameters have changed.
* `MDNSClass::begin(hostname)` now calls `end()` if the responder is running and
  the hostname changed.
* Changed both `EthernetServer` and `EthernetUDP` to disallow copying but
  allow moving.
* Changed raw frame support to be excluded by default. This changed the
* Changed `tcp_pcb` member accesses to use appropriate TCP API function calls.
  This fixes use of the altcp API.

### Removed
* `EthernetServer` and `EthernetUDP` begin functions that take a Boolean
  `reuse` parameter.

### Fixed
* `EthernetUDP::begin` functions now call `stop()` if there was a bind error.
* Fixed `EthernetClient::setNoDelay(flag)` to actually use the `flag` argument.
  The function was always setting the TCP flag, regardless of the value of
  the argument.
* Fixed printing unknown netif name characters in some debug messages.
* Fixed `EthernetClient::connect()` and `close()` operations to check the
  internal connection object for NULL across `yield()` calls.
* Fixed `lwip_strerr()` buffer size to include the potential sign.
* Don't close the TCP pcb on error since it's already been freed.

* New non-blocking TCP connect functions,
* Callbacks for listening to network interface status,
* `EthernetClient::connect()` functions now return an int having one of the Arduino API-defined values,
* Raw frame support is now excluded by default,
* Added a check to ensure lwIP isn't called from an interrupt context, and
* Fixed some apparent freezes when using `EthernetClient`.

@shawn: Looking to set up a usable test of Ethernet on Teensy - no other goal or reason.

Any examples suitable for: In the end having 'sketches' on two T_4.1's able to send each other data on command.

Downloaded p#142 v0.18.0 fresh from github.
Ran: "...\libraries\QNEthernet\examples\IPerfServer\IPerfServer.ino"

On a T_4.1 with PJRC adapter and a Windows 11 PC online.

Code built fine with TD 1.58 beta 3 and ran with startup - don't even see a single warning!:
Starting IPerfServer...
MAC = 04:e9:e5:0e:cf:8d
Starting Ethernet with DHCP...
[Ethernet] Link ON, 100 Mbps, Half duplex
[Ethernet] Address changed:
    Local IP =
    Subnet   =
    Gateway  =
    DNS      =
Starting server on port 5001...done.
Build notes {nicely smaller than recent posts suggested}:
teensy_size: Memory Usage on Teensy 4.1:
teensy_size:   FLASH: code:145880, data:21400, headers:8844   free for files:7950340
teensy_size:    RAM1: variables:58176, code:143236, padding:20604   free for local variables:302272
teensy_size:    RAM2: variables:51776  free for malloc/new:472512
Using library QNEthernet at version 0.18.0 in folder: C:\T_Drive\tCode\libraries\QNEthernet 
Using library Entropy in folder: C:\Users\Tim\AppData\Local\Arduino15\packages\teensy\hardware\avr\0.58.3\libraries\Entropy (legacy)

Have not done packet test in some time - maybe not since this computer new ... 2 years ago ... not finding the app used before.
Downloaded first thing that came up:
Send & receive TCP, UDP, SSL. HTTP Requests. And Panel Generation.
> what is a good program to send/receive packets? The other I had would repeat packets etc.

Did a default PING and it was responded to "<1ms", but no output from IPerfServer.ino.
Ping larger than this times out:
C:\Users\Tim>ping [B]-l 1472[/B]
Pinging with 1472 bytes of data:
Reply from bytes=1472 time<1ms TTL=255
Reply from bytes=1472 time<1ms TTL=255
Reply from bytes=1472 time<1ms TTL=255
Reply from bytes=1472 time=1ms TTL=255

Ping statistics for
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 0ms, Maximum = 1ms, Average = 0ms

Started packetsender for TCP to on port 5001 { Thanks for those #'s in the STARTUP NOTES! }

If I load file 'IPerfServer.ino' and send this shows up?
Connection count: 1 Settings:
    amount=1951688056 Settings error: bytes=48
Connection count: 0

Many of those things look ODD?
> Is this good or bad? : Settings error: bytes=48
> Flags is HEX so okay?, but none of those other values seem sensible?
> was expecting 'Perf' numbers?

And most importantly: 'Good Work'
The IPerfServer example is designed to work with the `iperf` command. (I use a Mac, and used `brew` to install it. I currently have v2.1.8.) It's designed for a specific protocol; that's why you're seeing those strange numbers and "Settings error." See the notes at the top of IPerfServer.ino for usage information. With 'packetsender', you'd have to write a server or something that processes those packets somehow.

An example command for your setup:
iperf -c -i 1 -l 1460

Thanks for playing with the library! :)

Over Wi-Fi and a mesh device, I often get 15-20Mbps on my particular network. Over wired, it's faster. When directly connected to my laptop, I've seen speeds as high as 94 or 95Mbps. Lots of settings to play with to tune this in lwIP (and iperf).
Last edited:
@shawn - thanks.
Not knowing iPerf was 'a thing' I searched and found
Code built and didn't read it - but then sample command line image in head and couldn't find where I saw it ... DOH! - there it is in the example.

Just found:
Will get back to it ...

With iPerf3 for Windows - it is transferring: iperf3.exe -c -p 5001 -M -F "C:\T_Drive\tCode\libraries\QNEthernet\examples\IPerfServer\IPerfServer.ino"
Connection count: 2 Settings:
I'm not certain IPerfServer works with iperf3. See if you can get v2 working instead. To glean the protocol, I had to read a bunch of mystery code from different places. At the time I wrote that example, I couldn't find much in the way of protocol docs.
Thanks, as usual here - just being TEST'y. ...

For Windows works, and iperf3 does not.
Starting IPerfServer...
MAC = 04:e9:e5:0e:cf:8d
Starting Ethernet with DHCP...
[Ethernet] Link ON, 100 Mbps, Half duplex
[Ethernet] Address changed:
    Local IP =
    Subnet   =
    Gateway  =
    DNS      =
Starting server on port 5001...done.
Connection count: 1 Settings:
    amount=-1000 ExtSettings:
Connection count: 0
and cmdline:
c:\T_Drive\Programs\iperf>iperf-win-1_9_23.exe -c -i 1 -l 1460
Client connecting to, TCP port 5001
TCP window size: 64.0 KByte (default)
[  1] local port 52641 connected with port 5001
[ ID] Interval       Transfer     Bandwidth
[  1] 0.00-1.00 sec  4.79 MBytes  40.2 Mbits/sec
[  1] 1.00-2.00 sec  8.07 MBytes  67.7 Mbits/sec
[  1] 2.00-3.00 sec  7.33 MBytes  61.5 Mbits/sec
[  1] 3.00-4.00 sec  8.03 MBytes  67.3 Mbits/sec
[  1] 4.00-5.00 sec  7.85 MBytes  65.9 Mbits/sec
[  1] 5.00-6.00 sec  9.27 MBytes  77.8 Mbits/sec
[  1] 6.00-7.00 sec  10.0 MBytes  83.9 Mbits/sec
[  1] 7.00-8.00 sec  10.1 MBytes  84.3 Mbits/sec
[  1] 8.00-9.00 sec  9.75 MBytes  81.8 Mbits/sec
[  1] 9.00-10.00 sec  8.86 MBytes  74.3 Mbits/sec
[  1] 0.00-10.01 sec  84.0 MBytes  70.4 Mbits/sec

-d dual also works:
c:\T_Drive\Programs\iperf>iperf2 -c -i 1 -l 1460 -d
Client connecting to, TCP port 5001
TCP window size: 64.0 KByte (default)
Server listening on TCP port 5001
TCP window size: -1.00 Byte (default)
[  2] local port 5001 connected with port 58625
[  1] local port 53002 connected with port 5001
[ ID] Interval       Transfer     Bandwidth
[  2] 0.00-1.00 sec  8.16 MBytes  68.5 Mbits/sec
[  1] 0.00-1.00 sec  8.22 MBytes  69.0 Mbits/sec
[  2] 1.00-2.00 sec  8.79 MBytes  73.7 Mbits/sec
[  1] 1.00-2.00 sec  8.79 MBytes  73.8 Mbits/sec
[  2] 2.00-3.00 sec  7.99 MBytes  67.1 Mbits/sec
[  1] 2.00-3.00 sec  7.99 MBytes  67.0 Mbits/sec
[  2] 3.00-4.00 sec  8.86 MBytes  74.3 Mbits/sec
[  1] 3.00-4.00 sec  8.87 MBytes  74.4 Mbits/sec
[  2] 4.00-5.00 sec  8.61 MBytes  72.3 Mbits/sec
[  1] 4.00-5.00 sec  8.62 MBytes  72.3 Mbits/sec
[  2] 5.00-6.00 sec  8.74 MBytes  73.4 Mbits/sec
[  1] 5.00-6.00 sec  8.73 MBytes  73.3 Mbits/sec
[  2] 6.00-7.00 sec  8.57 MBytes  71.9 Mbits/sec
[  1] 6.00-7.00 sec  8.58 MBytes  71.9 Mbits/sec
[  2] 7.00-8.00 sec  8.80 MBytes  73.8 Mbits/sec
[  1] 7.00-8.00 sec  8.80 MBytes  73.8 Mbits/sec
[  2] 8.00-9.00 sec  8.41 MBytes  70.5 Mbits/sec
[  1] 8.00-9.00 sec  8.42 MBytes  70.6 Mbits/sec
[  2] 9.00-10.00 sec  8.95 MBytes  75.1 Mbits/sec
[  2] 0.00-10.00 sec  85.9 MBytes  72.1 Mbits/sec
[  1] 9.00-10.00 sec  8.94 MBytes  75.0 Mbits/sec
[  1] 0.00-10.01 sec  86.0 MBytes  72.1 Mbits/sec
I just released 0.19.0. The changes:

### Added
* Added `Ethernet.loop()` calls to the `EthernetClient::write()` functions when
  the send buffer is full.
* Added Ethernet hardware detection to support (well, "non-support") the
  Teensy 4.1 NE.

### Changed
* Updated _lwipopts.h_ to examine `LWIP_MDNS_RESPONDER` when setting
  certain values.
* `Ethernet.loop()` calls are now attached/detached to/from _yield_ in
* Improved pin configurations and comments.
* Disabled `LWIP_STATS` option by default. Saves a little memory.
* Updated Arduino Ethernet API links in _keywords.txt_.
* Only add 1 to `MEMP_NUM_SYS_TIMEOUT` option for mDNS instead of 3.
* Updated examples to use both address and link state on network changes. This
  accommodates when a static IP is used.
* Changed UDP and TCP PCB creation to use an appropriate `ip_addr_t` type
  instead of the unspecified default.
* Changed CRC-32 function for multicast lookup to not use a lookup table. This
  saves 1KiB of flash but makes the calculation about 4.7x slower but still in
  the microsecond range (~0.090µs -> ~0.42µs).
* Moved `EthernetClass`, `EthernetFrameClass`, and `MDNSClass` constructors and
  destructors to `FLASHMEM` (where possible). This saves a little RAM1 space.
* Moved lwIP's memory pools into 4-byte aligned `DMAMEM` (RAM2). This saves
  a lot of RAM1 space, about 27KiB with the current configuration.
* Changed the promiscuous mode macro name from `QNETHERNET_PROMISCUOUS_MODE`
* Changed all the DHCP timeouts in the examples to 15 seconds.
* Changed `EthernetUDP::send()` functions to return an lwIP error code instead
  of a 1-or-0 Boolean value. Zero (`ERR_OK`) means no error. This makes it
  easier to diagnose any problems.

### Fixed
* Reverted how interrupts were being cleared to use assignment instead of OR'ing
  the bits. This seemed to fix an apparent freeze. (See this issue:
* Fixed a signedness comparison warning in `OSCPrinter` example.
* Addressed "extra" (`-Wextra`) and pedantic (`-Wpedantic`) warnings.

* "Non"-support for Teensy 4.1 NE (no Ethernet). The library can now do some hardware detection.
* RAM1 use reduction by putting more things in RAM2 and a few things FLASHMEM, and disabling stats collection by default. About 27KiB less.
* Fixed some crashing programs by reverting how interrupts were being cleared. I'm not quite sure why I did that in the first place (in v0.18.0).

I just released 0.20.0. The changes:

### Added
* Added `EthernetFrameClass::size()`, for parity with `EthernetUDP`.
* Added internal entropy functions to remove the _Entropy_ library dependency.
* New `QNETHERNET_ENABLE_CUSTOM_WRITE` macro for enabling the inclusion of the
  expanded `stdio` output behaviour.
* Added a bunch of unit tests. These use the Unity test framework from
  within PlatformIO.
* Added sections to the README that describe how to configure compiler options
  for both the Arduino IDE and PlatformIO.
* Added an `AltcpTemplate` example.
* Added a `BroadcastChat` example that implements a simple chat over UDP.

### Changed
* Updated `StdioPrint` adapter to use errors better. `errno` is set for the
  "write error" value and the stdio error state is cleared appropriately when
  the functions detect that "write error" is back to zero.
* Changed default `stdio` output behaviour to use the new system default. (This
  exists as of Teensyduino 1.58-beta4.) See: `QNETHERNET_ENABLE_CUSTOM_WRITE`.
* Added a timeout parameter to the callback version of
  `DNSClient::getHostByName()`. This helps prevent any references from going out
  of scope before the callback is called.
* Changed `EthernetUDP::send()` functions back to returning a Boolean value.
* There's now `Print` objects for each of `stdout` and `stderr`: `stdoutPrint`
  and `stderrPrint`.
* Improved `RawFrameMonitor` and `SNTPClient` examples.
* Changed "tcp" calls to "altcp" calls so that it's easier to add things like
  TLS and proxy support. There's accompanying `qnethernet_get_allocator(...)`
  and `qnethernet_free_allocator(...)` functions that need to be defined by the
  application code if the `LWIP_ALTCP` option is enabled.

### Removed
* `QNETHERNET_WEAK_WRITE` macro in favour of the new way to enable the library's
  internal `_write()` definition. See: `QNETHERNET_ENABLE_CUSTOM_WRITE`.
* Removed sending a DHCP INFORM message when setting a static IP. It seemed to
  interfere with any first subsequent DHCP requests.

### Fixed
* Fixed `EthernetClass::end()` to call `clearEvent()` before detaching the
  event responder.
* Fixed DNS client to be aware of lookup failures.
* Added set-no-address and link-down calls to `EthernetClass::end()` before
  bringing the interface down. This ensures all the callbacks are called.
* Fixed `EthernetUDP::parsePacket()` to also call `Ethernet.loop()` when there's
  no packet available.
* Increased PHY reset pulse and reset-to-MDIO times. Hopefully this fixes slow
  traffic after restarting the system via `Ethernet.end()`.

* Internal entropy functions
* Unit tests (for running within PlatformIO)
* New README section describing how to configure compiler options in Arduino IDE and PlatformIO
* Two new examples: AltcpTemplate and BroadcastChat
* Change internal TCP calls to lwIP's "altcp" to make it easier to implement TLS and proxy TCP connections
* A few fixes and README updates

got an undefined error for:
stdPrint = &Serial;
deleting it worked. Now full circle as had to add it way back for an earlier version:)