See Also: part 1 and part 2.
There is always a first time for everything, and my first time doing this was a few years ago with a day job task. I was going to create a Windows DLL (dynamic link library) that would handle hundreds of messages (write/read response) via the I2C protocol.
The message protocol featured a header that contained a Message Type and Command Code. There were several types of messages, but for this blog post let’s just look at two types:
- DATA – a message sent to do something, with our without payload data. These messages get a response back that is either an ACKnowledgement (it worked) or a NAK (it did not work). For example “set the time to X”.
- QUERY – a message sent to request information, such as “what time is it?”
The Command Code was a byte, which meant there could be up to 256 (0-255) commands per message type. For example, some DATA messages might be defined like this:
#define DATA_PING 0 // ACK if there, NAK if not
#define DATA_RESET_CPU 1 // Reset CPU
#define DATA_EXPLODE 2 // Cause the penguin on the TV set to explode.
And QUERY messages might be like:
#define QUERY_STATUS 0 // Returns a status response
#define QUERY_VOLTAGE 1 // Returns the voltage
#define QUERY_TEMPERATURE 2 // Returns the temperature
These are, of course, made up examples, but you get the idea.
We had a number of different board types on our system that could receive these messages. Some messages were only applicable to specific boards (like, you couldn’t get temperature from a board that did not have a thermometer circuit or whatever). My idea was to create simple C functions that represented the message sent to the specific board, like this:
resp = AlphaBoardPING ();
resp = BetaBoardPING ();
resp = DeltaBoardPING ();
...
resp = BetaBoardRESET_CPU ();
...
resp = DeltaBoardQUERY_STATUS (...);
…and so on. I thought it would make a super simple way to write code to send messages and get back the status or response payload or whatever.
This is what prompted me to write a post about returning values as full structures in C. That technique was used there, making it super simple to use (and no chance of a wrong pointer crashing things).
I experimented with these concepts on my own time to make sure this idea would work. Some of the things I did ended up on my GitHub page:
- https://github.com/allenhuffman/GetPutData – routines that let you put bytes or other data types in a buffer. Code similar to this was used to create the message bytes (header, payload, checksum at the end, etc.)
- https://github.com/allenhuffman/StructureToFromBuffer – routines that would let me define how bytes in a buffer should be copied into a C structure. I was proud of this approach since it let me pass a pointer to a buffer of bytes along with some tables and have it return a fully populated C structure ready to use. This greatly simplified the amount of work needed to use these messages.
But, as I like to point out, I am pretty lazy and hate doing so much typing. The thought of creating hundreds of functions by hand was not how I wanted to spend my time. Instead, I wanted to find a way to automate the creation of these functions. After all, they all followed the same pattern:
- Header – containing Message Type, Command Code, number of byte in a payload, etc.
- Payload – any payload bytes
- Checksum – at the end
The only thing custom would be what Message Type and Command Code to put in, and if there was a payload to send, populating those bytes with the appropriate data.
When a response was received, it would be parsed based on the Message Type and Command Code, and return a C structure matching the response payload.
A program to write a program?
Initially, I thought about making a program or script that would spit out hundreds of functions. But, this not lazy enough. Sure, I could have done this and created hundreds of functions, but what if those functions needed a change later? I’d have to update the program that created the functions and regenerate all the functions all over again.
There has to be a better lazier way.
Macros: a better lazier way
I realized I could make a set of #define macros that could insert proper C code or prototypes. Then, if I ever needed to change something, I only had to change the macro. There would be no regeneration needed, since the next compile would use the updated macro. Magic!
It worked very well, and created hundreds and hundreds of functions without me ever having to type more than the ones in the macro.
It worked so well that I ended up using this approach very recently for another similar task I was far too lazy to do the hard way. I thought I would share that much simpler example in case you are lazy as well.
A much simpler example
At work we use LabWindows/CVI, a Windows C compiler with its own GUI. It has a GUI editor where you create your window with buttons and check boxes and whatever, then you use functions to load the panel, display it, and hide it when done. They look like this:
int panelHandle = LoadPanel (0, "PanelMAIN.uir", PANEL_MAIN);
DisplayPanel (panelHandle);
// Do stuff...
HidePanel (panelHandle);
DiscardPanel (panelHandle);
Then, when you interact with the panel, you have callback functions (if the user clicks the “OK” button, it jumps to a function you might name “UserClickedOKButtonCallback()” or whatever.
If you need to manipulate the panel, such as changing the status of a Control (checkbox, text box, or whatever), you can set Values or Attributes of those Controls.
SetCtrlVal (panelHandle, PANEL_MAIN_OK_BUTTON, 1);
SetCtrlAttribute (panelHandle, PANEL_MAIN_CANCEL_BUTTON, ATTR_DIMMED, 1);
It is a really simple system and one that I, as a non-windows programmer who had never worked with GUIs before, was able to pick up and start using quickly.
Simplicity can get complicated quickly…
One of the issues with this setup is that you had to have the panel handle in order to do something. If a message came in from a board indicating there was a fault, that code might need to toggle on some “RED LED” graphics on a GUI panel to indicate the faulted condition. But, that callback function may not have any of the panel IDs. The designed created a lookup function to work around this:
int mainPanelHandle = LookUpPanelHandle(MAIN_PANEL);
SetCtrlVal (mainPanelHandle, PANEL_MAIN_FAULT_LED, 1);
A function similar to that was in the same C file where all the panels were loaded. Their handles saved in variables, then the LookUp function would go through a huge switch/case with special defines for every panel and return the actual panel handle that matched the define passed in.
It worked great but it was slower since it had to scan through that list every time we wanted to look up a panel. At some point, all the panel handles were just changed to global variables so they could be accessed quickly without any lookup:
SetCtrlVal (g_MainPanelHandle, PANEL_MAIN_FAULT_LED, 1);
This also worked great, but did not work from threads that did not have access to the main GUI context. Since I am not a Windows programmer, and have never used threads on any embedded systems, I do not actually understand the problem (but I hear there are “thread safe” variables that can be used for this purpose).
Self-contained panel functions for the win!
Instead of learning those special “thread safe” techniques, I decided to create a set of self-contained panel functions so you could do things like this:
int mainPanelHandle = PanelMainInit (); // Load/init the main panel.
PanelMainDispay (); // Display the panel.
SetCtrlVal (mainPanelHandle, PANEL_MAIN_FAULT_LEFT, 1);
...
PanelMainHide ();
...
PanelMainTerm (); // Unload main panel and release the memory.
When I needed to access a panel from another routine, I would use a special function that returned the handle:
int panelMainHandle = PanelMainGetHandle ();
SetCtrlVal (mainPanelHandle, PANEL_MAIN_FAULT_LEFT, 1);
I even made these functions automatically Load the panel if needed, meaning a user could just start using a panel and it would be loaded on-demand if was not already loaded. Super nice!
Here is a simple version of what that code looks like:
static int S_panelHandle = 0; // Zero is not a valid panel handle.
int PanelMainInit (void)
{
int panelHandle = 0;
if (S_panelHandle <= 0) // Zero is not a valid panel handle.
{
panelHandle = LoadPanel (0, "PanelMAIN.uir", PANEL_MAIN);
// Only set our static global if this was successful.
if (panelHandle > 0) // Zero is not a valid panel handle.
{
S_panelHandle = panelHandle;
}
}
else // S_panelHandle was valid.
{
panelHandle = S_panelHandle;
}
// Return handle or status in case anyone wants to error check.
return panelHandle;
}
int PanelMainGetHandle (void)
{
// Return handle or status in case anyone wants to error check.
return PanelMainInit ();
}
int PanelMainTerm (void)
{
int status = UIEHandleInvalid;
if (S_panelHandle > 0) // Zero is not a valid panel handle.
{
status = DiscardPanel (S_panelHandle);
if (status == UIENoError)
{
S_panelHandle = 0; // Zero is not a valid panel handle.
}
}
// Return status in case anyone wants to error check.
return status;
}
int PanelMainDisplay (void)
{
int status = UIEHandleInvalid;
int panelHandle;
panelHandle = PanelMainInit (); // Init if needed.
if (panelHandle > 0) // Zero is not a valid panel handle.
{
status = DisplayPanel (panelHandle);
}
// Return status in case anyone wants to error check.
return status;
}
int PanelMainHide (void)
{
int status = UIEHandleInvalid;
if (S_panelHandle > 0) // Zero is not a valid panel handle.
{
status = HidePanel (S_panelHandle);
}
// Return status in case anyone wants to error check.
return status;
}
This greatly simplified dealing with the panels. Now they could “just be used” without worrying about loading, etc. There was no long Look Up table, and no global variables. The only places the panel handles were kept was inside the file where the panel’s functions were.
Nice and simple, and it worked even greater than the first two attempts.
…until you have to make a hundred of these functions…
…and then decide you need to change something and have to make that change in a hundred functions.
My solution was to use #define macros to generate the code and prototypes, then I would only have to change the macro to alter how all the panels works. (Spoiler: This worked even greater than the previous greater.)
In part 2, I will share a simple example of how this works. If you are lazy enough, you might actually find it interesting.
Until then…