Learning C: UniFi Doorbell MQTT – Working with cJSON and Threads
In the second part of my UniFi Doorbell MQTT series, I dive into parsing JSON with cJSON, handling MQTT messages safely, and what I learned about threading in C.
In the first post of this series, I covered the overall goals and architecture of the UniFi Doorbell MQTT project. It is a small C-based service that updates the animations and sounds on a UniFi G4 Doorbell using SCP and SSH, with MQTT support for Home Assistant.
In this part, I made solid progress on the core communication logic. I set up cJSON, learned more about threads and concurrency in C, and solved an issue with the MQTT callback blocking the socket.
Resources for This Project
ChrisHansenTech is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to Amazon.com. As an Amazon Associate I earn from qualifying purchases.
These are the main tools, hardware, and references I’m using in this project:
Software
- Visual Studio Code
- Windows Subsystem for Linux (WSL)
- Doing all the development in Ubuntu running in Windows.
Hardware - Target devices
References
- The C Programming Language (book)
- C Programming: A Modern Approach (book)
- Hands-On Network Programming with C (book)
Getting cJSON Working
I decided to use cJSON for parsing and creating JSON objects. It’s lightweight, easy to integrate, and well suited for a project like this.
To incorporate it, I added cJSON.h and cJSON.c to my project under
include/third_party/cJSON/ and src/third_party/cJSON/. Once compiled in,
parsing was straightforward.
Here is an example that loads my config.json file, which contains a set of
holidays I map to animation folders:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
bool config_load(const char *filename, config_t *config) {
FILE *file = fopen(filename, "r");
if (!file) {
fprintf(stderr, "[ERROR] Configuration file '%s' does not exist.\n", filename);
return false;
}
fseek(file, 0, SEEK_END);
long length = ftell(file);
rewind(file);
char *data = malloc(length + 1);
size_t bytes_read = fread(data, 1, length, file);
if(bytes_read != (size_t)length) {
free(data);
fclose(file);
return false;
}
data[length] = '\0';
fclose(file);
const char *error_ptr = NULL;
cJSON *root = cJSON_ParseWithOpts(data, &error_ptr, 0);
free(data);
if (!root) {
printf("[ERROR] Configuration JSON parsing error before: %s\n", error_ptr);
return false;
}
cJSON *holidays = cJSON_GetObjectItem(root, "holidays");
if (!holidays) {
cJSON_Delete(root);
return false;
}
int count = cJSON_GetArraySize(holidays);
config->holiday_names = malloc(sizeof(char*) * count);
config->folder_names = malloc(sizeof(char*) * count);
config->count = count;
int i = 0;
cJSON *item = holidays->child;
while (item) {
config->holiday_names[i] = strdup(item->string);
config->folder_names[i] = strdup(cJSON_GetStringValue(item));
item = item->next;
i++;
}
cJSON_Delete(root);
return true;
}
The MQTT Callback Problem
While testing message handling, the MQTT client stopped responding after a few messages. The callback was doing all the work directly, and because the callback runs on the network thread, it blocked new messages and pings.
That sent me into a deeper look at how the MQTT loop works under the hood and how easily a callback can cause a stall.
I fixed this by creating a worker thread. The callback now pushes messages into a thread-safe queue, and the worker thread pulls them off and processes them. The MQTT loop stays responsive, and the blocking problem disappears.
Learning About Threads in C
Threading in C is very hands-on. You have to manage mutexes, condition variables, and shared memory yourself. It took a bit to get it right, but it was a good way to deepen my understanding.
This is the general flow I ended up with:
- The MQTT callback pushes received messages into a queue.
- A worker thread waits for items and processes them.
- The main MQTT loop never blocks or pauses.
Once this was in place, the client handled messages smoothly. Even small mistakes like reading shared memory without a mutex became obvious while debugging.
Sending and Receiving JSON Messages
With threading sorted out, I tested sending and receiving JSON commands.
Home Assistant can now publish messages like:
1
{ "type": "holiday", "name": "Christmas" }
The service parses the JSON, validates it, and publishes an update back:
1
{ "status": "uploading", "target": "Christmas" }
This is the first time the service could reliably send and receive structured messages without blocking or crashing.
What I Learned This Week
This part of the project taught me quite a bit:
- cJSON is a great option for simple JSON work.
- Threading in C takes patience, but it makes a huge difference once set up correctly.
- Callbacks should stay light so they do not block the network loop.
- A worker thread keeps everything responsive.
It is starting to feel like the project is taking shape.
If you’re following along or experimenting with similar setups, I’d love to hear about it. You can find the code and updates on GitHub – doorbell-mqtt-unifi
Part of the ongoing Learning C and Assembly series.
Want to share your thoughts or ask a question?
This blog runs on coffee, YAML, and the occasional dad joke.
If you’ve found a post helpful, you can
support my work or
☕ buy me a coffee.
Curious about the gear I use? Check out my smart home and homelab setup.
