2020-10-25

The Process that Makes Product Development Possible

What is going on in our mind when we design, review, test? Could it be like evolution - the process that has designed all living things? A property of evolution that is hard to grasp is that it has no goal, not even survival. And still, knowledge is accumulated over time in the species that do survive. New ideas on how to be and act is created by random mutations, which are criticised by reality - including other species. But this doesn't seem to be the way we design things. We don't build random things and keep the ones that are useful, do we? That would take a lot of time. We design and build products much faster than that. And we do it on purpose to reach a goal, to satisfy a need. Or could it be that we are mistaken about what is really going on and that design is exactly like evolution? That the things we have are just random creations that could have been completely different. That the things that are useful to us are preserved until they no longer are and then forgotten. That designs mutate and are then outcompeted by improved versions of themselves. Take a look at testing for example. There is something about our need to test our creations that resembles evolution. We are never 100% sure that the things we design will work in reality. It is true that we don't go ahead and build any random thing, but still.

I have noticed that to be able to program a computer at all, you need to have a special ability. You need to be able to simulate in your head the effect of a code statement running on the computer. To do this you first need to be able to load the rules of interpretation the computer follows into your head. That we have this ability implies that we are universal simulators. And this, I think, is also the key to understand how we design things in general. It means that we don't have to build everything that we come to think of to see if they will work. We can simulate them in our head instead and quickly reject things that doesn't work. The same principle applies to any action we could choose to take. The effects of the action can be simulated ahead of time. This means creativity can be purely random at its core just like mutations are. We quickly simulate the ideas that flow from this process and reject the ones that fails. Some ideas we act on and a few end up as products.

But still, we are pursuing goals that can be quite far away from where we start. We know ahead of time where we want to end up. This doesn't seem very random, and still it probably is. Where did the goal come from in the first place? Random creativity. And we did simulate it and we have seen that the goal is worth pursuing. The gap from here to a goal far away has to be filled with intermediate goals, random as well, and the simulation tells us which are steps in the right direction. This process of breakdown continues until the goals are trivial for us to achieve.

The simulation is not perfect and we always risk going down paths that doesn't lead where we want when we try our ideas in reality, or when later stages of the simulation fails to connect an intermediate goal to either the beginning or the end. At that moment we backtrack and wait for new random ideas to try out. It is good to not put unnecessary constraints on the end goal because you don't know which turns the random road to the goal will take. Be open minded and try to use whatever your creativity comes up with, maybe even adjust the goal itself to better fit the ideas that become available. If all you have is a hammer...

If we take the simulation idea further it means our minds are collectively running a set of virtual realities - simulations of physical reality. An environment where memes (in the original, broader sense) can replicate, evolve and influence our thinking. Despite being abstract, memes can affect the real world through us humans because how we act in the real world are influenced by abstractions in the virtual world. Memes are ideas that spread through various forms of communication between people. Each individual uses creativity and simulation to turn the incoming message into a personal idea that in turn has the power to change the simulation. All ideas are not memes, only the ones that can spread between minds are.

If creativity is an unconscious process that we are born with, i.e. genetic knowledge, is it possible for us to improve it or are we stuck with what we got? If we can figure out which methods are used by creativity, e.g. recombination of already existing ideas, we could simulate these methods and thereby control our own creative process, even when the defective genes of an individual lack knowledge of that particular method. We are, after all, universal simulators.

To wrap things up. We have found design to be simulated tests of random ideas produced by our creativity. Review is just another person using the same mechanisms, and the transfer of ideas between humans also requires creativity and simulation at the receiving end. The purpose of real tests are to verify that we didn't make mistakes in the simulation and make sure the thing we have built is fit for its purpose. We will never be completely sure though, because the tests are also designed with the same process and can themselves have errors. Still, the rational thing to do is to use a product that has passed all tests, simulated or real.

If you have gotten this far I should tell you that these are not my own ideas. If you want to learn more make sure to check out David Deutsch, Donald Campbell, Karl Popper and the blogs and podcasts from Bruce Nielson (The Four Strands, The Theory of Anything), Brett Hall (ToKCast), Christofer Lövgren (Do Explain).

The Temporal and Spatial Structures of Product Development

We use time to construct products. Over time we learn things, invent solutions, encounter problems, make decisions. The information we collect is accumulated in the spatial structure of the product and the artifacts that surrounds it. New information changes the shape of the product and lifts it to a new configuration, slightly more refined and fit for its purpose. The project itself is a temporal structure that organizes the flow of information into and out of the development process.

Should we do the specifications first, the code first, the tests first? We should do a little of everything in parallel using the information we have acquired so far. If we don't know from the start what the product should do exactly, then we should focus on code and assembly before specification. The things we learn along the way will go into the specification. If the goal is clear from the start then the specifications should be filled with more content before time is spent on constructing the product. When we are done the final configuration should look approximately the same regardless of how we got there. We should have specifications, code, drawings, assemblies, tests and guides.

It is not only the functional aspects of the product that shapes it. Of equal importance are constraints imposed by efficient industrial production and how to perform compliance testing. From the start of the development project, strive to maintain an integrated product from end to end, extending into production and verification. It is not only the product itself that needs to be constructed, a variety of specialized tools for production and test also needs to be developed.

2020-09-09

Extensible C APIs

The problem we want to solve is how to add functionality to an existing C API in an elegant way. You may have seen the practice to add one extra parameter at the end of a function call, reserved for future use. It must always be 0 in the first version of the API. The implementation should enforce it to be 0 to avoid unpleasant surprises in the future.

uint32_t do_work(int32_t a, uint8_t b, void* rfu);

Later, when additional requirements make it necessary to vary how "work is done" and the variation needs one additional parameter, uint16_t c. It is added in the second version of the API.

struct do_work_extensionA {
    uint16_t c;
    void* rfu;
};
uint32_t do_work(int32_t a, uint8_t b, do_work_extensionA* extA);

When extA is 0 do_work() should behave exactly like the first version to be backwards compatible. Existing users can keep their code as it is until they need the new functionality.

You may have spotted the pattern, and to be sure we will add a third version of the API with yet more parameters.

struct do_work_extensionB {
    uint8_t* d;
    uint32_t e;
    void* rfu;
};
struct do_work_extensionA {
    uint16_t c;
    do_work_extensionB* extB;
};
uint32_t do_work(int32_t a, uint8_t b, do_work_extensionA* extA);

An alternative to the extension scheme described above is to use multiple entry points.

uint32_t do_work(int32_t a, uint8_t b);
uint32_t do_work_extA(int32_t a, uint8_t b, uint16_t c);
uint32_t do_work_extB(int32_t a, uint8_t b, uint16_t c, uint8_t* d, uint32_t e);

You decide which method suits you best.

2020-09-05

PCB Version and Assembly Discrimination

You should give embedded software a way to figure out its place in the world. "I just woke up. Where am I?" This can be done by giving the PCB a version number. Allocate a few GPIOs, say three, and you can have the same binary run on 8 different PCBs. The software has the corresponding 8-bit vector which tells itself or a bootloader if it is compatible with a certain PCB. 0b00000101 means the software is compatible with PCB version 0 and 2. This prevents old, incompatible software to run afoul on new PCBs which it hasn't got routines to handle. It is common to forget to check this in software before there are more than one PCB version, but this check is needed from the start to prevent that old versions of the software is installed and executed on new versions of PCBs they don't support.

The next step is to allocate a few GPIOs for variations in the assembly. These are by default high by internal pullups. Zero Ohm resistors can be mounted to sink them to ground. You now have the possibility to vary the assembled components on the board and all variants can be handled by the same binary. Exactly what it means that any one GPIO is low can be decided at the moment when the need for an alternate assembly arises.

 If you have an analog pin to spare you can encode the assembly version with a voltage divider instead. The analog range is divided up in discrete intervals, each representing an assembly version. With 1% resistors it is reasonable to divide the full range (5V) into 25 separate ranges, i.e. 0.2V per range. One analog input can decode 25 different assembly versions. If you don't want the divider to always consume current you can connect the low side to a GPIO and turn off the divider by setting it high.

What if you only have one GPIO pin and want to encode more than two versions? Set the GPIO as output and charge a capacitor. Switch over to input and measure the discharge time. The more versions you need, the longer it will take to find out which is present.

It is possible to solve the problem with evolving hardware by building separate binaries for each configuration. The drawback is that you need to be careful with which binary you install on which hardware, because mistakes are not automatically detected. This is a complication I prefer to avoid.

2019-05-18

STM32F407G-DISC1 board and MEMS microphone MP45DT02

This is a repost of a couple of submissions I made on the ST Community site. It seems they are not searchable on the site, nor on Google, so I will republish them here in the hope to make them more visible.
I wanted to configure the I2S peripheral for the microphone MP45DT02 mounted on the STM32F4DISCOVERY board using STM32CubeMX and HAL. When searching for information regarding this I came across a couple of errors in the ST documentation I would like to point out, and at the same time give some hints on how to correctly capture audio data from the microphone. If I have made any mistakes I hope to be corrected.

References:

In [1] the "Data and Frame Format" for the I2S peripheral is set to "16 Bits Data on 32 Bits Frame" which I think is wrong. With "16 Bits Data on 16 Bits Frame" and an audio frequency of 16KHz, the data capturing rate in the peripheral will be 2*16 bits @ 16KHz and the I2S clock will oscillate with 2*16*16000 Hz. When the frame is extended to 32 bits the I2S clock will double its frequency to 2*32*16000 Hz, but the data capturing rate in the peripheral will not change. It is still 2*16 bits @ 16KHz. This means every other 16 bits of data coming from the microphone will be dropped as the pheripheral assumes they are zero. This is not what you want. I decided to use "32 Bits Data on 32 Bits Frame" to get a data aquisition rate of 2*32 bits @ 16KHz, i.e. 64 times oversampling.

This leads us to the next documentation error in [2]. When capturing 16 bits of data per frame, regardless of frame size, the size parameter is the number of 16 bit halfwords that will be captured. So far so good. When capturing 24 or 32 bits of data per frame the size parameter is the number of 32 bit words that will be captured, which results in 2*size number of 16 bit halfwords. This is not clearly expressed in the documentation of the function.

Setting the DMA channel to Circular will automatically restart the capture at the end of the buffer. To be able to work on stable data at the same time as the capturing is ongoing I use both the HAL_I2S_RxHalfCpltCallback() and HAL_I2S_RxCpltCallback(), each working on the part of the capturing buffer that is not currently being filled.
~ ~ ~
I thought I needed DMA double buffering as well and investigated how to get it to work. The HAL DMA module actually supports double buffering, but the HAL I2S module has no function to start a double buffer DMA transfer. I modified HAL_I2S_Receive_DMA() to work with double buffers and got it to work, but then I realized that HAL_I2S_RxHalfCpltCallback() can be used to accomplish the same thing and reverted my changes.

Yes, it is a PDM mono microphone which means you can't use the data as PCM samples directly. The data have to be filtered and decimated 64 times to become 16kHz PCM.
~ ~ ~ 

2019-05-12

Version Numbers

A version number is used to identify different versions, revisions or editions of an artifact. It is common to start to count from 0 and then continue with 1, 2, 3 and so on. No implied meaning should be assigned to any specific number. They are all equally good at identifying a version of the artifact.

A universal version number is open for branching at any existing version. There is always at least one free version number to pick. There are many ways to achieve this, but a simple variant only needs one extra rule; trailing zeros are free. You can always add ".0" to any version and it will still be the same version, e.g. 2, 2.0, 2.0.0, 2.0.0.0 are all the same version. Now when we want to branch, we add one or more zeros to designate the head of the branch. Then we increase the last zero with one for the first version on the branch.

   0 - 1 - 2 - 3 - 4 - 5 - (main line)
            \
      (2.0)  \- 2.1 - 2.2 - 2.3 - (1:st branch of v. 2)
              \
      (2.0.0)  \- 2.0.1 - 2.0.2 - 2.0.3 - (2:nd branch of v. 2)
                   \
      (2.0.1.0)     \- 2.0.1.1 - (1:st branch of v. 2.0.1)

It is easy to see that if you limit the number of dots in your version numbers, they are not universal.

In software it is common to track the version of different aspects of the artifact using dot notation and branches, e.g. 2.4, where the first number tracks the version of the interface and the second tracks the version of the implementation of that interface. Semantic Versioning[1] is one example of a versioning scheme that does this. Another case is when development takes place in multiple branches in parallel, for example when an old release is maintained at the same time as new functionality is added to a future release. Then you need a number to separate the branches.

   0.0           1.0 ---------> 2.0     3.0
      \           ^ \              \     ^
       \          |  \              \    |
        \- 0.1 - 0.2  \- 1.1 - 1.2   \- 2.1 - 2.2


Sometimes it is necessary to depart from the main line and branch off a separate version of the artifact. Semantic versioning lets you add an extra number to indicate that it is a patched version, e.g. 2.4.1. This has its limitations though. It is for example not possible to make changes to the interface as the interface version number is fixed. The insight that leads to a solution is that semantic version numbers always comes in pairs. Add a pair of numbers instead of just one, e.g. 2.4.0.1. The first is for interface changes and the second for implementation changes, just like the main line.

A few rules I try to follow for version numbers are...
  • All artifacts, insofar it is possible, should be able to tell you which version they are.
  • Change happens all the time. Don't assume you know beforehand when an artifact is free of errors and no longer will change.
  • Let the version numbers map to the branching strategy.
  • Use version numbers that are universal (see above).
  • Only change the version number when the artifact changes.
  • Don't use the version number for other things like review status, approval status or quality status.

The first version with a new major number, e.g. 1.0, 2.0 and so on, is not the end goal but the start of something new. If you treat it as the end goal you create a lot of practical problems for yourself. It is likely that 1.0 is less than perfect and you need to release a bugfix. You could call it 2.0 to replace 1.0 as the new perfect version with the high quality moniker .0, but it is better to reserve the first number to interface or other major changes where it serves a better purpose. You settle for 1.1. Now what do you call the pre-release version leading up to 2.0? Not 1.2 because that is the second bugfix release of 1.0. And you do want to give those internal pre-release artifacts a version number to be able to relate test results, reviews and other feedback to something traceable. Therefore it is better to start off the development with version 2.0 and tag the first few versions in the 2.x series as experimental pre-releases and then later decide which version of the 2.x series that happens to be good enough to qualify as the first usable version.

Release Candidate is also a useful concept, where you build an artifact that you hope will be good enough to later be released, but you don't know for sure until you have tested it. I am against rebuilding a good release candidate just to change the version number, therefore the RC designator will always be part of the version number, e.g. 2.0.12 which is the 12:th release candidate of version 2.0.

It is usually a mistake to use development versions and unstable pre-releases a la Semantic Versioning. Say you have decided to use something like Semantic Versioning to communicate with your users, but then say you won't follow it for 0.x versions. Then what's the point? It is likely that you will be stuck on 0.x for many years and your users will struggle to understand what will break in the next release. Use your versioning scheme right from the start, if only for the purpose of learning about its consequences. If you are too lazy to figure out if the interface has changed in the last iteration, just increase the major number to not give your users any unpleasant surprises. The urge not to waste numbers is a delusion. You will not run out of numbers and used numbers don't pollute.

To reiterate, a specific version number, say 2.0, is never a goal in itself. The purpose of the version number is to communicate what it supersedes and how extensive the changes are. The version number you eventually end up with is dictated by the changes you make and how often you release.

Another common misconception is when you insist that reviewed and approved documents should have the version number 1.0, 2.0, 3.0 and so on. The version number gets overloaded with meaning when its main purpose should be to uniquely identify the document and simplify traceability. Also, formal review is just one method among others to get feedback for improvement and if you link it to the version number it will suppress other types of feedback. One of the problems that this x.0 rule causes is that documents never get to be reviewed because the author thinks there still are too many loose ends for the document to be worthy a 1.0. Reviews are good for finding errors and spreading knowledge in the organization and a process that adds obstacles for reviews to take place is sub optimal. Another problem with x.0 is that you create artificial barriers to improve x.0 documents when you find errors because it costs too much effort to get it to the next y.0 version. There are very few organizations that can afford such rigor in the process. Most organizations I have worked with would fare much better with a process that prioritizes small and cheap iterative corrections to documents and only in rare cases require that documents should be reviewed and approved before the development can proceed. What I see in practice is that a 0.6 or a 2.1 never prevents the development to proceed, so why pretend to have a rigorous process when it only causes problems and discomfort? Track the quality status of a document with their own properties, e.g. as additional notes in the document changelog, so that both 0.7 and 2.3 can be reviewed as well as approved.

It is better to let the version number of documents follow the branching of the artifact they describe. This means you don't need to maintain a cross reference matrix for traceability. Documents with version 2.x contain changes that are valid for artifact version 2.y. If you need separate documentation for each minor version, then documents with version 2.1.x belong to artifact 2.1. You choose the granularity that is suitable for your circumstances. With documents in source control the most significant numbers in the version can be tracked globally by the branch. The least significant number can be assigned automatically, e.g. using the global Subversion repo revision or by counting the number of commits of the document in the branch.

Another reason to prefer the branch centric versioning scheme instead of the review centric is the way it changes how you think about your work. The artifact is never finished, only continuously improved.

A third superficial concern regarding version numbers is that users somehow are entitled to get all consecutive versions and that they will be confused if there is a jump in minor or major version between releases. If the user has version 2.3, they expect to get 2.4 next. To get 2.7 instead would cause severe psycological trauma. This is simply not true. Don't let this concern prevent you from tracking changes, may they be internal or external, with a universal version number. The persons that insist on this anti-pattern are typically also the ones that want to haphasardly jump to 10.0 just to catch up with competitors.

For practical advice on how to assign versions to software builds, see [2].

[1] https://semver.org/
[2] https://embeddedartistry.com/blog/2016/10/27/giving-you-build-a-version


2019-02-20

Wifi Remote Control

I have started to automate my home and thought that a dedicated remote control would be useful as an alternative to web pages and phone apps. Pushing a button on the remote performs an HTTP request that in turn controls some equipment at home.

An ESP8266 acts as wifi modem. Out of the box, it implements a serial protocol that is used to control its networking functions. An Arduino Pro Mini is master in the system and is responsible for monitoring the buttons and communicating with the ESP8266 to perform the desired action.

When the system is idle the ESP2866 is turned off and the Arduino is put to sleep. It is woken when a button push generates an interrupt on pin 2 or 3 (interrupt 0 and 1). An internal pull up resistor prevents the interrupt from being triggered when the button is inactive.

#include <avr/sleep.h>

#define GREEN_COLUMN 2
#define RED_COLUMN   3

#define FIRST_ROW   A3
#define SECOND_ROW  A2
#define THIRD_ROW   A1


int activeButton = 0;

void green_isr() { ... }

void red_isr() { ... }

void setup()
{
  pinMode(FIRST_ROW, OUTPUT); 
  pinMode(SECOND_ROW, OUTPUT);
  pinMode(THIRD_ROW, OUTPUT);

  digitalWrite(FIRST_ROW, 0);
  digitalWrite(SECOND_ROW, 0);
  digitalWrite(THIRD_ROW, 0);

  pinMode(GREEN_COLUMN, INPUT_PULLUP);
  pinMode(RED_COLUMN, INPUT_PULLUP);
 

  // Low power mode, no ADC
  ADCSRA &= ~(1<<ADEN);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
 

}

void loop()
{
  if (activeButton == 0) {
    attachInterrupt(0, green_isr, LOW);
    attachInterrupt(1, red_isr, LOW);

    ...
    sleep_cpu();
    ...

  } 
  else {
    detachInterrupt(0);
    detachInterrupt(1);

    ...
  }
}

You may notice that I don't use CLI and SEI before SLEEP which is necessary to make sure I don't miss an interrupt. This would have been required with for example interrupt mode FALLING but with interrupt mode LOW new interrupts keep getting generated and it doesn't matter if one is missed. We need the button to be pushed for longer than one interrupt anyway as we need some time to figure out which button is pressed, which leads us to the next step.

The 6 LED buttons are connected to the Arduino in a 3x2 matrix to share the two interrupt enabled pins, one for each column. By setting the row outputs to 0 one by one, it is possible to figure out which button is currently active.

#define GREEN1 5
#define GREEN2 7
#define GREEN3 9
#define RED1   4
#define RED2   6
#define RED3   8
 


int greens[4] = {0, GREEN1, GREEN2, GREEN3};
int reds[4] = {0, RED1, RED2, RED3};
 

int scanRow = 0;

void green_isr() 
{
  if (scanRow != 0) {
    activeButton = greens[scanRow];
  }
}

void red_isr()
{
  if (scanRow != 0) {
    activeButton = reds[scanRow];
  }
}
 

void loop()
{
    ...
    sleep_cpu();

    digitalWrite(FIRST_ROW, 1);
    digitalWrite(SECOND_ROW, 1);
    digitalWrite(THIRD_ROW, 1);

    scanRow = 1;
    digitalWrite(FIRST_ROW, 0);
    digitalWrite(FIRST_ROW, 1);

    scanRow = 2;
    digitalWrite(SECOND_ROW, 0);
    digitalWrite(SECOND_ROW, 1);

    scanRow = 3;
    digitalWrite(THIRD_ROW, 0);
    digitalWrite(THIRD_ROW, 1);

    scanRow = 0;
    digitalWrite(FIRST_ROW, 0);
    digitalWrite(SECOND_ROW, 0);
    digitalWrite(THIRD_ROW, 0);
    ...
}

The interrupt will be triggered when the correct row output is toggled. This results in an assignment of activeButton and the program moves to the second phase where the ESP8266 is started and instructed to do an HTTP request.

The serial data received from ESP8266 is written to a small circular buffer. Whenever a carriage return ("\r") is detected the buffer is inspected to see if we have received a message that we are interested in. The following messages will trigger an action in the code.

ready
OK
GOT IP
CONNECT
CLOSED
FAIL
ERROR

Whenever "ready" or "OK" is received, a command will be sent to the ESP8266. A normal exchange looks something like this.

< ready\r
> AT+CIPMUX=1\r\n
< OK\r
> AT+CWJAP="SSID","password"\r\n
< GOT IP\r
< OK\r
> AT+CIPMUX=1\r\n
< OK\r
> AT+CIPSTART=0,"TCP","192.168.0.102",8081\r\n
< CONNECT\r
< OK\r
> AT+CIPSEND=0,31\r\n
< OK\r
> GET /on?group=Nere HTTP/1.1\r\n\r\n
< OK\r
< OK\r
> AT+CIPCLOSE\r\n
< CLOSED\r

Note the HTTP GET and the dual CRLF which executes the command on the web server. The second "OK" thereafter is part of the response from the web server.

The messages "CLOSED", "FAIL" and "ERROR" terminates the communication, turns off the ESP8266 and puts the Arduino back in sleep mode. There is also a timeout condition that does the same.

#define ESP32_CS 10
#define ESP32_RST 11
 

unsigned long start = 0;

byte next = 0;
#define BUFSZ 16
byte buffer[BUFSZ] = { 0 };

byte   ready[5] = { 'r', 'e', 'a', 'd', 'y' };
byte   gotip[6] = { 'G', 'O', 'T', ' ', 'I', 'P' };
byte      ok[2] = { 'O', 'K' };
byte connect[7] = { 'C', 'O', 'N', 'N', 'E', 'C', 'T' };
byte  closed[6] = { 'C', 'L', 'O', 'S', 'E', 'D' };
byte    fail[4] = { 'F', 'A', 'I', 'L' };
byte   error[5] = { 'E', 'R', 'R', 'O', 'R' };

byte cmd = 0;


void setup()
{
  Serial.begin(115200);


  pinMode(ESP32_CS, OUTPUT);
  pinMode(ESP32_RST, OUTPUT);
  ...

}

void allOff()
{
  digitalWrite(GREEN1, 1);
  digitalWrite(GREEN2, 1);
  digitalWrite(GREEN3, 1);
  digitalWrite(RED1, 1);
  digitalWrite(RED2, 1);
  digitalWrite(RED3, 1);
}

bool received(byte* msg, byte n)
{
  int i;
  for (i = 0; i < n; i++) {
    if (buffer[ ( BUFSZ + next - (n+1) + i ) % BUFSZ ] != msg[i]) {
      return 0;
    }
  }
  return 1;
}

bool activeIsGreen()
{
  return activeButton == GREEN1 || activeButton == GREEN2 || activeButton == GREEN3;
}


void loop()
{
  if (activeButton == 0) {
    ...
    digitalWrite(ESP32_CS, 0);
    digitalWrite(ESP32_RST, 0);

    ...
    start = millis();
  }
  else {

    ...
    digitalWrite(ESP32_CS, 1);
    digitalWrite(ESP32_RST, 1);

    digitalWrite(activeButton, 0);

    if (Serial.available())
    {
      buffer[ ( next++ ) % BUFSZ ] = Serial.read();
      if (buffer[ ( BUFSZ + next-1 ) % BUFSZ ] == '\r') {
        if (received(ok, sizeof(ok))) {
          if (cmd == 1) {
            Serial.print("AT+CWJAP=\"SSID\",\"password\"\r\n");
          }
          else if (cmd == 2) {
            cmd = 3;
            Serial.print("AT+CIPMUX=1\r\n");
          }
          else if (cmd == 3) {
            Serial.print("AT+CIPSTART=0,\"TCP\",\"192.168.0.102\",8081\r\n");
          }
          else if (cmd == 4) {
            cmd = 5;
            if (activeIsGreen()) {
              Serial.print("AT+CIPSEND=0,31\r\n");
            }
            else {
              Serial.print("AT+CIPSEND=0,32\r\n");
            }
          }
          else if (cmd == 5) {
            cmd = 6;
            if (activeIsGreen()) {
              Serial.print("GET /on?group=Nere HTTP/1.1\r\n\r\n");
            }
            else {
              Serial.print("GET /off?group=Nere HTTP/1.1\r\n\r\n");
            }
          }
          else if (cmd == 6) {
            cmd = 7;
            // Wait for HTTP GET 200 OK
          }
          else if (cmd == 7) {
            cmd = 8;
            Serial.print("AT+CIPCLOSE\r\n");
          }
        }
        else if (received(ready, sizeof(ready))) {
          cmd = 1;
          digitalWrite(activeIsGreen() ? RED1 : GREEN1, 0);
          Serial.print("AT+CIPMUX=1\r\n");
        }
        else if (received(gotip, sizeof(gotip))) {
          cmd = 2;
          digitalWrite(activeIsGreen() ? RED2 : GREEN2, 0);
          start = millis();
        }
        else if (received(connect, sizeof(connect))) {
          cmd = 4;
          digitalWrite(activeIsGreen() ? RED3 : GREEN3, 0);
          start = millis();
        }
        else if (received(closed, sizeof(closed)) ||
                 received(fail, sizeof(fail)) ||
                 received(error, sizeof(error))) {
          cmd = 0;
          allOff();
          activeButton = 0;

        }
      }
    }
    else if (millis() - start > 8000UL) {
      cmd = 0;
      allOff();
      activeButton = 0;

    }     
  }

}

Worth noting here is the timeout expression "millis() - start > 8000UL" which always works, especially when the number of millseconds rolls over to zero. A slightly different expression like "millis() > start + 8000UL" does not work as expected close to the rollover point.

As you can see, I have the same action on all green and red buttons, but this can easily be extended with a couple of if statements where the command is sent.

The three LED buttons of the opposite color of the one being pushed are used as a tiny progress bar.

To reduce the power consumption in idle mode I removed the power indicator LEDs from the Arduino and the ESP8266.

When debugging the serial communication I used my PC and the program socat to print a trace of the communication between ESP8266 and Arduino. I used two FTDI USB to Serial cables connected to the Arduino and an adapter board for the ESP8266 which besides the connector for FTDI also contains a 3.3V regulator and voltage dividers on RST, CS and RXT to allow it to be driven from the 5V Arduino. It is visible in the image, here connected directly to the Arduino.

$ socat -v /dev/ttyUSB0 /dev/ttyUSB1

It is highly recommended as a debugging tool.

Debugging with Popper