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: .. code-block:: c if (!lwip_start()) return 1; if (!lwip_network_up()) return 1; See :doc:`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: - `TCP connections and streams `_ - `UDP datagrams `_ - `ALTCP abstraction layer `_ — wraps TCP, TLS, WebSocket - `Raw IP protocol PCBs `_ - `Packet buffer (pbuf) management `_ - `Network interface (netif) control `_ 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. .. c:function:: 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 calls ``lwip_service_events()``. :param type: Transport selector (``lwip_socket_type_t``). See table below. :param bind: Netif preference (``lwip_socket_bind_descriptor_t``). ``LWIP_NETIF_EXT`` selects USB Ethernet, ``LWIP_NETIF_ANY`` accepts loopback or external, and ``LWIP_NETIF_LOOP`` restricts to loopback. Selecting an interface does not wait for it to become ready. :param addrinfo: ``NULL`` uses DHCP on external interfaces. A non-``NULL`` ``lwip_socket_addrinfo_t`` supplies static ``ip``/``netmask``/``gateway`` instead. The address settings are copied into the socket. :param timeout_ms: Inactivity watchdog window in milliseconds, covering service wait, connection establishment, TLS handshake, and data transfer. Network activity extends the deadline. ``0`` disables the inactivity timeout; the application manages the socket's lifetime. :returns: Owned socket pointer, or ``NULL`` on 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. .. c:function:: 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 in ``LWIP_SOCKET_RX_RING_STEP_SIZE`` (512 B) increments. ``rx_ring_max = 0`` selects the default ``LWIP_SOCKET_RX_RING_MAX_SIZE`` (4096 B). :param type: Transport selector (``lwip_socket_type_t``). :param bind: Netif preference (``lwip_socket_bind_descriptor_t``). :param addrinfo: ``NULL`` for DHCP on external interfaces; non-``NULL`` for static IPv4. :param timeout_ms: Inactivity watchdog in milliseconds. ``0`` disables it. :param rx_ring_max: Hard ceiling for the RX ring in bytes. ``0`` uses ``LWIP_SOCKET_RX_RING_MAX_SIZE`` (4096 B). :returns: Owned socket pointer, or ``NULL`` on failure. Transport selectors: .. list-table:: :header-rows: 1 :widths: 32 68 * - Protocol - Meaning * - ``LWIP_SOCKET_TCP`` - Raw TCP via lwIP ``tcp_*``. * - ``LWIP_SOCKET_UDP`` - UDP via lwIP ``udp_*``. * - ``LWIP_SOCKET_ALTCP`` - ALTCP using the default TCP allocator. * - ``LWIP_SOCKET_ALTCP_TLS`` - ALTCP wrapped in the CE TLS client path. Requires TLS enabled in the app configuration wizard. * - ``LWIP_SOCKET_ALTCP_WS`` - WebSocket over plain TCP (RFC 6455). * - ``LWIP_SOCKET_ALTCP_WSS`` - WebSocket over TLS. Also requires TLS enabled in wizard. An example use case is given below: .. code-block:: c 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. .. c:function:: 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_WS`` or ``LWIP_SOCKET_ALTCP_WSS`` socket. Must be called after ``lwip_socket_create()`` and before ``lwip_socket_connect()``. Has no effect on non-WebSocket socket types. :param socket: A WS or WSS socket handle. :param path: WebSocket resource path, e.g. ``"/"``. Borrowed — must remain valid until ``lwip_socket_connect()`` returns. :param subprotocol: Optional ``Sec-WebSocket-Protocol`` value, or ``NULL``. Borrowed under the same lifetime constraint as ``path``. :returns: ``LWIP_OK`` on success, ``LWIP_ERR_ARG`` if ``socket`` or ``path`` is ``NULL``. Socket Reconfiguration ----------------------- Some socket attributes are modifiable in flight should the situation call for it. .. c:function:: 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. :param socket: An initialised socket handle. :param timeout_ms: New watchdog window in milliseconds. ``0`` disarms the watchdog entirely (use with care — a stalled connect will never time out). :returns: ``LWIP_OK`` on success, ``LWIP_ERR_ARG`` if ``socket`` is ``NULL``. .. c:function:: 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_max`` could 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 a ``Content-Length`` header that tells you the response will be larger than expected. :param socket: An initialised socket handle. :param new_max: New hard ceiling in bytes. :returns: ``LWIP_OK`` on success, ``LWIP_ERR_ARG`` for invalid arguments, or ``LWIP_ERR_MEM`` if 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. .. c:function:: 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()`` with ``netif = NULL``. Blocks until services are up or the timeout expires. Services are shared — this does not create a private service per socket. :param 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 for ``lwip_socket_connect()``. - ``LWIP_SOCKET_SVC_SNTP`` — start SNTP for time synchronisation. :param timeout_ms: How long to wait for the services to come up, in milliseconds. ``0`` queues the request and returns immediately (fire-and-forget; use ``lwip_are_services_ready()`` to poll). :returns: ``LWIP_OK`` once all requested services are up, ``LWIP_ERR_ARG`` if ``flags`` is empty, ``LWIP_ERR_STATE`` if the stack is not running, or a timeout error if services did not come up within ``timeout_ms``. .. c:function:: 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 ``cb`` is ``NULL`` the call returns immediately after kicking the services (equivalent to ``timeout_ms = 0``). :param netif: Target interface, or ``NULL`` for the default interface. :param flags: Bitwise OR of ``LWIP_SOCKET_SVC_*`` flags (same as above). :param timeout_ms: Deadline in milliseconds; 0 means fire-and-forget. :param 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_id`` is the single service that fired, ``ev->status`` is ``UP`` / ``FAILED`` / ``TIMEOUT``, ``ev->ready_bitmap`` is the bitmask of all services currently up. :param cb_data: Passed through as ``arg`` to the callback. :returns: ``LWIP_OK`` on success, ``LWIP_ERR_ARG`` if flags is empty, ``LWIP_ERR_STATE`` if the stack is not running, ``LWIP_ERR_MEM`` if the service-request table is full. .. c:function:: bool lwip_are_services_ready(struct netif *netif, uint8_t flags) Synchronous poll — returns ``true`` if every service bit in ``flags`` is currently up on ``netif`` (``NULL`` = default interface). No side effects, no timer involvement. Use as the main-loop readiness gate after a fire-and-forget call to ``lwip_netif_request_services()``. :param netif: Interface to query, or ``NULL`` for the default interface. :param flags: Bitwise OR of ``LWIP_SOCKET_SVC_*`` flags to check. :returns: ``true`` iff all requested services are currently up. .. code-block:: c /* 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. .. c:function:: 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``, and ``LWIP_STATUS_CONNECTING`` as needed before reaching ``LWIP_STATUS_CONNECTED`` (or ``LWIP_STATUS_ERROR`` on failure). Poll ``lwip_socket_status(socket)`` from the main loop or subscribe to ``LWIP_SOCKET_EVENTF_STATE_CHANGE`` via ``lwip_socket_on_event()`` to know when the socket is ready. :param socket: A socket handle previously initialised with ``lwip_socket_create()``. :param host: Hostname or dotted-decimal IPv4 address. DNS resolution is performed automatically if required. :param port: Remote port number (host byte order). :returns: ``LWIP_OK`` if the attempt was queued, ``LWIP_ERR_STATE`` if the socket is not in ``LWIP_STATUS_INIT`` state, ``LWIP_ERR_ARG`` if ``socket`` or ``host`` is ``NULL``. 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. .. c:function:: 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_TCP`` socket in ``LWIP_STATUS_INIT`` state. After this call the socket is a passive listener — do not call ``lwip_socket_connect()`` on it. Incoming peers are dequeued with ``lwip_socket_accept()``. :param socket: A TCP socket handle previously initialised with ``lwip_socket_create()``. :param port: Local port number to bind (host byte order). :returns: ``LWIP_OK`` on success, ``LWIP_ERR_STATE`` if the socket is not in ``LWIP_STATUS_INIT`` state, ``LWIP_ERR_PROTO`` if the socket is not TCP, ``LWIP_ERR_MEM`` on allocation failure. .. c:function:: struct lwip_socket *lwip_socket_accept(struct lwip_socket *socket) Dequeue one accepted peer from a listening socket. Non-blocking — returns ``NULL`` if no peer is ready. On success, returns an owned, heap-allocated socket in ``LWIP_STATUS_CONNECTED`` state. Destroy each accepted peer with ``lwip_socket_destroy()`` when done; do not create another socket for it. :param socket: A listening socket (``lwip_socket_listen()`` must have been called on it). :returns: Owned peer pointer, or ``NULL`` if 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. .. c:function:: 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. :param socket: A connected socket handle. :returns: Number of bytes available to read; ``0`` if the ring is empty. .. c:function:: size_t lwip_socket_read(struct lwip_socket *socket, uint8_t *buf, size_t len) Read up to ``len`` bytes from the socket's RX ring into ``buf``. Never blocks — returns immediately with however many bytes are available, clamped to ``len``. Returns ``0`` if the ring is empty. :param socket: A connected socket handle. :param buf: Caller-supplied buffer to receive the data. :param len: Maximum number of bytes to read. :returns: Number of bytes actually read (``0``–``len``). .. c:function:: lwip_error_t lwip_socket_write(struct lwip_socket *socket, const uint8_t *buf, size_t len) Send ``len`` bytes from ``buf`` over the socket. The socket must be in ``LWIP_STATUS_CONNECTED`` state. :param socket: A connected socket handle. :param buf: Data to send. :param len: Number of bytes to send. Must be greater than ``0``. :returns: ``LWIP_OK`` on success, ``LWIP_ERR_CLOSED`` if the connection is closing or already closed, ``LWIP_ERR_STATE`` if the socket is not yet connected, ``LWIP_ERR_ARG`` if any argument is ``NULL`` or ``len`` is ``0``. 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. .. c:function:: 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_CLOSING`` and then to ``LWIP_STATUS_CLOSED`` once the exchange completes. On timeout, the PCB is hard-aborted internally and ``LWIP_ERR_CLOSED`` is returned, but the handle is still safe to destroy. For UDP sockets, ``lwip_socket_close()`` simply removes the PCB and sets the status to ``LWIP_STATUS_CLOSED`` immediately. :param socket: A connected (or connecting) socket handle. :returns: ``LWIP_OK`` on clean close, ``LWIP_ERR_CLOSED`` if the ACK wait timed out, ``LWIP_ERR_MEM`` if the FIN could not be enqueued (retry), ``LWIP_ERR_ARG`` if ``socket`` is ``NULL``. .. c:function:: 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. :param socket: A connected TCP socket handle. :returns: ``LWIP_OK`` if the FIN was queued, ``LWIP_ERR_STATE`` if the socket has no live PCB, ``LWIP_ERR_ARG`` if ``socket`` is ``NULL``. Not applicable to UDP sockets. .. c:function:: 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). :param socket: Any socket handle. :returns: ``LWIP_OK`` on success, ``LWIP_ERR_ARG`` if ``socket`` is ``NULL``. When the **remote** closes the connection you will see the status change without calling any close function yourself: .. list-table:: :header-rows: 1 :widths: 30 70 * - Status - Meaning * - ``LWIP_STATUS_CLOSED`` - Remote sent FIN — clean close. Any unread bytes in the RX ring are still available before you destroy the socket. * - ``LWIP_STATUS_RESET`` - Remote sent RST — connection immediately gone. No graceful exchange. * - ``LWIP_STATUS_ERROR`` - Stack error: timeout, out-of-memory, or a local ``lwip_socket_abort()``. 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. .. c:function:: 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()`` or ``lwip_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 after ``lwip_socket_abort()``. :param socket: Any socket handle (connected, closed, or aborted). :returns: ``LWIP_OK`` on success, ``LWIP_ERR_ARG`` if ``socket`` is ``NULL``. 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. .. code-block:: c #include #include #include #include #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. .. code-block:: c #include #include #include #include #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. .. code-block:: c 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()``) .. list-table:: :header-rows: 1 :widths: 33 33 34 * - 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 :doc:`debugging`.