Integrating FreeRTOS for simple connection/sending packets for ESP32-based applications

So I’m planning on writing an app for ESP32 that uses RTOS as a means of “handling cases” in terms of switching to a specific state based on the condition.

Came up with this flow of the program where I have 3 tasks:

  • Connection_task: takes care of wifi init and once connected, writes to a Connection_Done queue which the SendPktTask is waiting on.
  • SendPktTask: Once connection is done, it waits on the packet to send which is provided by DataTask. Once the queue is populated, it goes onto send the packet and then waits on the respective queues for the next packet to be sent.

I’m not sure if it’s the best approach but that’s just an abstrac that I came up with, and any suggestions would be appreciated. (not all the states are shown though)

My concern is whether Connection_Done queue should exist, or rather a task notification should be used since that’s basically what it’s acting as.

The basic idea is to use this library with any other application particularly where have to send data elsewhere.

So for instance, if the connection goes down, the RECOVERY_STATE should do the handling accordingly whereas SendPkt task waits on the queue

Sounds reasonable so far. I wonder if connection done (and I think also disconnect) signaling needs to be done at all. Assuming the data packets are sent over WiFi connection the send calls probably just error out if the connection is down.
So you could keep the data and packet tasks running and draining the packet queue, which might otherwise run full and perhaps needs to be flushed (on reconnect).
Or maybe a simple state flag set by the connection task is just fine to avoid useless send calls if the connection is not yet (re-)established and discard the incoming packets immediately.

the send calls probably just error out if the connection is down.

I was trying to make it so that the tasks are independent in the sense that ConnectTask takes care of the connection whereas SendPktTask takes care of sending packets as long as there’s connection and a packet to send. So if there’s no connection or packet to send, SendPktTask just waits…

So you could keep the data and packet tasks running and draining the packet queue, which might otherwise run full and perhaps needs to be flushed (on reconnect).

So more like waiting on the queue for receiving data first and then ensuring the connection exists before going on to sending the packet. This way we are not going to overflow the queue and rather read from it as the data comes in, yeah

My thought would be that when you have one task just waiting for another, it often makes sense to combine them. It could be that a bettor organization is a single task first calls a ‘Connection’ function that does what is need to wait for a connection and do what is needed there, Then the function returns and you do the SendPkt routine to send packets, and when it gets an error due to the connection breaking, to goes back to the Connection routine. One task with two modes.

Unless connection has something to do once the connection is established (after it would start SendPkt) there is no reason to have two tasks that always at least one of them has nothing to do, with a very clean transfer of which is running.

An additional benefit of the solution suggested by Richard Damon is that only one task accesses your WiFi driver. So you wont need to bother about whether or not the WiFi driver is thread safe - for example, conditions like send task must not try to send when re-connection is in progress.

Thanks.

1 Like

So you’re saying just have one task that takes care of the wifi connection and sending packets as opposed to have two different tasks, yeah?

I’m using switch...case where I have each case defined, one of which is for connection and sending packets now if I include sending packets part in the same task as well. So I don’t have specific functions for each method. Does that still sound okay to you?

Yes, that can work, or just different parts of the loop. Making them functions just puts less code in one chunk to make things easier to understand.

right, but how’d you use functions in state machines? unless you wanna change the approach and not use states…

Something like this?

so in the Send_Pkt state, I’m calling xQueueReceive(dataQueue) and then xQueueSend(destinationQueue) basically?

State machines are reasonable ways to document operation, but not always the direct way to implement them, though one way that is done is each state is a function, which returns the state to go to next, and the task main is basically an infinite loop and a switch.

Your state diagram isn’t complete (unless loss of connection is terminal) as you have dead end states that should probably go back to the wait for connection state.

Yes, Send_Pkt may be a simple xQueueReceive, check if still connected, and the xQueueSend. If you have lost connection, switch to a lost connection state that tries to reconnect.

one way that is done is each state is a function, which returns the state to go to next, and the task main is basically an infinite loop and a switch.

each state is a function? you’re not using switch..case and rather just function calls who’s return value contains the next state?

yes, I didn’t include all the transitions in the state diagram.

so something like:

xQueueReceive(xDataQueue, (void*)localBuffer); // receive data
SendPacket(localBuffer);

also, how do you make the localBuffer generic in the sense that it could take data of different type?

For each state a function, something sort of like:

void task(void* parm){
   int state = 0;
   while(1){
      switch(state){
      case 0: state = state1fun(); break;
      case 1: state = state2fun(); break; 
...
      default: /* Some sort of error recovery here */
      }
  }
{

xQueueReceive only handles fixed sized objects, so you just need a local buffer that sized.
Network packets are often passed by address (so the queue just store a pointer) and the overall structure handles to reusing a buffer until you are done with it.

so this approach with functions is cleaner but logically the same as far as I see.

And my concern is the type of the local buffer more than the size. How would I determine the type of the data being sent from the sender task?

Queue send ‘monolithic’ data. Any variability in what that data means has to be contained inside it. Maybe your buffers should begin with a message type item.

so something as below could be sent with type defined but how would you know on the receiving end about the type? dynamically allocate the buffer first based on type?

typedef struct {
   int type;
   char buffer[];
} message_t;

I suggest you read the section on Queue again carefully. Queue send and receive FIXED SIZED objects, and do so by copying them (twice). So to send variable sized messages, or big messages, your generally do so by sending pointers to the buffers. If you send pointer to buffers, then the receiving side doesn’t need to allocate anything but the space for the buffer POINTER. The buffer itself is never copied (which means that buffer can’t be used again until the receiver is done with it.

Note, that says that on the xQueue call you send the address of a pointer, something like:

message_t msg;
message_t *ptr = &msg;
xQueueSend(queueHandle, &ptr, config_MAX_DELAY);

and receive with something like:

message_t *ptr;

xQueueReceive(queueHandle, &ptr, config_MAX_DELAY);

Note the type of ptr.

There is also a MessageBuffer type that can send variable length messages. (And again, it does it by copying, so they shouldn’t be too large).

I’m sending fixed size blocks too. and isn’t sending ptr and &msg mean the same? you’re passing the address of msg either way, isn’t it?

The queue handle contains pcWriteTo and pcHead which are used for allocating the dynamic memory that stores the messages. And their types are int8_t, and upon memcpy’ing, it’s cast to void*.

Question is: the sender needs to send, say, a value of type int. Would it not store that value somewhere in message_t? If yes, what would be the type of the member variable?

But I am send &ptr, not ptr. I.E the object to send is the pointer, not the buffer.

The Queue, once created, NEVERS allocates more storage, but is effectively an array of message_size buffers that get copied into and out of. The parameter you pass is the ADDRESS of the buffer to send, from which it copies the message_size bytes.
ou ca
Note also, your ‘buffer’ isn’t converted into a void*, but the address of the buffer is, so that you can pass the address of anything (but it you need to be sure it is the right sort of thing).

Think about what a message needs to say, if someone came up to you and said ‘5’ would it mean much? Only if you had prior agreed on some code, so with messages, you need to define what messages mean, and all you get are a sequence of bytes, so YOU need to assign how to interpret them.