Criando JSON com hierarquia de dois níveis
Agora vamos avançar para um caso mais realista. Em vez de criar um JSON totalmente plano, vamos montar uma mensagem com hierarquia de dois níveis, ou seja, um objeto principal contendo objetos internos.
Isso é muito comum em firmware embarcado, porque normalmente queremos separar os dados por contexto. Por exemplo, podemos ter um bloco com identificação do dispositivo, outro bloco com leituras dos sensores e outro bloco com informações de diagnóstico.
A mensagem que vamos gerar terá esta estrutura:
{
"device": {
"id": "mcu-001",
"model": "generic-mcu",
"firmware": "1.0.0"
},
"telemetry": {
"temperature": 28.75,
"voltage": 3.29,
"uptime": 15240
}
}
Observe que temos um objeto raiz, e dentro dele existem dois objetos filhos: device e telemetry. Esse é o segundo nível da hierarquia.
Em termos visuais:
root
├── device
│ ├── id
│ ├── model
│ └── firmware
└── telemetry
├── temperature
├── voltage
└── uptime
Essa organização torna a mensagem mais clara. O servidor que recebe o JSON consegue distinguir facilmente o que é identificação do equipamento e o que é dado medido. Em projetos IoT, essa separação ajuda bastante quando a API cresce, porque novos blocos podem ser adicionados sem bagunçar a estrutura principal.
A seguir, vamos criar funções mock para simular sensores e informações do dispositivo.
#ifndef SENSOR_MOCK_H
#define SENSOR_MOCK_H
float sensor_mock_read_temperature(void);
float sensor_mock_read_voltage(void);
unsigned long sensor_mock_get_uptime(void);
#endif
Agora a implementação:
#include "sensor_mock.h"
/**
* @brief Simula a leitura de temperatura.
*
* Em um microcontrolador real, esta função poderia acessar um sensor
* via ADC, I2C, SPI, 1-Wire ou outro barramento.
*
* @return Temperatura simulada em graus Celsius.
*/
float sensor_mock_read_temperature(void)
{
return 28.75f;
}
/**
* @brief Simula a leitura da tensão de alimentação.
*
* Em um firmware real, esta leitura poderia vir de um ADC interno
* ou de um divisor resistivo ligado a uma entrada analógica.
*
* @return Tensão simulada em volts.
*/
float sensor_mock_read_voltage(void)
{
return 3.29f;
}
/**
* @brief Simula o tempo de atividade do sistema.
*
* Em um microcontrolador real, esse valor poderia vir de um contador
* de milissegundos, de um timer de sistema ou do tick do RTOS.
*
* @return Tempo de atividade simulado em segundos.
*/
unsigned long sensor_mock_get_uptime(void)
{
return 15240UL;
}
Agora vamos montar uma função responsável por criar o JSON. Ela retornará uma char *, ou seja, uma string alocada dinamicamente contendo o JSON final. Quem chamar essa função ficará responsável por liberar essa memória com free().
#ifndef JSON_BUILDER_H
#define JSON_BUILDER_H
char *json_builder_create_telemetry_message(void);
#endif
Agora a implementação:
#include <stdlib.h>
#include "cJSON.h"
#include "sensor_mock.h"
#include "json_builder.h"
/**
* @brief Cria uma mensagem JSON de telemetria com hierarquia de dois níveis.
*
* Estrutura gerada:
*
* {
* "device": {
* "id": "mcu-001",
* "model": "generic-mcu",
* "firmware": "1.0.0"
* },
* "telemetry": {
* "temperature": 28.75,
* "voltage": 3.29,
* "uptime": 15240
* }
* }
*
* @return Ponteiro para string JSON alocada dinamicamente.
* O chamador deve liberar com free().
* Retorna NULL em caso de erro.
*/
char *json_builder_create_telemetry_message(void)
{
char *json_string = NULL;
cJSON *root = cJSON_CreateObject();
if (root == NULL)
{
return NULL;
}
cJSON *device = cJSON_CreateObject();
if (device == NULL)
{
cJSON_Delete(root);
return NULL;
}
cJSON *telemetry = cJSON_CreateObject();
if (telemetry == NULL)
{
cJSON_Delete(root);
return NULL;
}
cJSON_AddStringToObject(device, "id", "mcu-001");
cJSON_AddStringToObject(device, "model", "generic-mcu");
cJSON_AddStringToObject(device, "firmware", "1.0.0");
cJSON_AddNumberToObject(telemetry, "temperature", sensor_mock_read_temperature());
cJSON_AddNumberToObject(telemetry, "voltage", sensor_mock_read_voltage());
cJSON_AddNumberToObject(telemetry, "uptime", sensor_mock_get_uptime());
cJSON_AddItemToObject(root, "device", device);
cJSON_AddItemToObject(root, "telemetry", telemetry);
json_string = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return json_string;
}
Há um detalhe muito importante nesse código. Depois que usamos:
cJSON_AddItemToObject(root, "device", device);
o objeto device passa a pertencer ao objeto root. O mesmo acontece com telemetry. Isso significa que, ao chamar:
cJSON_Delete(root);
a cJSON também libera os objetos filhos automaticamente. Portanto, não devemos chamar cJSON_Delete(device) ou cJSON_Delete(telemetry) depois que eles foram adicionados ao root, pois isso poderia causar liberação duplicada de memória.
Agora podemos testar essa função no main.c:
#include <stdio.h>
#include <stdlib.h>
#include "json_builder.h"
int main(void)
{
char *message = json_builder_create_telemetry_message();
if (message == NULL)
{
printf("Erro ao criar mensagem JSON.\n");
return 1;
}
printf("Mensagem JSON gerada:\n%s\n", message);
free(message);
return 0;
}
A saída esperada será parecida com:
{"device":{"id":"mcu-001","model":"generic-mcu","firmware":"1.0.0"},"telemetry":{"temperature":28.75,"voltage":3.29,"uptime":15240}}
Esse formato é compacto porque usamos cJSON_PrintUnformatted(). Para debug, poderíamos usar temporariamente:
json_string = cJSON_Print(root);
Nesse caso, a saída ficaria mais legível, com indentação e quebras de linha. Porém, em comunicação real entre microcontrolador e servidor, a versão compacta costuma ser mais adequada.
Um cuidado adicional: as funções cJSON_AddStringToObject() e cJSON_AddNumberToObject() podem falhar se não houver memória suficiente. Em exemplos didáticos, muitas vezes ignoramos isso para manter o código mais limpo. Porém, em firmware crítico, é melhor criar cada item separadamente, verificar se foi criado corretamente e só então anexá-lo ao objeto pai.
A ideia central desta seção é que o JSON pode ser usado como uma pequena “árvore de dados”. No nível raiz, temos a mensagem completa. No segundo nível, temos blocos especializados, como device e telemetry. Esse padrão é muito útil para IoT, porque permite que o firmware envie mensagens organizadas e fáceis de evoluir.