Since I began programming the ESP8266, I have been struggling to get the TCP network API of the SDK working consistently without crashes. I think that I have cracked it at last. Here are some notes, so that I may never forget, and maybe help someone else.

ESP8266 TCP server.

This post is about using the ESP8266 as TCP server, but the information is useful if you want to know how to tackle ESP8266 networking in general. First lets set up a server like the SDK tells us to do it:

  • Initialise struct espconn parameters according the protocol type you want (ESPCONN_TCP).
  • Register connect callback and reconnect callback functions.
  • Call espconn_accept to start listening for packets.
  • When a connection has been made on the listening connection, the registered connect callback is called.

Some code to do this:

bool ICACHE_FLASH_ATTR tcp_listen(unsigned int port)
{
    int ret;
    struct espconn *conn;

    //Get some memory for the connection information.
    conn = os_zalloc(sizeof(struct espconn));
    if (conn)
    {
        //Set up TCP connection configuration.
        //TCP connection.
        conn->type = ESPCONN_TCP;
        //No state.
        conn->state = ESPCONN_NONE;
        //Set port.
        conn->proto.tcp->local_port = port;

        //Register SDK callbacks.
        espconn_regist_connectcb(conn, tcp_connect_cb);
        espconn_regist_reconcb(conn, tcp_reconnect_cb);
        espconn_regist_disconcb(conn, tcp_disconnect_cb);

        ret = espconn_accept(conn);
        if (ret != ESPCONN_OK)
        {
            os_printf("Error when starting the listener: %d.\n", ret);
            return(false);
        }
        return(true);
    }
    os_printf("Could not allocate memory for the listening connection.\n");
    return(false);
}

Callbacks.

The SDK network API is event based. The way this works is that you register some functions for the API to call when certain events happen (connected, disconnected, data received, and so on). Your callback function receives a parameter called rag, from the SDK, which is a pointer to an espconn struct (defined in espconn.h in the SDK). This struct contains the connection information. You will probably need a way to identify a connection as it triggers the callbacks. I have additional data from my own application coupled to the connection, that I need to handle when an event happens to the connection. In fact I have a list of all open connections using my own data structures. When I first started using the ESP8266 I ignored the part of the Programming Guide, that told me to not use the arg pointer from the callbacks, to distinguish one connection from another. The following is an example of a struct I was using at the time to keep track of connections.

struct tcp_connection
{
    //Pointer to the struct espconn structure associated with the
    //connection.
    struct espconn *conn;
    //Pointer to the data meant for the current callback.
    struct tcp_callback_data callback_data;
    //Is the connection sending data.
    bool sending;
    //Are the connection closing.
    bool closing;
    //A pointer for the user, never touched.
    void *user;
    //Pointers for the prev and next entry in the list.
    struct tcp_connection *prev;
    struct tcp_connection *next;
};

When ever the program entered a callback, the code would compare the arg pointer to the one stored in the conn member of tcp_connection, and couple my private data to the connection that way. This works most of the time, but fails in couple if cases.

  1. The SDK tells us not to do this, which in my book tells me, that sometime later the SDK may change internally enough, that this could stop working.
  2. The reconnect and disconnect callbacks gets an arg parameter passed, that point to the listening connection, not the one that the event happened too.

There are ways around the second problem that works quite well, but why bother when the whole thing may break with some later version of the SDK, and there is a nicer option outlined in the Programming Guide?

The solution from the Programming Guide is to use the remote address and port of the connection to identify each connection. This took a little more code, but when done, revealed a few things about the flow of the connections in the API.

Connection flow in the ESP8266 API.

Let us look at some excerpts of debug log files from my HTTP server:

Listening connection.

Just after the espconn_accept call the situation looks like this:

Connection 0x3fff3158 (0x3fff3480) state "ESPCONN_LISTEN".
 Remote address 0.0.0.0:0.
 SDK remote address 0.0.0.0:0.
1 connection.

This is the listening connection. This connection is the one that the remote computer connects to, and has to remain open as long as the server is expected to serve connections. 0x3fff3480 is a pointer to the struct espconn data for the connection. 0x3fff3158 is the connection data, that I am keeping. The struct looks somewhat like this:

struct tcp_connection
{
    //SDK connection data.
    struct espconn *conn;
    //Remote IP.
    uint8 remote_ip[4];
    //Remote port.
    unsigned int remote_port;
    //Local IP.
    uint8 local_ip[4];
    //Remote port.
    unsigned int local_port;
    //Pointer to the data meant for the current callback.
    struct tcp_callback_data callback_data;
    //Pointers to callback functions for handling connection states.
    struct tcp_callback_funcs *callbacks;
    //Is the connection sending data.
    bool sending;
    //Is the connection closing.
    bool closing;
    //Pointers for the prev and next entry in the list.
    struct tcp_connection *prev;
    struct tcp_connection *next;
};

As will later become clear, espconn may be deallocated by the SDK. Any data that you may need to preserve to keep track of the connection should be copied somewhere else, hence the IP addresses and ports in the above struct.

New connection.

Next up someone is connecting to the server:

TCP connected (0x3fff3cb8).
Connection 0x3fff3158 (0x3fff3480) state "ESPCONN_CONNECT".
 Remote address 0.0.0.0:0.
 SDK remote address 192.168.4.2:34480.
1 connection.

0x3fff3cb8 is the address of a new espconn struct, that the SDK has created for the incoming connection. This is where I create a new tcp_connection struct, and add it to the list of connections. The Remote address is the one saved when the connection was created, and the SDK remote address is the one currently in the espconn struct.

Data received.

A request comes in:

TCP received (0x3fff3cb8).
Connection 0x3fff3158 (0x3fff3480) state "ESPCONN_CONNECT".
 Remote address 0.0.0.0:0.
 SDK remote address 192.168.4.2:34480.
Connection 0x3fff3d18 (0x3fff3cb8) state "ESPCONN_READ".
 Remote address 192.168.4.2:34480.
 SDK remote address 192.168.4.2:34480.
2 connections.

The callback gets a pointer to the newly created espconn struct, but still according to the docs we can not expect this behaviour.

Sending the answer.

To answer the request, find the connection that has a remote address of 192.168.4.2:34480 and use the espconn pointer (0x3fff3cb8) when calling espconn_send. After calling espconn_send you have to wait for the sent callback before sending any more data. I have not seen any mention of the data size limit of espconn_send in the official docs, but I have seen 1440 bytes mentioned elsewhere. You may have to split your data and do more than one espconn_send, wait, sent callback cycle.

This is what the situation looks like when entering the sent callback:

TCP sent (0x3fff3cb8).
Connection 0x3fff3158 (0x3fff3480) state "ESPCONN_CONNECT".
 Remote address 0.0.0.0:0.
 SDK remote address 192.168.4.2:34480.
Connection 0x3fff3d18 (0x3fff3cb8) state "ESPCONN_CONNECT".
 Remote address 192.168.4.2:34480.
 SDK remote address 192.168.4.2:34480.
2 connections.

This is where the penny finally dropped for me. Notice the SDK remote addresses? It may be obvious, but the listening connection tracks the state of the currently sending connection. It looks like listening connection does the actual sending.

Closing the connection.

What happens when the connection is closing, or an error (reconnect) occurs, makes sense after seeing the above, but eluded me for a long time.

TCP disconnected (0x3fff3480).
Connection 0x3fff3158 (0x3fff3480) state "ESPCONN_CLOSE".
 Remote address 0.0.0.0:0.
Connection 0x3fff3d18 (0x3fff3cb8) state "ESPCONN_CLOSE".
 Remote address 192.168.4.2:34480.
2 connections.

The SDK is a harsh mistress, and deallocates the espconn connection data before calling the disconnect/reconnect callback. That is why there is no SDK remote addresses and also why I saved the remote address in my own connection data earlier. The espconn pointer returned in the arg parameter of the callback still contain the remote address of the closing connection (and points to the listening connection). The code uses this to look up the connection and deallocate my stuff.

After several syncs emerge kept complaining about missing digests for a huge amount of ebuilds on the system. I guess this shuold solve itself at some future sync, but in the meantime I couldn't merge anything because of the mising digest, so I digged around and crafted this command to rebuild all digests/manifests in the portage tree.

find /usr/portage -type d -exec sh -c 'find "{}" -maxdepth 1 -type f -name '*.ebuild'| sort | head -n 1| xargs -r -I --  sudo ebuild -- manifest' ";"

The command finds the first .ebuild file in every subdirectory of /usr/portage and runs sudo ebuld minifest on it.

Following up on the general notes, here I some notes on programming for the ESP8266 in C. I use Linux for development, but most of this is OS independent.

General.

  • Prefixing a function with "ICACHE_FLASH_ATTR" places it in flash, not doing so places the function in RAM.
  • The IoT SDK is single tasked and event driven. I've always found event driven programming a pain, but this implementation stinks. There is no way to return to the system, in the middle of processing, to not stall the system tasks. The docs, says that you not may stay in a function for more than 10 ms, without the system code getting behind. If I were to start over I would try the FreeRTOS SDK, unfortunately it seems that Espressif is giving it less love.

Network.

  • Some basic code for connecting to a WIFI. GitHub
  • The connect callback returns a pointer to the struct espconn in the arg parameter. It is not the same as the one used by espconn_accept.
  • The disconnect_callbackreceives a pointer to the listening connection, not the active connected one, at least when in TCP server mode.
  • In struct espconn, member reverse, seems to be free to use., except in the disconnect callback, where it suddenly has a new value.

Flash.

  • It seems the file system of choice is best placed after the code in flash.
  • On suggestion from Dave Hylands, I use a ZIP file as file system image. The file is uncompressed and used as a simple container.

The buggers have been lying around while life happened, and I was waiting for the last one I ordered, which was on a neat breakout board. Well I got tired of waiting, and started playing with the ESP12 version. These are some random notes, of things I have discovered during my experiments.

  • My ESP12's have their GPIO4 & GPIO5 pins swapped on the silkscreen.
  • The toolchain for the Arduino ESP8266 IDE seems to be 64-bit.
  • I cannot reliably program using a PL2303 adaptor, but my Raspberry Pi Model B's serial port works fine.
  • In SoftAP mode, the SDK seems to set up a DHCP server on 192.168.4.1, all without me doing anything but setting the mode, and the AP config.
  • Baud rate at boot is 74880.
  • Untested Zero-wire auto-reset. (esptool.py needs a modification to send the break signal).

I wanted to start working with my ESP8266 and needed a 3.3V regulator to get enough current. I had ordered some 1117 regulators, but went looking through what I had laying about, and "created" this simple circuit.

It is not very efficient, and would benefit from a Darlington in place of Q1, but it gets the job done with enough current for the ESP8266. If the output voltage is just a tad to large, try replacing D1 with a Schottky type.

Adding a 10uF capacitor from 5V to ground is a good idea.


My Acer Aspire One D250, kept going into suspend every minute or so, whenever I was not logged in to a window manager. I run Debian testing and had my first real interaction with systemd (except for systemctl).

Turns out there are some settings in /etc/systemd/logind.conf that configures some of the power management features.

#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
#
# See logind.conf(5) for details

[Login]
#NAutoVTs=6
#ReserveVT=6
#KillUserProcesses=no
#KillOnlyUsers=
#KillExcludeUsers=root
#InhibitDelayMaxSec=5
HandlePowerKey=poweroff
HandleSuspendKey=ignore
HandleHibernateKey=ignore
HandleLidSwitch=ignore
#PowerKeyIgnoreInhibited=no
#SuspendKeyIgnoreInhibited=no
#HibernateKeyIgnoreInhibited=no
#LidSwitchIgnoreInhibited=yes
IdleAction=ignore
#IdleActionSec=30min
#RuntimeDirectorySize=10%
#RemoveIPC=yes

First I asked systemd to ignore everything but the power button, and as I somewhat expected this didn't do the trick. Then I set the IdleActionSec to the actual amount of second (1800), that didn't work either. In the end completely disabling idling, stopped the suspend.

IdleAction=ignore

I needed a second screen for my netbook, as the screen real estate was not large enough for working with QT creator comfortably. I did not want (and did not have) a 5 meter VGA cable crossing the living room, but I wanted to use my TV as the second monitor.

I grabbed my Raspberry Pi B, a 2GB SD-card i had in the spares, and set out to configure the Raspberry Pi to mirror the screen on the TV from the network.

I tried using a protocol of X, called XDMX, that is designed to do what I wanted. I used much time, and had inconsistent results. I am sure that the problems where all of the wetware kind, but the documentation seems really sparse, and that does not help.

The solution for now, is less elegant. I installed a basic LXDE environment on the Raspberry Pi. Shared the keyboard and mouse using Synergy, and run the X applications on the netbook, through a ssh tunnel from the Raspberry Pi.

First things first, an OS (Raspberry Pi).

The standard Raspbian image will not fit on the 2GB card I had, but The minimal Raspbian unattended netinstaller comes to the rescue.

Simply download the installer image, write it to the SD-card, and boot the Pi, with it. Instructions for doing this can be found in the README. ua-netinst will start installing a Raspbian system, from the Internet, this takes a while.

I then lost my whits and asked the Pi to install a full lxde environment, it took ages! After starting from scratch, I added myself as a user, and installed X and a minimal LXDE environment.

adduser username

apt-get install keyboard-configuration  
apt-get install xserver-xorg
apt-get install lxde-core
apt-get install xinit

and Synergy

apt-get install synergy

Synergy (control node).

Synergy acting as a server, needs a configuration file in /etc/synergy.conf to setup the layout.

I have the Raspberry Pi (connected to the TV) with the host name pi, and the netbook called ace2 (the alias section). The TV is left of the netbook.

section: screens
        pi:
        netbook:
end

section: links
        pi:
                left = netbook
        netbook:
                right = pi
end

section: aliases
    netbook:
        ace2
end

Start the Synergy server.

synergys

Final step (Raspberry Pi).

I needed to run QT creator on the TV, but you can run any X application. Complex things like video and fancy GUIS, are slow. Probably due to missing hardware acceleration and network speed.

I start the Synergy client, after which the netbook mouse and keyboard works on the Pi as well.

synergy (IP address of the controlling computer)

Then I ssh into the the netbook with X tunneling and compression, and start the program that I want to use. I now have the program (qtcreator) running on the TV, but can use the netbook keyboard and mouse to control it.

ssh -CX (IP address of the controlling computer)
qtcreator

Generated on 2015-08-24 22:05:45.296844