Using the Network
This page covers everything you need after lwip_start() to open
connections, serve requests, and diagnose network problems.
Before starting, make sure the stack is initialized and the network interface is up:
if (!lwip_start()) return 1;
if (!lwip_network_up()) return 1;
See Getting Started for the full setup sequence including the
required BSSHEAP_LOW makefile setting.
Socket-Style v. PCB-Level API
lwip.h provides a socket-style API so that users familiar with sockets but not PCB-level programming can use a familiar API in their programs. If you want fine-grained control, use the PCB-level API for:
ALTCP abstraction layer — wraps TCP, TLS, WebSocket
The full lwIP raw/callback API is documented by the lwIP project. If you don’t need fine-grained control, the socket-style API described below covers most use cases.
Creating a Socket
Sockets are opaque, heap-allocated handles. lwip_socket_create() returns
an owned pointer; check it for NULL and release it with
lwip_socket_destroy() when finished. Do not embed struct lwip_socket
in another structure or allocate it on the stack. Use accessors such as
lwip_socket_status() and lwip_socket_last_error() to inspect it.
-
struct lwip_socket *lwip_socket_create(lwip_socket_type_t type, lwip_socket_bind_descriptor_t bind, const lwip_socket_addrinfo_t *addrinfo, uint32_t timeout_ms)
Allocate and initialize a socket handle. Non-blocking — records the netif preference, applies static IP when an interface is available, and requests the services needed by the transport. The interface can appear after creation;
lwip_socket_connect()awaits readiness asynchronously while your main loop callslwip_service_events().- Parameters:
type – Transport selector (
lwip_socket_type_t). See table below.bind – Netif preference (
lwip_socket_bind_descriptor_t).LWIP_NETIF_EXTselects USB Ethernet,LWIP_NETIF_ANYaccepts loopback or external, andLWIP_NETIF_LOOPrestricts to loopback. Selecting an interface does not wait for it to become ready.addrinfo –
NULLuses DHCP on external interfaces. A non-NULLlwip_socket_addrinfo_tsupplies staticip/netmask/gatewayinstead. The address settings are copied into the socket.timeout_ms – Inactivity watchdog window in milliseconds, covering service wait, connection establishment, TLS handshake, and data transfer. Network activity extends the deadline.
0disables the inactivity timeout; the application manages the socket’s lifetime.
- Returns:
Owned socket pointer, or
NULLon failure, including allocation failure, invalid arguments, an unsupported transport, a stopped stack, or failure to initialize the requested transport.
Sockets own an RX ring buffer read with lwip_socket_read(). By default it
starts at 512 bytes and grows up to 4 KiB. Use the extended creation function
to choose a different maximum.
-
struct lwip_socket *lwip_socket_create_ex(lwip_socket_type_t type, lwip_socket_bind_descriptor_t bind, const lwip_socket_addrinfo_t *addrinfo, uint32_t timeout_ms, size_t rx_ring_max)
Like
lwip_socket_create(), with an explicit RX ring maximum. The returned pointer has the same ownership and readiness behavior.The ring starts at
LWIP_SOCKET_RX_RING_INIT_SIZE(512 B), clamped to the selected maximum, and grows inLWIP_SOCKET_RX_RING_STEP_SIZE(512 B) increments.rx_ring_max = 0selects the defaultLWIP_SOCKET_RX_RING_MAX_SIZE(4096 B).- Parameters:
type – Transport selector (
lwip_socket_type_t).bind – Netif preference (
lwip_socket_bind_descriptor_t).addrinfo –
NULLfor DHCP on external interfaces; non-NULLfor static IPv4.timeout_ms – Inactivity watchdog in milliseconds.
0disables it.rx_ring_max – Hard ceiling for the RX ring in bytes.
0usesLWIP_SOCKET_RX_RING_MAX_SIZE(4096 B).
- Returns:
Owned socket pointer, or
NULLon failure.
Transport selectors:
Protocol |
Meaning |
|---|---|
|
Raw TCP via lwIP |
|
UDP via lwIP |
|
ALTCP using the default TCP allocator. |
|
ALTCP wrapped in the CE TLS client path. Requires TLS enabled in the app configuration wizard. |
|
WebSocket over plain TCP (RFC 6455). |
|
WebSocket over TLS. Also requires TLS enabled in wizard. |
An example use case is given below:
struct lwip_socket *s = lwip_socket_create(
LWIP_SOCKET_TCP, LWIP_NETIF_EXT, NULL, 30000);
if (!s)
return 1;
/* TCP, external interface, DHCP, 30-second inactivity timeout.
* Use s directly in socket calls, then release the owned handle. */
lwip_socket_destroy(s);
Using Websockets
When using LWIP_SOCKET_ALTCP_WS or LWIP_SOCKET_ALTCP_WSS, you will need to call one additional function to attach special configuration to the socket. The full function specification is below.
-
lwip_error_t lwip_socket_set_ws_config(struct lwip_socket *socket, const char *path, const char *subprotocol)
Set the WebSocket resource path and optional subprotocol for a
LWIP_SOCKET_ALTCP_WSorLWIP_SOCKET_ALTCP_WSSsocket. Must be called afterlwip_socket_create()and beforelwip_socket_connect(). Has no effect on non-WebSocket socket types.- Parameters:
socket – A WS or WSS socket handle.
path – WebSocket resource path, e.g.
"/". Borrowed — must remain valid untillwip_socket_connect()returns.subprotocol – Optional
Sec-WebSocket-Protocolvalue, orNULL. Borrowed under the same lifetime constraint aspath.
- Returns:
LWIP_OKon success,LWIP_ERR_ARGifsocketorpathisNULL.
Socket Reconfiguration
Some socket attributes are modifiable in flight should the situation call for it.
-
lwip_error_t lwip_socket_set_timeout(struct lwip_socket *socket, uint32_t timeout_ms)
Update the socket’s timeout window after creation. It covers service wait, connection establishment, handshake, and data transfer. The watchdog re-arms on the next service tick; network activity extends the deadline.
- Parameters:
socket – An initialised socket handle.
timeout_ms – New watchdog window in milliseconds.
0disarms the watchdog entirely (use with care — a stalled connect will never time out).
- Returns:
LWIP_OKon success,LWIP_ERR_ARGifsocketisNULL.
-
lwip_error_t lwip_socket_resize_rx_max(struct lwip_socket *socket, size_t new_max)
Resize the RX ring’s ceiling on an existing socket, growing or shrinking it immediately. The ring’s initial size (set once at creation) is never changed by this call, and data already buffered is preserved. This fails only if there’s more data already in the ring than
new_maxcould hold (shrinking), or there isn’t enough heap to grow — it never requires the ring to be empty first.Prefer
lwip_socket_create_ex()when you know the right ceiling up front. Use this function when the ceiling needs to change after creation — for example, after reading aContent-Lengthheader that tells you the response will be larger than expected.- Parameters:
socket – An initialised socket handle.
new_max – New hard ceiling in bytes.
- Returns:
LWIP_OKon success,LWIP_ERR_ARGfor invalid arguments, orLWIP_ERR_MEMif the resize couldn’t be performed (not enough room to shrink into, or heap exhaustion while growing).
Requesting Services
Socket clients request their required services automatically. External sockets without static address settings request DHCP. DNS readiness is required when connecting to a hostname; TLS and WSS also require SNTP synchronization before starting the handshake. Static and loopback sockets do not start DHCP.
lwip_socket_connect() parks the socket in LWIP_STATUS_WAITING_SERVICES
until its interface and required services are ready. Keep calling
lwip_service_events() to advance this wait. You can also request services
explicitly to prepare an interface before listening or doing PCB-level work.
-
lwip_error_t lwip_request_services(uint8_t flags, uint32_t timeout_ms)
Request one or more netif-level services on the default interface. Convenience wrapper around
lwip_netif_request_services()withnetif = NULL. Blocks until services are up or the timeout expires. Services are shared — this does not create a private service per socket.- Parameters:
flags –
Bitwise OR of one or more service flags:
LWIP_SOCKET_SVC_DHCP— start DHCP on the default interface.LWIP_SOCKET_SVC_DNS— make DNS resolution available forlwip_socket_connect().LWIP_SOCKET_SVC_SNTP— start SNTP for time synchronisation.
timeout_ms – How long to wait for the services to come up, in milliseconds.
0queues the request and returns immediately (fire-and-forget; uselwip_are_services_ready()to poll).
- Returns:
LWIP_OKonce all requested services are up,LWIP_ERR_ARGifflagsis empty,LWIP_ERR_STATEif the stack is not running, or a timeout error if services did not come up withintimeout_ms.
-
lwip_error_t lwip_netif_request_services(struct netif *netif, uint8_t flags, uint32_t timeout_ms, lwip_netif_service_cb cb, void *cb_data)
Request services on a specific interface, with an optional per-service callback. The callback fires once per service as it transitions to UP, FAILED, or TIMEOUT — never batched. If
cbisNULLthe call returns immediately after kicking the services (equivalent totimeout_ms = 0).- Parameters:
netif – Target interface, or
NULLfor the default interface.flags – Bitwise OR of
LWIP_SOCKET_SVC_*flags (same as above).timeout_ms – Deadline in milliseconds; 0 means fire-and-forget.
cb – Callback invoked per-service transition, or
NULL. Signature:void cb(struct netif *netif, const lwip_netif_service_event_t *ev, void *arg).ev->service_idis the single service that fired,ev->statusisUP/FAILED/TIMEOUT,ev->ready_bitmapis the bitmask of all services currently up.cb_data – Passed through as
argto the callback.
- Returns:
LWIP_OKon success,LWIP_ERR_ARGif flags is empty,LWIP_ERR_STATEif the stack is not running,LWIP_ERR_MEMif the service-request table is full.
-
bool lwip_are_services_ready(struct netif *netif, uint8_t flags)
Synchronous poll — returns
trueif every service bit inflagsis currently up onnetif(NULL= default interface). No side effects, no timer involvement. Use as the main-loop readiness gate after a fire-and-forget call tolwip_netif_request_services().- Parameters:
netif – Interface to query, or
NULLfor the default interface.flags – Bitwise OR of
LWIP_SOCKET_SVC_*flags to check.
- Returns:
trueiff all requested services are currently up.
/* Optional blocking form — prepare SNTP on the default interface.
* TLS/WSS socket clients request it automatically. */
lwip_request_services(LWIP_SOCKET_SVC_SNTP, 10000);
/* Per-service callback form — fires once per service as it comes up,
* fails, or times out. Useful when you want to react to each transition
* rather than block until all services are ready. */
static void on_service(struct netif *netif,
const lwip_netif_service_event_t *ev,
void *arg)
{
if (ev->status == LWIP_NETIF_SERVICE_UP) {
/* ev->service_id tells you which service just came up */
if (ev->service_id == LWIP_SOCKET_SVC_SNTP)
/* time is now synchronised */;
}
}
lwip_netif_request_services(NULL,
LWIP_SOCKET_SVC_DHCP | LWIP_SOCKET_SVC_SNTP,
10000, on_service, NULL);
If you need DNS but DHCP did not configure a resolver, set one manually with
dns_setserver() before
calling lwip_request_services() or lwip_socket_connect().
Socket as Client
As a client you are connecting a socket to another remote endpoint.
-
lwip_error_t lwip_socket_connect(struct lwip_socket *socket, const char *host, uint16_t port)
Initiate a connection to a remote host. Non-blocking — returns as soon as the attempt is queued. The socket transitions through
LWIP_STATUS_WAITING_SERVICES,LWIP_STATUS_RESOLVING, andLWIP_STATUS_CONNECTINGas needed before reachingLWIP_STATUS_CONNECTED(orLWIP_STATUS_ERRORon failure). Polllwip_socket_status(socket)from the main loop or subscribe toLWIP_SOCKET_EVENTF_STATE_CHANGEvialwip_socket_on_event()to know when the socket is ready.- Parameters:
socket – A socket handle previously initialised with
lwip_socket_create().host – Hostname or dotted-decimal IPv4 address. DNS resolution is performed automatically if required.
port – Remote port number (host byte order).
- Returns:
LWIP_OKif the attempt was queued,LWIP_ERR_STATEif the socket is not inLWIP_STATUS_INITstate,LWIP_ERR_ARGifsocketorhostisNULL.
Socket as Server
As a server, you listen on one socket and accept incoming connections as individual peer sockets, each of which you read from and write to independently.
-
lwip_error_t lwip_socket_listen(struct lwip_socket *socket, uint16_t port)
Bind a TCP socket to a local port and begin listening for connections. Only valid on a
LWIP_SOCKET_TCPsocket inLWIP_STATUS_INITstate. After this call the socket is a passive listener — do not calllwip_socket_connect()on it. Incoming peers are dequeued withlwip_socket_accept().- Parameters:
socket – A TCP socket handle previously initialised with
lwip_socket_create().port – Local port number to bind (host byte order).
- Returns:
LWIP_OKon success,LWIP_ERR_STATEif the socket is not inLWIP_STATUS_INITstate,LWIP_ERR_PROTOif the socket is not TCP,LWIP_ERR_MEMon allocation failure.
-
struct lwip_socket *lwip_socket_accept(struct lwip_socket *socket)
Dequeue one accepted peer from a listening socket. Non-blocking — returns
NULLif no peer is ready. On success, returns an owned, heap-allocated socket inLWIP_STATUS_CONNECTEDstate. Destroy each accepted peer withlwip_socket_destroy()when done; do not create another socket for it.- Parameters:
socket – A listening socket (
lwip_socket_listen()must have been called on it).
- Returns:
Owned peer pointer, or
NULLif the queue is empty or the listener argument is invalid.
See the full multi-connection server skeleton in TCP Server (multi-connection) below.
Sending/Receiving Data Over a Socket
Received bytes are copied into the socket’s RX ring and acknowledged to lwIP
immediately. There is no pbuf ownership or recved call in the socket API.
-
size_t lwip_socket_available(const struct lwip_socket *socket)
Return the number of bytes currently waiting in the socket’s RX ring. Use this to check before calling
lwip_socket_read()to avoid a zero-length read.- Parameters:
socket – A connected socket handle.
- Returns:
Number of bytes available to read;
0if the ring is empty.
-
size_t lwip_socket_read(struct lwip_socket *socket, uint8_t *buf, size_t len)
Read up to
lenbytes from the socket’s RX ring intobuf. Never blocks — returns immediately with however many bytes are available, clamped tolen. Returns0if the ring is empty.- Parameters:
socket – A connected socket handle.
buf – Caller-supplied buffer to receive the data.
len – Maximum number of bytes to read.
- Returns:
Number of bytes actually read (
0–len).
-
lwip_error_t lwip_socket_write(struct lwip_socket *socket, const uint8_t *buf, size_t len)
Send
lenbytes frombufover the socket. The socket must be inLWIP_STATUS_CONNECTEDstate.- Parameters:
socket – A connected socket handle.
buf – Data to send.
len – Number of bytes to send. Must be greater than
0.
- Returns:
LWIP_OKon success,LWIP_ERR_CLOSEDif the connection is closing or already closed,LWIP_ERR_STATEif the socket is not yet connected,LWIP_ERR_ARGif any argument isNULLorlenis0.
Closing/Destroying a Socket
There are three ways to close a connection, plus a separate step to free the handle. Choose based on whether you want a clean TCP handshake, a half-close, or an immediate abort.
-
lwip_error_t lwip_socket_close(struct lwip_socket *socket)
Initiate an orderly TCP close. Sends a FIN to the remote, then waits (up to an internal timeout) for the remote to ACK and send its own FIN. The socket transitions to
LWIP_STATUS_CLOSINGand then toLWIP_STATUS_CLOSEDonce the exchange completes. On timeout, the PCB is hard-aborted internally andLWIP_ERR_CLOSEDis returned, but the handle is still safe to destroy.For UDP sockets,
lwip_socket_close()simply removes the PCB and sets the status toLWIP_STATUS_CLOSEDimmediately.- Parameters:
socket – A connected (or connecting) socket handle.
- Returns:
LWIP_OKon clean close,LWIP_ERR_CLOSEDif the ACK wait timed out,LWIP_ERR_MEMif the FIN could not be enqueued (retry),LWIP_ERR_ARGifsocketisNULL.
-
lwip_error_t lwip_socket_shutdown(struct lwip_socket *socket)
Send a FIN without waiting for the remote to close its side — TCP half-close. The local side stops sending but can still receive until the remote also closes. The socket transitions to
LWIP_STATUS_CLOSING. Use this when you have finished sending but want to drain any remaining inbound data before destroying the socket.- Parameters:
socket – A connected TCP socket handle.
- Returns:
LWIP_OKif the FIN was queued,LWIP_ERR_STATEif the socket has no live PCB,LWIP_ERR_ARGifsocketisNULL. Not applicable to UDP sockets.
-
lwip_error_t lwip_socket_abort(struct lwip_socket *socket)
Immediately tear down the connection by sending a TCP RST. No FIN handshake — the PCB is removed and all callbacks are detached synchronously. Use this when the connection must be torn down without waiting (e.g. error recovery, application exit).
- Parameters:
socket – Any socket handle.
- Returns:
LWIP_OKon success,LWIP_ERR_ARGifsocketisNULL.
When the remote closes the connection you will see the status change without calling any close function yourself:
Status |
Meaning |
|---|---|
|
Remote sent FIN — clean close. Any unread bytes in the RX ring are still available before you destroy the socket. |
|
Remote sent RST — connection immediately gone. No graceful exchange. |
|
Stack error: timeout, out-of-memory, or a local |
In all three cases lwip_socket_is_active() returns false. Poll
lwip_socket_status(socket) or subscribe to LWIP_SOCKET_EVENTF_STATE_CHANGE to
detect these transitions.
-
lwip_error_t lwip_socket_destroy(struct lwip_socket *socket)
Free the socket handle itself and all resources it owns. The pointer becomes invalid after this call; clear stored pointers and do not reuse it. This is not a graceful close — call
lwip_socket_close()orlwip_socket_abort()first if the connection is still live, otherwise the PCB will be hard-aborted internally. For a listener socket, any connections waiting in the accept queue are also aborted and freed.Always call
lwip_socket_destroy()exactly once per handle, even afterlwip_socket_abort().- Parameters:
socket – Any socket handle (connected, closed, or aborted).
- Returns:
LWIP_OKon success,LWIP_ERR_ARGifsocketisNULL.
Client/Server Examples
These are production-quality skeletons. Copy them as a starting point and fill in your application logic where the comments indicate.
TCP Client
Connects to a remote host, exchanges data across multiple ticks, then shuts down cleanly. The protocol is left entirely to your application — substitute your own send/receive logic where the comments indicate.
#include <lwip.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#define RX_BUF_MAX 2048
typedef struct {
struct lwip_socket *socket;
uint8_t rx_buf[RX_BUF_MAX];
size_t rx_len;
bool want_close; /* set by app when it is done sending */
bool done; /* set when main loop should exit */
int exit_code;
} client_ctx_t;
static void client_on_event(struct lwip_socket *sock,
lwip_socket_event_type_t type,
const void *ev_data,
void *arg)
{
client_ctx_t *ctx = (client_ctx_t *)arg;
switch (type) {
case LWIP_SOCKET_EV_STATE_CHANGE: {
const lwip_socket_state_data_t *sd =
(const lwip_socket_state_data_t *)ev_data;
if (sd->current == LWIP_STATUS_CONNECTED) {
/* TODO: send your opening message here, e.g.:
* lwip_socket_write(sock, my_handshake, sizeof(my_handshake));
* or just set a flag so the app knows its good to send stuff
*/
} else if (sd->current == LWIP_STATUS_CLOSED ||
sd->current == LWIP_STATUS_RESET ||
sd->current == LWIP_STATUS_ERROR) {
ctx->exit_code = (sd->current == LWIP_STATUS_ERROR) ? 1 : 0;
ctx->done = true;
}
break;
}
case LWIP_SOCKET_EV_IO: {
const lwip_socket_io_data_t *io =
(const lwip_socket_io_data_t *)ev_data;
size_t space = sizeof(ctx->rx_buf) - ctx->rx_len;
if (io->readable && space) {
ctx->rx_len += lwip_socket_read(
sock,
ctx->rx_buf + ctx->rx_len,
space < io->readable ? space : io->readable);
}
/* TODO: parse ctx->rx_buf[0..ctx->rx_len] for complete messages.
* Consume processed bytes by memmove-ing the remainder to the front
* and adjusting ctx->rx_len.
* Set ctx->want_close = true when the session is complete. */
break;
}
case LWIP_SOCKET_EV_ERROR:
ctx->exit_code = 1;
ctx->done = true;
break;
}
}
int main(void)
{
if (!lwip_start()) return 1;
if (!lwip_network_up()) return 1;
client_ctx_t ctx = {0};
ctx.socket = lwip_socket_create(LWIP_SOCKET_TCP,
LWIP_NETIF_EXT, NULL, 30000);
if (!ctx.socket)
return 1;
lwip_socket_on_event(ctx.socket,
LWIP_SOCKET_EVENTF_STATE_CHANGE |
LWIP_SOCKET_EVENTF_IO,
client_on_event, &ctx);
if (lwip_socket_connect(ctx.socket, "your.server.com", YOUR_PORT) != LWIP_OK) {
lwip_socket_destroy(ctx.socket);
return 1;
}
while (!ctx.done) {
lwip_service_events();
/* Initiate half-close once the response is complete. */
if (ctx.want_close &&
lwip_socket_status(ctx.socket) == LWIP_STATUS_CONNECTED) {
lwip_socket_shutdown(ctx.socket);
ctx.want_close = false;
}
/* Your UI, key-scan, timer, and app work goes here. */
}
/* If the remote did not already close us, do so now. */
if (lwip_socket_status(ctx.socket) != LWIP_STATUS_CLOSED &&
lwip_socket_status(ctx.socket) != LWIP_STATUS_RESET) {
lwip_socket_close(ctx.socket);
}
lwip_socket_destroy(ctx.socket);
return ctx.exit_code;
}
TCP Server (multi-connection)
Listens on a port, accepts multiple concurrent connections from a fixed pool, dispatches each one per main-loop tick, and tears them down cleanly when the remote closes or an error occurs.
#include <lwip.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#define PORT 8080
#define MAX_CLIENTS 8
#define BUF_MAX 512
typedef enum {
PEER_IDLE = 0,
PEER_ACTIVE,
PEER_CLOSING, /* close issued, waiting for CLOSED */
} peer_state_t;
typedef struct {
struct lwip_socket *socket;
peer_state_t state;
char buf[BUF_MAX];
size_t buf_len;
} peer_slot_t;
static peer_slot_t peers[MAX_CLIENTS];
/* Find a free slot; returns NULL if the pool is full. */
static peer_slot_t *peer_alloc(void)
{
for (int i = 0; i < MAX_CLIENTS; i++)
if (peers[i].state == PEER_IDLE)
return &peers[i];
return NULL;
}
static void peer_release(peer_slot_t *p)
{
lwip_socket_destroy(p->socket);
memset(p, 0, sizeof(*p)); /* returns slot to pool */
}
/* Called once per tick for each active peer.
* Returns true when the slot should be released. */
static bool peer_service(peer_slot_t *p)
{
lwip_status_t st = lwip_socket_status(p->socket);
/* Remote closed cleanly, reset, or a stack error — tear down. */
if (st == LWIP_STATUS_CLOSED ||
st == LWIP_STATUS_RESET ||
st == LWIP_STATUS_ERROR)
return true;
/* Waiting for our own close to complete. */
if (p->state == PEER_CLOSING)
return !lwip_socket_is_active(p->socket);
/* ---- application logic ---- */
/* Accumulate incoming bytes. */
size_t avail = lwip_socket_available(p->socket);
if (avail) {
size_t space = sizeof(p->buf) - p->buf_len;
size_t n = avail < space ? avail : space;
p->buf_len += lwip_socket_read(
p->socket, (uint8_t *)p->buf + p->buf_len, n);
}
/* TODO: parse p->buf[0..p->buf_len] for a complete message.
* When ready to respond:
* lwip_socket_write(p->socket, my_response, my_response_len);
* When done with this peer:
* lwip_socket_close(p->socket);
* p->state = PEER_CLOSING;
* p->buf_len = 0;
*/
return false;
}
int main(void)
{
if (!lwip_start()) return 1;
if (!lwip_network_up()) return 1;
/* Wait for an address before listening; listen() does not await DHCP. */
if (lwip_request_services(LWIP_SOCKET_SVC_DHCP, 30000) != LWIP_OK)
return 1;
struct lwip_socket *server = lwip_socket_create(
LWIP_SOCKET_TCP, LWIP_NETIF_EXT, NULL, 0);
if (!server)
return 1;
if (lwip_socket_listen(server, PORT) != LWIP_OK) {
lwip_socket_destroy(server);
return 1;
}
bool running = true;
while (running) {
lwip_service_events();
/* Accept new connections while pool has space. */
peer_slot_t *slot = peer_alloc();
if (slot) {
slot->socket = lwip_socket_accept(server);
if (slot->socket) {
slot->state = PEER_ACTIVE;
}
/* else: queue empty this tick — slot stays IDLE */
}
/* Service every active peer. */
for (int i = 0; i < MAX_CLIENTS; i++) {
if (peers[i].state != PEER_IDLE) {
if (peer_service(&peers[i]))
peer_release(&peers[i]);
}
}
/* Your UI, key-scan, timer, and app work goes here.
* Set running = false to exit gracefully. */
}
/* Abort any still-open peers and destroy the listener. */
for (int i = 0; i < MAX_CLIENTS; i++) {
if (peers[i].state != PEER_IDLE) {
lwip_socket_abort(peers[i].socket);
peer_release(&peers[i]);
}
}
lwip_socket_destroy(server);
return 0;
}
Memory Safety & Usage
One of the biggest issues you will run into with lwIP is memory-related.
Hardware Stack Usage: lwIP consumes some space on the stack for scratch while copying resources, performing cryptography, and more. A quick review of the code suggests that average stack frame usage is ~200 bytes, with TLS Certificate Verify spiking to ~1 KiB and TLS Client Hello using ~800 bytes. Consider this when using stack-memory as the caller.
Heap Usage: The calculator’s heap is shared between your app, lwIP’s internal pools, and anything else running. lwIP-CE tracks memory usage for all of its internals, and exposes an API by which the caller can reserve memory.
uint8_t *http_buf = mem_request(4096);
if (!http_buf) {
return 1; /* not enough heap left to proceed */
}
/* ... use http_buf for the lifetime of the app ... */
/* ... possibly resize http_buf ... */
uint8_t *new_http_buf = mem_resize(http_buf, 8192);
if(new_http_buf)
http_buf = new_http_buf;
mem_release(http_buf);
While you are at perfect liberty to use malloc() or just statically-allocate memory, it is recommended to use this API for any heap allocations. Because lwIP-CE can wind up operating under severe memory constraints, having awareness of all buffers in use is important lest you wind up with out-of-memory errors while still believing you have memory available. Additionally, lwIP-CE can take certain actions to relieve memory pressure (ex: defer TCP window updates), but those will never happen if lwIP-CE is unaware that memory is actually low.
To illustrate this, take the following example. Say you allocate an 8 KiB buffer via something like uint8_t buf[8192]; as a static variable versus via mem_request(). The table below will illustrate how lwIP-CE manages memory based on 54 KiB (what the stack tends to default to) and 46 KiB (the result of using mem_request())
Pressure Level |
@ 54 KiB |
@ 46 KiB |
|---|---|---|
MILD (70%) |
37.8 KiB |
32.2 KiB |
HIGH (85%) |
45.9 KiB |
39.1 KiB |
SEVERE (90%) |
48.6 KiB |
41.4 KiB |
CRITICAL (95%+) |
51.3 KiB |
43.7 KiB |
The table lists the memory usage at which the stack would enter pressure state for each max heap cap. Notice that, in the case in which you reserved your 8 KiB buffer but lwIP-CE was unaware, the stack would OOM-fault before even hitting CRITICAL pressure state. This is just one example of the larger issue: the stack will make decisions based on the supposition it has more memory than it actually does if you do not tell it the memory is reserved. This is the main reason why I exposed this API surface and highly recommend people use it.
Users may return the current lwIP memory usage heuristics via the mem_get_stats() function.
Debugging
For real-time event logging and post-mortem traceback, see Debugging.