or

Version: 0.38.0-snapshot
The QNEthernet library provides Ethernet functionality for the Teensy 4.1 and
possibly some other platforms. It’s designed to be compatible with the
Arduino-style Ethernet API.
This library is distributed under the “AGPL-3.0-or-later” license. Please
contact the author if you wish to inquire about other license options.
Please see the lwip-info/ directory for the info files provided with the
lwIP release.
Table of contents
- Introduction
- Three notes
- Other differences and notes
- Requirements
- Additional functions and features not in the Arduino-style API
Ethernet
EthernetClient
- TCP socket options
- IP header values
EthernetServer
EthernetUDP
- IP header values
parsePacket() return values
EthernetFrame
MDNS
DNSClient
- Ping
- Ping reply
- Print and Stream utilities
operator bool() and explicit
- Use of
errno
- How to run
- Concurrent use is not supported
- How to move the stack forward and receive data
- Link detection
- Notes on
yield()
- How to write data to connections
writeFully() with more break conditions
- Write immediacy
- A note on the examples
- A survey of how connections (aka
EthernetClient) work
- Connections and link/interface detection
connect() behaviour and its return values
- Non-blocking connection functions
- Getting the TCP state
- How to use multicast
- How to use listeners
- How to change the number of sockets
- UDP receive buffering
- mDNS services
- DNS
- stdio
- Adapt stdio files to the Print interface
- Raw Ethernet frames
- Promiscuous mode
- Raw frame receive buffering
- Raw frame loopback
- Raw frame filter hook
- How to implement VLAN tagging
- Application layered TCP: TLS, proxies, etc.
- About the allocator functions
- About the TLS adapter functions
- How to enable Mbed TLS
- Installing the Mbed TLS library
- Mbed TLS library install for Arduino IDE
- Mbed TLS library install for PlatformIO
- Implementing the altcp_tls_adapter functions
- Implementing the Mbed TLS entropy function
- On connections that hang around after cable disconnect
- Mitigations
- Notes on ordering and timing
- Notes on RAM1 usage (Teensy 4)
- Heap memory use
- Entropy generation
- The
random_device UniformRandomBitGenerator
- Interference mitigation
- Security features
- Secure TCP initial sequence numbers (ISNs)
- Disabling ICMP echo (ping) replies
- Configuration macros
- Configuring macros using the Arduino IDE
- Configuring macros using PlatformIO
- Changing lwIP configuration macros in
lwipopts.h
- Auxiliary tools
- Print and Stream tools
std::random_device-compatible uniform random bit generator
- Space-savings on some platforms
std::chrono-compatible Clocks
steady_clock_ms
arm_high_resolution_clock
elapsedTime<Clock>
- Complete list of features
- Compatibility with other APIs
- Other notes
- To do
- Code style
- References
Introduction
The QNEthernet library is designed to be a drop-in replacement for code using
the Arduino-style Ethernet API.
Note: Please read the function docs in the relevant header files for
more information.
Three notes
There are three notes, as follows:
- The
QNEthernet.h header must be included instead of Ethernet.h.
- Everything is inside the
qindesign::network namespace. In many cases,
adding the following at the top of your program will obviate the need to
qualify any object uses or make any other changes:using namespace qindesign::network;
- On non-Teensy platforms:
Ethernet.loop() must be called regularly, either
at the end of the main loop() function, or by hooking into yield().
(This is already done for you on the Teensy platform by hooking into
yield() via EventResponder.)
See: How to move the stack forward and receive data
For API additions beyond what the Arduino-style API provides, see:
Additional functions and features not in the Arduino-style API
Other differences and notes
Requirements
This library requires the following:
- C++11
- C11
- Somewhere in the range of 55-60Ki of flash, 5-10Ki of RAM, and some heap
memory, depending on the platform. Including more features will require
more space.
Additional functions and features not in the Arduino-style API
QNEthernet defines functions that don’t exist in the Arduino-style API as it’s
currently defined. (See:
Arduino Ethernet Reference)
This section documents those functions.
Features:
- The
read(buf, len) functions allow a NULL buffer so that the caller can skip
data without having to read into a buffer.
Ethernet
The Ethernet object is the main Ethernet interface.
-
begin(): Initializes the library, uses the Teensy’s internal MAC address or
some default MAC address, and starts the DHCP client. This returns whether
startup was successful. This does not wait for an IP address.
-
begin(ipaddr, netmask, gw): Initializes the library, uses the Teensy’s
internal MAC address or some default MAC address, and uses the given
parameters for the network configuration. This returns whether startup was
successful. The DNS server is not set. This starts the DHCP client if the IP
address is INADDR_NONE.
-
begin(ipaddr, netmask, gw, dns): Initializes the library, uses the Teensy’s
internal MAC address or some default MAC address, and uses the given
parameters for the network configuration. This returns whether startup was
successful. The DNS server is only set if dns is not INADDR_NONE; it
remains the same if dns is INADDR_NONE. This starts the DHCP client if the
IP address is INADDR_NONE.
Passing dns, if not INADDR_NONE, ensures that the DNS server IP is set
before the address-changed callback is called. The alternative approach to
ensure that the callback has all the information is to call
setDNSServerIP(ip) before the three-parameter version.
-
broadcastIP(): Returns the broadcast IP address associated with the current
local IP and subnet mask. If Ethernet is not initialized then this will return
255.255.255.255.
-
dnsServerIP(index): Gets a specific DNS server IP address. This returns
INADDR_NONE if the index is not in the exclusive range,
[0, DNSClient::maxServers()).
-
driverCapabilities(): Returns a DriverCapabilities struct containing the
driver’s set of capabilities.
Notes:
- If the link state is not detectable then it must be managed
with
setLinkState(flag).
-
end(): Shuts down the library, including the Ethernet clocks.
-
hostByName(hostname, ip): Convenience function that tries to resolve a
hostname into an IP address. This returns whether successful.
See also DNSClient.
-
hostname(): Gets the DHCP client hostname. An empty string means that no
hostname is set. The default is “qnethernet-lwip”.
-
interfaceName(): Returns the interface name, or, if Ethernet has not been
initialized, an empty string.
-
interfaceStatus(): Returns the network interface status, true for UP and
false for DOWN.
-
isDHCPActive(): Returns whether DHCP is active.
-
isDHCPEnabled(): Returns whether the DHCP client is enabled. This is valid
whether Ethernet has been started or not.
-
linkInfo(): Returns a LinkInfo struct containing information about
the link.
-
linkState(): Returns a bool indicating the link state. This returns true
if the link is on and false otherwise. This may be managed manually
with setLinkState(flag).
-
joinGroup(ip): Joins a multicast group.
-
leaveGroup(ip): Leaves a multicast group.
-
macAddress(): Convenience function that returns a pointer to the current
MAC address.
-
macAddress(mac): Fills the 6-byte mac array with the current MAC address.
Note that the equivalent Arduino function is MACAddress(mac).
-
ping(hostname[, ttl]): Pings a host, given by a string, and returns the
round trip time. The ttl parameter is optional. See also Ping.
-
ping(ip[, ttl]): Pings a host, given as an IPAddress, and returns the
round trip time. The ttl parameter is optional. See also Ping.
-
renewDHCP(): Renews any active DHCP lease and returns whether the request
was sent successfully.
-
setDHCPEnabled(flag): Enables or disables the DHCP client. This may be
called either before or after Ethernet has started. If DHCP is desired and
Ethernet is up, but DHCP is not active, an attempt will be made to start the
DHCP client if the flag is true. This returns whether that attempt was
successful or if no restart attempt is required.
-
setDNSServerIP(dnsServerIP): Sets the DNS server IP address. Note that the
equivalent Arduino function is setDnsServerIP(dnsServerIP).
-
setDNSServerIP(index, ip): Sets a specific DNS server IP address. This does
nothing if the index is not in the exclusive range,
[0, DNSClient::maxServers()).
-
setHostname(hostname): Sets the DHCP client hostname. The empty string will
set the hostname to nothing. To use something other than the default at system
start, call this before calling begin().
-
setLinkState(flag): Manually sets the link state. This is useful when using
the loopback feature. Network operations will usually fail unless there’s
a link.
-
setMACAddressAllowed(mac, flag): Allows or disallows Ethernet frames
addressed to the specified MAC address. This is useful when processing raw
Ethernet frames.
-
waitForLink(timeout): Waits for the specified timeout (milliseconds) for
a link to be detected. This is useful when setting a static IP and making
connections as a client. Returns whether a link was detected within the
given timeout.
-
waitForLocalIP(timeout): Waits for the specified timeout (milliseconds) for
the system to have a local IP address. This is useful when waiting for a
DHCP-assigned address. Returns whether the system obtained an address within
the given timeout. Note that this also works for a static-assigned address.
-
operator bool(): Tests if Ethernet is initialized.
-
Callback functions: Note that callbacks should be registered before any other
Ethernet functions are called. This ensures that all events are captured. This
includes Ethernet.begin(...).
onLinkState(cb): The callback is called when the link changes state, for
example when the Ethernet cable is unplugged.
onAddressChanged(cb): The callback is called when any IP settings have
changed. This might be called before the link or network interface is up if
a static IP is set.
onInterfaceStatus(cb): The callback is called when the network interface
status changes. It is called after the interface is up but before the
interface goes down.
-
static constexpr bool isPromiscuousMode(): Returns whether promiscuous mode
is enabled.
-
static const char* libraryVersion(): Returns the library version.
-
static constexpr size_t maxMulticastGroups(): Returns the maximum number of
available multicast groups, not including the “all systems” group.
-
static constexpr size_t mtu(): Returns the MTU.
EthernetClient
abort(): Aborts a connection without going through the TCP close process.
close(): Closes a connection, but without waiting. It’s similar to stop().
This is superseded by setConnectionTimeoutEnabled(flag) with stop().
closeOutput(): Shuts down the transmit side of the socket. This is a
half-close operation.
connectNoWait(ip, port): Similar to connect(ip, port), but it doesn’t
wait for a connection. This is superseded by
setConnectionTimeoutEnabled(flag) with connect(ip, port).
connectNoWait(host, port): Similar to connect(host, port), but it doesn’t
wait for a connection. Note that the DNS lookup will still wait. This is
superseded by setConnectionTimeoutEnabled(flag) with connect(host, port).
connecting(): Returns whether the client is in the middle of connecting.
This is used when doing a non-blocking connect.
connectionId(): Returns an ID for the connection to which the client refers.
It will return non-zero if connected and zero if not connected. Note that it’s
possible for new connections to reuse previously-used IDs.
connectionTimeout(): Returns the current timeout value.
isConnectionTimeoutEnabled(): Returns whether connection timeout is enabled.
localIP(): Returns the local IP of the network interface used for the
client. Currently, This returns the same value as Ethernet.localIP().
setConnectionTimeout(timeout): The parameter is a uint32_t and not a
uint16_t. The spec, as of this writing, specifies a uint16_t parameter.
setConnectionTimeoutEnabled(flag): Enables or disables use of a connection
timeout. If disabled, then calls to connect(...) and stop() won’t block.
This supersedes the connectNoWait(...) and close() calls.
status(): Returns the current TCP connection state. This returns one of
lwIP’s tcp_state enum values. To use with altcp, define the
LWIP_DEBUG macro.
write(const void*, size_t): Convenience function for writing data from
pointers of any type.
writeFully(b): Writes a single byte.
writeFully(s): Writes a string (const char *).
writeFully(buf, size): Writes a data buffer (const void *).
static constexpr size_t maxSockets(): Returns the maximum number of
TCP connections.
TCP socket options
setNoDelay(flag): Sets or clears the TCP_NODELAY flag in order to disable or
enable Nagle’s algorithm, respectively. This must be changed for each new
connection. Returns true if connected and the option was set, and
false otherwise.
isNoDelay(): Returns whether the TCP_NODELAY flag is set for the current
connection. Returns false if not connected.
outgoingDiffServ(): Returns the current value of the differentiated services
(DiffServ) field from the outgoing IP header, or zero if not connected.
setOutgoingDiffServ(ds): Sets the differentiated services (DiffServ) field
in the outgoing IP header, if connected. Returns true if connected and the
option was set, and false otherwise.
outgoingTTL(): Returns the current value of the TTL field from the outgoing
IP header, or zero if not connected.
setOutgoingTTL(ttl): Sets the TTL field in the outgoing IP header, if
connected. Returns true if connected and the option was set, and
false otherwise.
EthernetServer
begin(port): Starts the server on the given port, first disconnecting any
existing server if it was listening on a different port. This returns whether
the server was successfully started.
beginWithReuse(): Similar to begin(), but also sets the SO_REUSEADDR
socket option. This returns whether the server was successfully started.
beginWithReuse(port): Similar to begin(port), but also sets the
SO_REUSEADDR socket option. This returns whether the server was
successfully started.
end(): Shuts down the server.
port(): Returns the server’s port, a signed 32-bit value, where -1 means the
port is not set and a non-negative value is a 16-bit quantity.
write(const void*, size_t): Convenience function for writing data from
pointers of any type.
static constexpr size_t maxListeners(): Returns the maximum number of
TCP listeners.
EthernetServer(): Creates a placeholder server without a port. This form is
useful when you don’t know the port in advance.
All the begin functions call end() first only if the server is currently
listening and the port or reuse options have changed.
EthernetUDP
beginWithReuse(localPort): Similar to begin(localPort), but also sets the
SO_REUSEADDR socket option.
beginMulticastWithReuse(ip, localPort): Similar to
beginMulticast(ip, localPort), but also sets the SO_REUSEADDR socket option.
data(): Returns a pointer to the received packet data.
destIP(): Returns the destination IP of a received packet.
droppedReceiveCount(): Returns the total number of dropped received packets
since reception was started. Note that this is the count of dropped packets at
the layer above the driver.
localPort(): Returns the port to which the socket is bound, or zero if it is
not bound.
receiveQueueCapacity(): Returns the receive queue capacity.
receiveQueueSize(): Returns the number of packets currently in the
receive queue.
receivedTimestamp(): Returns the approximate packet arrival time, measured
with millis(). This is useful in the case where packets have been queued and
the caller needs the approximate arrival time. Packets are timestamped when
the UDP receive callback is called.
send(host, port, data, len): Sends a packet without having to use
beginPacket(), write(), and endPacket(). It causes less overhead. The
host can be either an IP address or a hostname.
setReceiveQueueCapacity(capacity): Changes the receive queue capacity. The
minimum possible value is 1 and the default is 1. If a value of zero is used,
it will default to 1. If the new capacity is smaller than the number of items
in the queue then all the oldest packets will get dropped.
size(): Returns the total size of the received packet data.
totalReceiveCount(): Returns the total number of received packets, including
dropped packets, since reception was started. Note that this is the count at
the layer above the driver.
write(const void*, size_t): Convenience function for writing data from
pointers of any type.
operator bool(): Tests if the socket is listening.
static constexpr size_t maxSockets(): Returns the maximum number of
UDP sockets.
EthernetUDP(capacity): Creates a new UDP socket having the specified receive
packet queue capacity. The minimum possible value is 1 and the default is 1.
If a value of zero is used, it will default to 1.
All the begin functions call stop() first only if the socket is currently
listening and the local port or reuse options have changed.
outgoingDiffServ(): Returns the current value of the differentiated services
(DiffServ) field from the outgoing IP header, or zero if the object hasn’t yet
been set up.
receivedDiffServ(): Returns the DiffServ value of the last received packet.
setOutgoingDiffServ(ds): Sets the differentiated services (DiffServ) field
in the outgoing IP header, setting up any necessary internal state. Returns
whether successful.
outgoingTTL(): Returns the current value of the TTL field from the outgoing
IP header, or zero if the object hasn’t yet been set up.
receivedTTL(): Returns the TTL value of the last received packet.
setOutgoingTTL(ttl): Sets the TTL field in the outgoing IP header, setting
up any necessary internal state. Returns whether successful.
parsePacket() return values
The EthernetUDP::parsePacket() function in QNEthernet is able to detect
zero-length UDP packets. This means that a zero return value indicates a
valid packet.
Many Arduino examples do the following to test whether there’s packet data:
int packetSize = udp.parsePacket();
if (packetSize) { // <-- THIS IS NOT CORRECT
// ...do something...
}
This is not correct because negative values mean that there’s no packet
available, and negative values are implicitly converted to a Boolean true.
Instead, the code should look like this:
int packetSize = udp.parsePacket();
if (packetSize >= non_negative_value) { // non_negative_value >= 0
// ...do something...
}
Note that if (packetSize > 0) would also be correct, or even something like
if (packetSize >= 4), just as long as the if (packetSize) form is not used.
EthernetFrame
The EthernetFrame object adds the ability to send and receive raw Ethernet
frames. It provides an API that is similar in feel to EthernetUDP. Because,
like EthernetUDP, it derives from Stream, the Stream API can be used to
read from a frame and the Print API can be used to write to the frame.
beginFrame(): Starts a new frame. New data can be added using the
Print API. This is similar to EthernetUDP::beginPacket().
beginFrame(dstAddr, srcAddr, typeOrLen): Starts a new frame and writes the
given addresses and EtherType/length.
beginVLANFrame(dstAddr, srcAddr, vlanInfo, typeOrLen): Starts a new
VLAN-tagged frame and writes the given addresses, VLAN info, and
EtherType/length.
clear(): Clears the outgoing and incoming buffers.
data(): Returns a pointer to the frame data.
destinationMAC(): Returns a pointer to the destination MAC.
droppedReceiveCount(): Returns the total number of dropped received frames
since reception was started. Note that this is the count of dropped frames at
the layer above the driver.
endFrame(): Sends the frame. This returns whether the send was successful. A
frame must have been started, its data length must be in the range 14-1514 for
non-VLAN frames or 18-1518 for VLAN frames, and Ethernet must have been
initialized. This is similar to EthernetUDP::endPacket().
etherTypeOrLength(): Returns the EtherType/length value immediately
following the source MAC. Note that VLAN frames are handled specially.
parseFrame(): Checks if a new frame is available. This is similar
to EthernetUDP::parseFrame().
payload(): Returns a pointer to the payload immediately following the
EtherType/length field. Note that VLAN frames are handled specially.
receiveQueueCapacity(): Returns the receive queue capacity.
receiveQueueSize(): Returns the number of frames currently in the
receive queue.
receivedTimestamp(): Returns the approximate frame arrival time, measured
with millis(). This is useful in the case where frames have been queued and
the caller needs the approximate arrival time. Frames are timestamped when
the unknown ethernet protocol receive callback is called.
send(frame, len): Sends a raw Ethernet frame without the overhead of
beginFrame()/write()/endFrame(). See the description of endFrame() for
size limits. This is similar to EthernetUDP::send(data, len).
setReceiveQueueCapacity(capacity): Sets the receive queue capacity. The
minimum possible value is 1 and the default is 1. If a value of zero is used,
it will default to 1. If the new capacity is smaller than the number of items
in the queue then all the oldest frames will get dropped.
size(): Returns the total size of the frame data.
sourceMAC(): Returns a pointer to the source MAC.
totalReceiveCount(): Returns the total number of received frames, including
dropped frames, since reception was started. Note that this is the count at
the layer above the driver.
write(const void*, size_t): Convenience function for writing data from
pointers of any type.
static constexpr size_t maxFrameLen(): Returns the maximum frame length, not
including the 4-byte FCS. Note that this size includes VLAN frames, which are
4 bytes larger.
static constexpr size_t minFrameLen(): Returns the minimum frame length, not
including the 4-byte FCS. Note that padding does not need to be managed by the
caller, meaning frames smaller than this size are allowed; the system will
insert padding as needed.
MDNS
The MDNS object provides an mDNS API.
begin(hostname): Starts the mDNS responder and uses the given hostname as
the name. If the responder is already running and the hostname is different
then the current state is renamed.
end(): Stops the mDNS responder.
addService(type, protocol, port): Adds a service. The protocol will be set
to "_udp" for anything other than "_tcp". The strings should have a "_"
prefix. Uses the hostname as the service name.
addService(type, protocol, port, getTXTFunc): Adds a service and associated
TXT records.
hostname(): Returns the hostname if the responder is running and an empty
string otherwise.
removeService(type, protocol, port): Removes a service.
restart(): Restarts the responder, for use when the cable has been
disconnected for a while and then reconnected. This isn’t normally needed
because the responder already watches for link reconnect.
operator bool(): Tests if the mDNS responder is operating.
static constexpr size_t maxServices(): Returns the maximum number of
supported services.
DNSClient
The DNSClient class provides an interface to the DNS client.
See also: Ethernet’s hostByName(hostname, ip)
setServer(index, ip): Sets a DNS server address.
getServer(index): Gets a DNS server address.
getHostByName(hostname, callback, timeout): Looks up a host by name and
calls the callback when there’s a result. The callback is not called once the
timeout has been reached. The timeout is ignored if it’s set to zero.
getHostByName(hostname, ip, timeout): Looks up a host by name.
static constexpr size_t maxServers(): Returns the maximum number of
DNS servers.
Ping
The Ping class provides a way to send ICMP echo requests and receive replies.
See also: Ethernet’s ping(host[, ttl])
How to use:
- Enable ping send support (or raw IP support) with the
QNETHERNET_ENABLE_PING_SEND configuration macro (or LWIP_RAW).
- Create an instance of the
Ping class.
- Provide a reply callback function either in the constructor or with a call
to
setCallback(f). (See the QNPing.h header for more details.)
Ping ID, timeout, and TTL all have defaults. These are defined
in qnethernet_opts.h.
Ping reply
The above describes how to send ICMP echo requests. It is also possible to
disable echo replies so that the stack does not respond to echo requests. Simply
set the QNETHERNET_ENABLE_PING_REPLY configuration macro to zero.
See also:
Disabling ICMP echo (ping) replies
Print and Stream utilities
The qnethernet/util/PrintUtils.h file declares some useful output and input
functions and classes. Note that this file is included when QNEthernet.h is
included; there’s no need to include it separately.
The functions and classes are in the qindesign::network::util namespace, so if
you’ve already added using namespace qindesign::network; to your code, they
can be called with util::writeMagic() syntax. Otherwise, they need to be fully
qualified: qindesign::network::util::writeMagic().
Functions:
-
writeFully(Print&, buf, size, breakf = nullptr): Attempts to completely
write bytes to the given Print object; the optional breakf function is
used as the stopping condition. It returns the number of bytes actually
written. The return value will be less than the requested size only if
breakf returned true before all bytes were written. Note that a NULL
breakf function is assumed to return false.
For example, the EthernetClient::writeFully(...) functions use this and
pass the “am I disconnected” condition as the breakf function.
-
writeMagic(Print&, mac, breakf = nullptr): Writes the payload for a
Magic packet to
the given Print object. This uses writeFully(...) under the covers and
passes along the breakf function as the stopping condition.
Classes:
-
NullPrint: A Print object that sends all data nowhere.
-
PrintDecorator: A Print decorator meant to be used as a base class. This
provides printf checking on those platforms that don’t by default in their
Print implementation.
-
StdioPrint: A Print decorator for stdio output files. It provides a
Print interface so that it is easy to print Printable objects to stdout
or stderr without having to worry about buffering and the need to flush any
output before printing a Printable directly to, say, Serial.
-
StreamDecorator: A Stream decorator meant to be used as a base class.
This provides printf checking on those platforms that don’t by default in
their Stream/Print implementation.
All the classes implement a write(const void*, size_t) convenience function
for writing data from pointers of any type.
operator bool() and explicit
All the operator bool() functions in the API are marked as explicit. This
means that you might get compiler errors in some circumstances when trying to
use a Boolean-convertible object.
You can use the object as a Boolean expression. For example in an if statement
or ternary conditional.
You can’t return the object as a bool from a function. For example, the
following code should give a compiler error:
EthernetClient client_;
bool isConnected() {
return client_;
}
Instead, use the following code; it fixes the problem:
EthernetClient client_;
bool isConnected() {
return client_ ? true : false;
// Or this:
// if (client_) {
// return true;
// } else {
// return false;
// }
}
This will also work:
bool isConnected() {
return static_cast<bool>(client_);
}
See also:
- The safe bool problem
explicit specifier
Use of errno
When a function call fails, it is often the case that errno will be set to
something appropriate. See the function documentation of interest in the
relevant header for more information.
If some networking feature is not implemented, for example, DNS is disabled,
then associated functions will set errno to ENOSYS.
How to run
This library works with both PlatformIO and Arduino. To use it with Arduino,
here are a few steps to follow:
-
Add #include <QNEthernet.h>. Note that this include already includes the
header for EthernetUDP. Some external examples also include SPI.h. This
is not needed unless you’re actually using SPI in your program.
-
Below that, add: using namespace qindesign::network;
-
You likely don’t want or need to set/choose your own MAC address, so just
call Ethernet.begin() with no arguments to use DHCP, and the three- or
four-argument version (IP, subnet mask, gateway[, DNS]) to set your own
address. If you really want to set your own MAC address, see
setMACAddress(mac) or one of the begin(...) functions that takes a MAC
address parameter.
-
There is an Ethernet.waitForLocalIP(timeout) convenience function that can
be used to wait for DHCP to supply an address because Ethernet.begin()
doesn’t wait. Try 15 seconds (15,000 ms) and see if that works for you.
Alternatively, you can use listeners to watch for
address, link, and network interface activity changes. This obviates the need
for waiting and is the preferred approach.
-
Ethernet.hardwareStatus() can detect both the Ethernet and no-Ethernet
versions of the hardware.
-
Most other things should be the same.
Please see the examples for more things you can do with the API, including but
not limited to:
- Using listeners to watch for network changes,
- Monitoring and sending raw Ethernet frames, and
- Setting up an mDNS service.
How to move the stack forward and receive data
All reception is processed in Ethernet.loop(). There’s no thread or ISR that
regularly processes the input. This means that this function must be called
regularly. For example, it could be called at the end of the main loop()
function. Another good place is to hook into yield() because the Arduino
framework calls that every time loop() finishes and likely during a call
to delay().
On the Teensy platform, the call is already hooked into yield() via the
built-in EventResponder approach, so there’s nothing more to add.
Concurrent use is not supported
Concurrent use of QNEthernet is not currently supported. This includes ISR
approaches and multi-threading approaches.
First, the underlying lwIP stack must be configured and used a certain way in
order to provide concurrent support. QNEthernet does not configure lwIP for
this. Second, the QNEthernet API, the layer on top of lwIP, isn’t designed for
concurrent use.
Link detection
Normally, a link is detected by the driver at some polling rate. (For the
curious, see driver_poll() and its uses.) However, it’s possible for a driver
to not be able to detect a link. For example, the W5100 chip is unable to read
this state.
Since the underlying lwIP stack depends on the link being up in order to operate
properly, a project will need to manage the link state itself for those drivers.
The suggestion is this:
- Start Ethernet as you normally would.
- If successful, check
Ethernet.driverCapabilities().isLinkStateDetectable.
- If
false, then call Ethernet.setLinkState(true).
Code example:
if (ethernet_is_started &&
!Ethernet.driverCapabilities().isLinkStateDetectable) {
Ethernet.setLinkState(true);
}
Notes on yield()
It may be the case that the yield() function is overridden, say to implement
cooperative multitasking. If that implementation doesn’t call Ethernet.loop()
then there are two things to be aware of:
Ethernet.loop() needs to be called regularly somewhere. One good place is
at the end of the main program loop.
- Any library functions that use
yield() while waiting for an event, say in
Ethernet.waitForLocalIP() or EthernetClient::connect(), need to call
Ethernet.loop() during the wait, otherwise the stack won’t move forward and
the event will never occur. A good place to do this is after the
yield() call.
To accomplish #2, there is a configuration macro,
QNETHERNET_DO_LOOP_IN_YIELD, that enables a call to Ethernet.loop() after
each yield() call, guaranteeing that the stack moves forward. See the
Configuration macros section.
The complete list of functions, as of this writing, that use yield() is:
DNSClient::getHostByName()
EthernetClass::waitForLink()
EthernetClass::waitForLocalIP()
EthernetClient::connect()
EthernetClient::stop()
How to write data to connections
I’ll start with these statements:
- Don’t use the
printX(…) functions when writing data
to connections.
- Always check the
write(...) and printX(…) return
values, retrying if necessary.
- Data isn’t necessarily sent immediately.
The write(...) and printX(…) functions in the Print
API all return the number of bytes actually written. This means that you must
always check the return value, retrying any missing bytes if you want all your
data to get sent.
For example, the following code won’t necessarily send all 250×102 bytes.
Buffers might get full. There might be retries. Etcetera.
void sendTestData(EthernetClient& client) {
for (int i = 0; i < 250; i++) {
// 102-byte string (println appends CRLF)
client.println("1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890");
}
}
The following modification will print a message every time the number of bytes
actually written does not match the number of bytes sent to the function. You
might find that the message prints one or more times.
void sendTestData(EthernetClient& client) {
for (int i = 0; i < 250; i++) {
// 102-byte string (println appends CRLF)
size_t written = client.println("1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890");
if (written != 102) {
// This is not an error!
Serial.println("Didn't write fully");
}
}
}
The solution is to utilize the raw write(...) functions and retry any bytes
that aren’t sent. Let’s create a writeFully(...) function and use that to send
the data:
// Keep writing until all the bytes are sent or the connection
// is closed.
void writeFully(EthernetClient& client, const char* data, size_t len) {
// Don't use client.connected() as the "connected" check because
// that will return true if there's data available, and this loop
// does not check for data available or remove it if it's there.
while (len > 0 && client) {
size_t written = client.write(data, len);
len -= written;
data += written;
}
}
void sendTestData(EthernetClient& client) {
for (int i = 0; i < 250; i++) {
// 102-byte string
size_t written = writeFully(client, "1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"\r\n");
}
}
Note that the library implements writeFully(...); you don’t have to roll
your own.
Rewriting this to use the library function:
void sendTestData(EthernetClient& client) {
for (int i = 0; i < 250; i++) {
// 102-byte string
size_t written = client.writeFully("1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"1234567890"
"\r\n");
}
}
Let’s go back to our original statement about not using the
printX(…) functions. Their implementation is opaque and
they sometimes make assumptions that the data will be “written fully”. For
example, Teensyduino’s current print(const String&) implementation attempts
to send all the bytes and returns the number of bytes sent, but it doesn’t tell
you which bytes were sent. For the string "12345", print(s) might send
"12", fail to send "3", and successfully send "45", returning the value 4.
Similarly, we have no idea what print(const Printable& obj) does because the
Printable implementation passed to it is beyond our control. For example,
Teensyduino’s IPAddress::printTo(Print&) implementation prints the address
without checking the return value of the print(...) calls.
Also, most examples I’ve seen that use any of the
printX(…) functions never check the return values.
Common practice seems to stem from this style of usage. Network applications
work a little differently, and there’s no guarantee all the data gets sent.
The write(...) functions don’t have this problem (unless, of course, there’s a
faulty implementation). They attempt to send bytes and return the number of
bytes actually sent.
In summary, my strong suggestion is to use the write(...) functions when
sending network data, checking the return values and acting on them. Or you can
use the library’s writeFully(...) functions.
See the discussion at the Teensy Forum:
NativeEthernet stalling with dropped packets
writeFully() with more break conditions
By default, the EthernetClient versions of writeFully() wait until the
connection is closed. However, TCP connections can sometimes persist for a long
time after a cable/link disconnect, and a user might wish to only wait so long
for a connection to return. This can be solved by adding additional checks to
the stopping condition function (the breakf parameter).
For example, to break on connection close or link down:
size_t writeFully(EthernetClient& c, const uint8_t* buf, size_t size) {
return qindesign::network::util::writeFully(c, buf, size, [&c]() {
return !static_cast<bool>(c) || !Ethernet.linkState();
});
}
To break on connection close or timeout:
size_t writeFully(EthernetClient& c, const uint8_t* buf, size_t size,
uint32_t timeout) {
uint32_t startT = millis();
return qindesign::network::util::writeFully(
c, buf, size, [&c, startT, timeout]() {
return !static_cast<bool>(c) || (millis() - startT) >= timeout;
});
}
See also:
Data isn’t necessarily completely sent across the wire after write(...) or
writeFully(...) calls. Instead, data is merely enqueued until the internal
buffer is full or a timer expires. Now, if the data to send is larger than the
internal TCP buffer then data will be sent and the extra data will be enqueued.
In other words, data is only sent when either the buffer is full or an internal
timer has expired.
To send any buffered data, call flush().
To quote lwIP’s tcp_write() docs:
Write data for sending (but does not send it immediately).
It waits in the expectation of more data being sent soon (as
it can send them more efficiently by combining them together).
To prompt the system to send data now, call tcp_output() after
calling tcp_write().
flush() is what always calls tcp_output() internally. The write(...) and
writeFully(...) functions only call this when the buffer is full. The
suggestion is to call flush() when done sending a “packet” of data, for some
definition of “packet” specific to your application. For example, after sending
a web page to a client or after a chunk of data is ready for the server
to process.
There is a configuration option, QNETHERNET_FLUSH_AFTER_TCP_WRITE, that causes
an automatic flush after data is written. However, this may reduce TCP
efficiency. This option is for use with hard-to-modify code or libraries that
assume data will get sent immediately. The preferred approach is to call flush()
in the code or library.
A note on the examples
The examples aren’t meant to be simple. They’re meant to be functional. There
are plenty of Arduino-style Ethernet library examples out there. This library
does not aim to be just like the others, it aims to provide some more powerful
tools to enable more powerful programs. The Teensy 4.1 is a serious device; it
demands serious examples.
An attempt was made to use a more robust programming style and modern C++ tools.
For those not as experienced with C++ or larger projects, these may have a
steeper learning curve. Hopefully this brings up lots of questions and an
exposure to new concepts. For example, state machines, lambdas, callbacks,
data structures, vector::emplace_back, and std::move.
For those that do have more C++ and larger project experience, I invite you to
improve or add to the examples so that the set of examples here becomes what
you’ve always hoped great library examples could be.
A survey of how connections (aka EthernetClient) work
Hopefully this section disambiguates some details about what each function does:
connected(): Returns whether connected OR data is still available
(or both).
operator bool(): Returns whether connected (at least in QNEthernet).
available(): Returns the amount of data available, whether the connection
is closed or not.
read(...): Reads data if there’s data available, whether the connection is
closed or not.
Connections will be closed automatically if the client shuts down a connection,
and QNEthernet will properly handle the state such that the API behaves as
expected. In addition, if a client closes a connection, any buffered data will
still be available via the client API. If it were up to me, I’d have swapped the
meaning of operator bool() and connected(), but see the above list as
a guide.
Some options:
- Keep checking
connected() (or operator bool()) and
available()/read(...) to keep reading data. The data will run out when
the connection is closed and after all the buffers are empty. The calls to
connected() (or operator bool()) will indicate connection status (plus
data available in the case of connected() or just connection state in the
case of operator bool()).
- Same as the above, but without one of the two connection-status calls
(
connected() or operator bool()). The data will just run out after
connection-closed and after the buffers are empty.
Connections and link/interface detection
A link and active network interface must be present before a connection can be
made. Either call Ethernet.waitForLink(timeout) or check the link state or
network interface status before attempting to connect. Which approach you use
will depend on how your code is structured or intended to be used.
Be aware when using a listener approach to start or stop services that it’s
possible, when setting a static IP, for the address-changed callback to be
called before the link or network interface is up.
Note that this section also applies to the DNS client.
connect() behaviour and its return values
Firstly, connect() blocks. See the
next section for a non-blocking way
to connect.
The Arduino-style API,
client.connect(),
used to define a set of possible int return values for this function, but now
it returns a Boolean value indicating success. Note that the function signatures
still return an int.
Non-blocking connection functions
The connectNoWait() functions implement non-blocking TCP connections. These
functions behave similarly to connect(), however they do not wait for the
connection to be established.
To check for connection establishment, simply call connecting() to determine
if the client is still in the process of connecting, and then either
connected() or the Boolean operator. If a connection can’t be established then
close() must be called on the object.
Note that DNS lookups for hostnames will still wait.
Getting the TCP state
TCP connections can be in one of
several states.
Knowing a connection’s underlying TCP state can be useful, for example, to avoid
waiting for a timeout via reading data.
This state can be retrieved using an EthernetClient’s status() function.
Note that, to avoid modiyfing the lwIP code too much, the LWIP_DEBUG macro
must be defined when using this function with altcp. If not using it with
altcp, there’s no such concern.
The states, as defined by lwIP’s tcp_state enum:
- CLOSED
- LISTEN
- SYN_SENT
- SYN_RCVD
- ESTABLISHED
- FIN_WAIT_1
- FIN_WAIT_2
- CLOSE_WAIT
- CLOSING
- LAST_ACK
- TIME_WAIT
This enum isn’t a C++ “enum class”, so its values can be used directly. The
definition is already included by qnethernet/QNEthernetClient.h, which is, in
turn, included by QNEthernet.h.
References:
- TCP states
- consider adding
status() in EthernetClient · Issue #52 · ssilverman/QNEthernet
- WiFiNINA - client.status() - Arduino Reference
(client.status())
How to use multicast
There are a few ways in the API to utilize multicast to send or receive packets.
For reception, you must join a multicast group.
The first is by using the EthernetUDP::beginMulticast(ip, port) function. This
both binds to a specific port, for only receiving traffic on that port, and
joins the specified group address. It’s similar to
EthernetUDP::begin(localPort) in that the socket will “own” the port.
Since only one socket at a time can be bound to a specific port, you will need
to use the same socket if you want to receive traffic sent to multiple groups on
the same port. You can accomplish this by either calling
beginMulticast(ip, port) multiple times, or by using begin(localPort) once,
and then calling Ethernet.joinGroup(ip) for each group you want to join.
To send multicast traffic, simply send to the appropriate IP address. There’s no
need to join a group.
The lwIP stack keeps track of a group “use count”. This means:
- That
joinGroup(ip) can be called multiple times, it just needs to be paired
with a matching number of calls to leaveGroup(ip). Each call increments an
internal count.
- Each call to
leaveGroup(ip) decrements a count, and when that count reaches
zero, the stack actually leaves the group.
How to use listeners
Instead of waiting for certain states at system start, for example link-up or
address-changed, it’s possible to watch for state changes using listeners, and
then act on those state changes. This will make your application more robust and
responsive to state changes during program operation.
Note that callbacks should be registered before any other Ethernet functions are
called. This ensures that all events are captured. This includes
Ethernet.begin(...).
The relevant functions are (see the Ethernet section for further
descriptions):
Ethernet.onLinkState(cb)
Ethernet.onAddressChanged(cb)
Ethernet.onInterfaceStatus(cb)
Link-state events occur when an Ethernet link is detected or lost.
Address-changed events occur when the IP address changes, but its effects are
a little more subtle. When setting an address via DHCP, the link and network
interface must already be up in order to receive the information. However, when
setting a static IP address, the event may occur before the link or network
interface is up. This means that if a connection or DNS lookup is attempted when
it is detected that the address is valid, the attempt will fail.
Interface-status events happen when the network interface comes up or goes
down. No network operations can happen before the network interface is up. For
example, when setting a static IP address, the address-changed event may occur
before the network interface has come up. This means that, for example, any
connection attempts or DNS lookup attempts will fail.
It is suggested, therefore, that when taking an action based on an
address-changed event, the link state and network interface status are checked
in addition.
Servers, on the other hand, can be brought up even when there’s no link or
active network interface.
In summary, no network operations can be done before all three of link-up,
address_changed-to-valid, and interface-up occur.
Last, no network tasks should be performed inside the listeners. Instead, set a
flag and then process that flag somewhere in the main loop.
See: Connections and link/interface detection
How to change the number of sockets
lwIP preallocates almost everything. This means that the number of sockets (UDP,
TCP, etc.) the stack can handle is set at compile time. The following table
shows how to change the number for each socket type.
| Socket Type |
Macro in lwipopts.h |
Notes |
| UDP |
MEMP_NUM_UDP_PCB |
|
| TCP |
MEMP_NUM_TCP_PCB |
Simultaneously active |
| TCP (listening) |
MEMP_NUM_TCP_PCB_LISTEN |
Listening |
UDP receive buffering
If UDP packets come in at a faster rate than they are consumed, some may get
dropped. To help mitigate this, the EthernetUDP(capacity) constructor can be
called with a value > 1. The minimum capacity is 1, meaning any new packets will
cause any existing packet to get dropped. If it’s set to 2 then there will be
space for one additional packet for a total of 2 packets, and so on. Setting a
value of zero will use the default of 1.
mDNS services
It’s possible to register mDNS services. Some notes:
- Similar to
Ethernet, there is a global MDNS object. It too is in the
qindesign::network namespace.
- It’s possible to add TXT items when adding a service. For example, the
following code adds “path=/” to the TXT of an HTTP service:
MDNS.begin("Device Name");
MDNS.addService("_http", "_tcp", 80, []() {
return std::vector<String>{"path=/"};
});
You can add more than one item to the TXT record by adding to the vector.
- When adding a service, the function that returns TXT items defaults to NULL,
so it’s not necessary to specify that parameter. For example:
MDNS.begin("Device Name");
MDNS.addService("_http", "_tcp", 80);
- The host name is normally used as the service name, but there are also
functions that let you specify the service name. For example:
MDNS.begin("Host Name");
MDNS.addService("my-http-service", "_http", "_tcp", 80);
DNS
The library interfaces with DNS using the DNSClient class. Note
that all the functions are static.
Things you can do:
- Look up an IP address by name, and
- Set multiple DNS servers.
The Ethernet.setDNSServerIP(ip) function sets the zeroth DNS server address
and the Ethernet.setDNSServerIP(index, ip) function sets the nth DNS server
address. Corresponding dnsServerIP() functions get the DNS server addresses.
See also: Ethernet’s hostByName(hostname, ip)
stdio
Internally, lwIP uses printf for debug output and assertions. QNEthernet
defines its own _write() function so that it has more control over stdout
and stderr than that provided by the internal output support.
Note: As of Teensyduino 1.58-beta4, there’s internal support for printf,
partially obviating the need for QNEthernet to define its own. However, it
always maps all of stdin, stdout, and stderr specifically to Serial.
Compared to the internal printf support, the QNEthernet version:
- Can map output to any
Print interface, not just Serial,
- Can separate
stdout and stderr outputs, and
- Disallows
stdin as a valid output.
To enable the QNEthernet version, set the QNETHERNET_CUSTOM_WRITE macro to
1 and set the stdoutPrint or stderrPrint variables to point to a valid
Print implementation. Note that if the feature is disabled, then neither
stdoutPrint nor stderrPrint will be defined.
Both variables default to NULL.
For example:
// Define QNETHERNET_CUSTOM_WRITE somewhere
void setup() {
Serial.begin(115200);
while (!Serial && (millis() </string></bool></bool></qnethernet></bool></clock>
> README 内容过长,已截取前 64 KB。完整内容请前往 GitHub 仓库查看。
评论