Introdução
Os sistemas supervisórios devem ser capazes de armazenar os dados históricos da planta de forma permanente. Normalmente, estes dados são valores de variáveis, horário de logins de usuário, alertas e quaisquer outros eventos relevantes ocorridos na planta monitoradas, sendo importantes para registrar todo o histórico da planta.
Uma das formas mais eficiente, confiável e segura de armazenar estes dados é utilizando softwares de bancos de dados (do inglês, Data Base, ou DB). Além de viabilizarem um armazenamento seguro, estes softwares disponibilizam ferramentas para recuperar e manipular os dados de forma eficiente.
Para a nossa prática, iremos interagir o Node-RED com um servidor de banco de dados do Mongo-DB, software gratuito e muito utilizado devido a sua versatilidade e fácil integração com Javascript, permitindo a escrita e leitura de dados diretamente em formato JSON.
Na prática, o software do Mongo-DB disponibiliza um servidor de banco de dados e um serviço que permite receber chamadas em uma das portas da máquina que o executa. Assim, qualquer software terceiro que deseja utilizar este serviço deve interagir com o Mongo-DB através de seu endereço URI (i.e., https://<IP>:<PORTA>). Por padrão, o Mongo-DB utiliza a porta 27017.
Organização dos dados
Um único servidor Mongo-DB é capaz de armazenar vários bancos de dados, que são unidades de armazenamento independentes identificados por um nome único naquele servidor. O identificador deve ser uma string única naquele servidor. Recomenda-se definir seu nome somente com letras minúsculas, sem espaços ou caracteres especiais.
Cada banco de dados pode ter diversas coletâneas (do inglês, collection), que são similares ao conceito de planilhas do excel, permitindo registrar diversas colunas de diferentes tipos de dados, cada coluna podendo receber várias entradas (linhas/rows) de dados. Cada coletânea também é identificada por uma string única cujo nome deve ser definido de maneira similar aos identificadores dos bancos de dados.
A tabela a seguir é um exemplo de dados registrados em uma coletânea do Mongo-DB:
| _id | timestamp-da-leitura | valor-do-sensor |
|---|---|---|
| 001 | 1737464134322 | 23.4 |
| 002 | 1737464154150 | 23.8 |
| 003 | 1737464166302 | 24.2 |
| 004 | 1737464181754 | 24.6 |
| 005 | 1737464196396 | 24.4 |
No exemplo acima, a coletânea possui três colunas de dados (_id, timestamp-da-leitura e valor-do-sensor) e cinco linhas de dados (rows). Os nomes das colunas podem ser livremente definidos.
O Mongo-DB exige a existência da coluna _id, que é um identificador único para cada linha. Se você não definir esta coluna/valor explicitamente, o Mongo-DB fará isto automaticamente para você.
Cada linha de dados inserida em uma coletânea deve seguir a estrutura de dados já definidas pelas colunas. O Mongo-DB permite que tipos de dados diferentes sejam enviados para uma mesma coluna, o que porém é uma prática não recomendada a menos que seja explicitamente assim desejado.
Instalando o Mongo-DB
O Mongo-DB pode ser instalado diretamente no Sistema Operacional. Os links a seguir descrevem como fazê-lo no Linux, macOS e Windows.).
Entretanto, para quem já tem o Docker instalado na máquina, basta executar o seguinte comando para levantar um container executando o servidor do Mongo-DB:
docker run -d -p 27017:27017 --name mongo-container -v mongo-data:/data/db --network net-nodered mongo:latest
Os parâmetros do comando acima são:
docker run: Invoca o docker e pede para ele rodar um container a partir de uma imagem.-d: Executa em modo "detached", i.e., o container será executado em segundo plano, não "prendendo" o terminal atual.-p 27017:27017: Mapeia a porta27017o computador host na porta27017do container, i.e., chamadas na porta27017do host serão direcionadas para a porta27017do container. Caso queira disponibilizar o Mongo-DB em outra porta do host, pode-se alterar o primeiro valor deste argumento. Por exemplo, para disponibilizar o BD na porta30301, pode-se alterar este argumento para-p 30301:27017.--name mongo-container: Nome do container que será executado. Este nome pode ser alterado para o que preferir.-v mongo-data:/data/db: Cria um volume de armazenamento permanente no host chamadomongo-datae remapeia este volume no caminho/data/dbdo container. Este parâmetro garante que os dados armazenados pelo Mongo-DB serão permanentes enquanto o volumemongo-dataexistir no host, mesmo que o container do servidor seja deletado. Este parâmetro é essencial na criação de servidores de banco de dados.--network net-nodered: conecta o container à redenet-nodered. Este parâmetro garante que o container do banco de dados estará conectado à mesma rede virtual que o container do Node-RED.mongo:latest: O container será criado utilizando a imagemmongoem sua versãolatest(i.e., a última versão estável disponível). Note que o docker faz automaticamente o download desta imagem a partir do dockerhub, que é seu servidor de imagens oficial.
Após levantar o servidor, pode-se testar se o Mongo-DB está disponível utilizando o comando nmap:
nmap -p <PORTA-DO-MONGO> <IP>
Caso esteja executando o docker em sua própria máquina, pode substituir o parâmetro <IP> por localhost.
Existem softwares que permitem interagir com um banco de dados a fim de permitir visualizar e manipular os dados armazenados de maneira mais visual e interativa. Para o Mongo-DB recomenda-se o MongoDB Compass.
Interagindo o Node-RED com um servidor Mongo-DB
Para interagir com o servidor Mongo-DB, instale no Node-RED o módulo node-red-contrib-mongodb4, cujo manual pode ser encontrado neste link. Ao instalá-lo, irá aparecer na paleta o nó mongodb4, que permite interagir e realizar operações em um servidor Mongo-DB.

Para iniciar, arraste o nó mongodb4 para o flow e dê 2x cliques nele para abrir suas propriedades. A propriedade Connection permite criar uma conexão com um servidor em execução. Para isto, clique no símbolo de + e abrirá a janela Simple Connection URI. Como configurações mínimas, defina os seguintes parâmetros:
Hostname: Insira o IP da máquina que está executando o servidor.Port: Insira a porta em que o serviço está disponibilizado.Database: O nome do banco de dados que será utilizado. Caso ainda não exista, o nó irá criá-lo no servidor.
Caso esteja rodando o Node-RED e o Mongo-DB ambos em containers docker, fique atento pois o IP será o próprio nome do container do Mongo-DB (que o Docker automaticamente resolve para o endereço do mesmo em sua rede interna) e a porta será a porta interna do Mongo-DB (i.e., 27017).

Clique então em Add para criar a conexão com este banco de dados. A janela irá retornar para as propriedades do nó mongodb4 com a propriedade Connection já definida com a conexão recém criada. Pronto! assim já é possível interagir com o banco de dados.
A criação da conexão com cada banco de dados deve ser feita somente uma única vez. Caso adicione novos nós do mongodb4 em seu flow, basta apenas definir a propriedade Connection com a conexão já criada.
Mensagem básica de interação
Para utilizar o nó mongodb4, deve-se enviar uma msg na entrada de seu bloco. É obrigatório que cada mensagem contenha os seguintes campos:
msg.collection: String com o nome da coletânea (tabela) de trabalho.msg.operation: Tipo da operação que será realizada (find,findOne,insertOne,insertMany,updateOne,updateMany,deleteOne,deleteMany,aggregate).msg.payload: Conteúdo em função da operação que será realizada.
Exemplo de função que cria a estrutura básica de interação com o banco de dados:
msg = {}; // limpa completamente a mensagem
msg.collection = "nome-da-coletanea";
msg.operation = "tipo-de-operacao";
msg.payload = [ /* argumentos posicionais */ ];
return msg;
Msg.payload
O msg.payload é sempre um array posicional que mapeia diretamente para os argumentos do driver oficial do MongoDB para Node.js:
| Posição | Conteúdo típico |
|---|---|
[0] | Filtro / query |
[1] | Opções (e.g., sort, limit, projection, ...) ou documento de update |
[2] | Opções extras (ex.: { upsert: true }) |
Operadores de filtro mais comuns
| Operador | Significado | Exemplo |
|---|---|---|
$eq | Igual a | { valor: { $eq: 1 } } |
$ne | Diferente de | { status: { $ne: "erro" } } |
$gt | Maior que | { valor: { $gt: 0.5 } } |
$gte | Maior ou igual | { valor: { $gte: 0.5 } } |
$lt | Menor que | { valor: { $lt: 0.9 } } |
$lte | Menor ou igual | { valor: { $lte: 0.9 } } |
$in | Dentro de um array | { sensor: { $in: ["S1","S2"] } } |
$nin | Fora de um array | { sensor: { $nin: ["S3"] } } |
$and | E lógico | { $and: [ {a: 1}, {b: 2} ] } |
$or | Ou lógico | { $or: [ {a: 1}, {b: 2} ] } |
Opções de leitura mais comuns (2º elemento do array)
| Opção | Tipo | Descrição |
|---|---|---|
sort | object | Ordenação: 1 crescente, -1 decrescente. Ex.: { timestamp: -1 } |
limit | number | Número máximo de documentos retornados |
skip | number | Pula N documentos — útil para paginação |
projection | object | Seleciona campos: 1 inclui, 0 exclui |
maxTimeMS | number | Timeout máximo da operação em milissegundos |
Operações
A seguir seguem as definições e exemplos de utilização de cada uma das possíveis operações com o Mongo-DB.
insertOne
Insere uma única linha de dados na collection. Os dados devem ser organizados como um JSON, sendo a chave o nome da coluna e seu valor o dado a ser inserido naquela linha.
Sua sintaxe é:
msg.payload = [ {nome-coluna-1: <dado-a-ser-inserido>,
nome-coluna-2: <dado-a-ser-inserido>
} ];
Exemplo de código típico no armazenamento de dados de sensores com marcação de hora da leitura.
msg = {} // Limpa a mensagem por completo.
msg.collection = "sensores-temperatura";
msg.operation = "insertOne";
// Dados a serem inseridos no banco de dados
msg.payload = [{
sensor: "sensor-12", // uma string que descreve o nome do sensor que coletou o dado
timestamp: new Date().getTime(), // recebe um número com o timestamp atual
value: Math.random()*100 // cria um número aleatório entre 0 e 100 para mockar o dado do sensor
}];
return msg;
Ao injetar esta mensagem, colete a saída do nó mongodb4 em um nó Debug e note que é retornada uma mensagem indicando sucesso (ou não) na operação e o identificador da entrada de dados recém criada.

insertMany
Insere múltiplos documentos no banco de dados em uma única operação. Esta operação é mais eficiente do que chamar várias vezes insertOne, sendo preferida caso já haja uma grande quantidade de linhas disponíveis a serem salvas.
A sintaxe é:
msg.payload = [ arrayDeDocumentos, opções ];
O exemplo a seguir realiza a inserção de vários registros em uma coletânea de uma só vez.
msg = {} // Limpa a mensagem por completo.
msg.collection = "sensores-temperatura";
msg.operation = "insertMany";
msg.payload = [[{ sensor: "sensor-1", timestamp: new Date().getTime(), value: Math.random()*100 },
{ sensor: "sensor-2", timestamp: new Date().getTime(), value: Math.random()*100 },
{ sensor: "sensor-3", timestamp: new Date().getTime(), value: Math.random()*100 }
]]; // note que há um array de entradas dentro do array que, caso tivesse, traria os filtros e opções nos outros elementos.
return msg;
find
Retorna um array contendo todos os documentos que satisfazem o filtro enviado.
Sintaxe:
msg.payload = [ <filtro>, <opções> ];
Para retornar todos os registros de uma collection:
// Todos os documentos da collection
msg.collection = "sensores-temperatura";
msg.operation = "find";
msg.payload = [ {}, {} ];
return msg;
Para retornar uma entrada buscando-se por seu identificador:
msg.collection = "sensores-temperatura";
msg.operation = "findOne";
msg.payload = [
{ _id: new mongodb.ObjectId("6a15e0a54ea48a43ab568637") }
];
return msg;
No código de exemplo acima, perceba que o identificador é um objeto próprio da bibliteca do mongodb. Para utilizar a classe ObjectId, é necessário adicionar o module name com valor mongodb e importar como mongodb na aba Setup das propriedades do bloco de função.
Para retornar os últimos 10 registros:
msg.collection = "sensores-temperatura";
msg.operation = "find";
msg.payload = [
{},
{ sort: { timestamp: -1 }, // `-1` para ordenação decrescente
limit: 10 } //
];
return msg;
Para retornar com filtro de intervalo de valores + projeção de campos:
msg.collection = "sensores-temperatura";
msg.operation = "find";
msg.payload = [
{ value: { $gte: 60, $lte: 95 } },
{
sort: { timestamp: 1 }, // `1` para ordenação crescente
limit: 50, // quantidade máxima de retorno
projection: { sensor: 1, timestamp: 1, value: 1, _id: 0 } // retorna apenas `timestamp`, `value` e nome do sensor
}
];
return msg;
Em todos os casos, o retorno em msg.payload será um array com os registros que satisfazem os filtros definidos.
findOne
Retorna apenas o primeiro documento que satisfaz o filtro. Esta operação é mais eficiente que find quando deseja-se encontrar somente um registro.
Sua sintaxe é:
msg.payload = [ filtro, opções ];
Retorna o último elemento registrado na coletânea ordenado por timestamp.
msg.collection = "sensores-temperatura";
msg.operation = "findOne";
msg.payload = [{}, { sort: { timestamp: -1 } }
];
return msg;
Retorna o primeiro registro da coletânea que tenha a chave sensor igual a sensor-1.
msg.collection = "sensores-temperatura";
msg.operation = "findOne";
msg.payload = [{sensor: "sensor-1"}, {}
];
return msg;
Em qualquer caso, a variável msg.payload receberá um único objeto (ou null se nada for encontrado).
updateOne
Atualiza o primeiro registro que satisfaz o filtro.
Sintaxe:
msg.payload = [ filtro, update, opções ];
Os operadores de update mais comuns são:
| Operador | Ação |
|---|---|
$set | Define/substitui campos |
$unset | Remove campos |
$inc | Incrementa valor numérico |
$push | Adiciona item a um array |
$pull | Remove item de um array |
Alterando o nome de sensor de uma entrada específica utilizando seu identificador:
msg.collection = "sensores-temperatura";
msg.operation = "updateOne";
msg.payload = [
{ _id: new mongodb.ObjectId("6a15e0a54ea48a43ab568635") },
{ $set: { sensor: "sensor-24" } }
];
return msg;
Remove o campo value do primeiro registro que tenha a chave sensor igual a sensor-1:
msg.collection = "sensores-temperatura";
msg.operation = "updateOne";
msg.payload = [
{ sensor: "sensor-1" },
{ $unset: { value: "" } }
];
return msg;
updateMany
Atualiza todos os registros que satisfazem o filtro. Possui a mesma sintaxe do
updateOne, mas afeta múltiplos documentos ao mesmo tempo.
Altera para sensor-01 o valor da chave sensor em todas as entradas onde esta tiver o valor sensor-1.
msg.collection = "sensores-temperatura";
msg.operation = "updateMany";
msg.payload = [
{ sensor: "sensor-1" },
{ $set: { sensor: "sensor-01" } }
];
return msg;
Adiciona a chave planta com o valor Ouro Branco a todos os registros da coletânea.
msg.collection = "sensores-temperatura";
msg.operation = "updateMany";
msg.payload = [
{ },
{ $set: { planta: "Ouro Branco" } }
];
return msg;
deleteOne
Remove o primeiro documento que satisfaz o filtro.
Sua sintaxe é somente:
msg.payload = [ filtro ];
Deleta o primeiro registro da coletânea ordenado por timestamp:
msg.collection = "sensores-temperatura";
msg.operation = "deleteOne";
msg.payload = [
{ },
{ sort: {timestamp: 1} }
];
return msg;
Deleta um registro específico por seu ID:
msg.collection = "sensores-temperatura";
msg.operation = "deleteOne";
msg.payload = [{ _id: new mongodb.ObjectId("6a15e0a54ea48a43ab568632") },
{ }];
return msg;
deleteMany
Remove todos os documentos que satisfazem o filtro.
Caso deixe o filtro {} vazio, toda a coletânea será deletada.
Sintaxe da mensagem:
msg.payload = [ filtro ];
Deleta todos os registros mais antigos que 10 minutos
msg.collection = "sensores-temperatura";
msg.operation = "deleteMany";
let dezMinutosAtras = new Date().getTime() - ( 10 * 60 * 1000);
msg.payload = [
{ timestamp: { $lt: dezMinutosAtras } }
];
return msg;
Deleta todos os registros do sensor 2:
msg.collection = "sensores-temperatura";
msg.operation = "deleteMany";
msg.payload = [
{ sensor: "sensor-2" }
];
return msg;
aggregate
Executa um pipeline de agregação, permitindo encadear estágios de filtragen, transformação, agrupamento e cálculo sobre os dados. É a operação mais poderosa do MongoDB.
Sua sintaxe é:
msg.payload = [[ estágio1, estágio2, ... ]]; // array dentro de array
Os estágios mais comuns são:
| Estágio | Função |
|---|---|
$match | Filtra documentos (equivalente ao find) |
$sort | Ordena documentos |
$limit | Limita quantidade de documentos |
$skip | Pula N documentos |
$group | Agrupa e calcula métricas |
$project | Seleciona/transforma campos |
$count | Conta documentos |
$unwind | Desdobra arrays em documentos separados |
Os acumuladores para o estágio $group são:
| Acumulador | Função |
|---|---|
$avg | Média |
$sum | Soma (ou contagem com $sum: 1) |
$max | Valor máximo |
$min | Valor mínimo |
$first | Primeiro valor do grupo |
$last | Último valor do grupo |
Calcula a média, máximo e mínimo da chave value para os 30 últimos registros da coletânea:
msg.collection = "sensores-temperatura";
msg.operation = "aggregate";
msg.payload = [[
{ $sort: { timestamp: -1 } },
{ $limit: 20 },
{
$group: {
_id: null,
media: { $avg: "$value" },
maximo: { $max: "$value" },
minimo: { $min: "$value" },
}
}
]];
return msg;
Calcula a média de leitura agrupado por sensor:
msg.collection = "sensores-temperatura";
msg.operation = "aggregate";
msg.payload = [[
{
$group: {
_id: "$sensor", // cria um grupo onde o _id é o valor da chave sensor dos dados de entrada
media: { $avg: "$value" },
count: { $sum: 1 }
}
},
{ $sort: { _id: 1 } }
]];
return msg;
Calcula a média por hora do dia:
msg.collection = "sensores-temperatura";
msg.operation = "aggregate";
msg.payload = [[
{
$group: {
_id: {
$dateToString: {
format: "%Y-%m-%dT%H:00", // note que o marcador vai até horas, o resto está zerado
date: { $toDate: "$timestamp" }
}
},
media: { $avg: "$value" },
count: { $sum: 1 }
}
},
{ $sort: { _id: 1 } }
]];
return msg;
Calcula a média e momento da última leitura dos últimos 50 registros de sensor-1 com valor maior que 70 e retorna somente as chaves calculadas media e ultimo, sem _id.
msg.collection = "sensores-temperatura";
msg.operation = "aggregate";
msg.payload = [[
{ $match: { sensor: "sensor-1", value: { $gt: 70 } } },
{ $sort: { timestamp: -1 } },
{ $limit: 50 },
{
$group: {
_id: null,
media: { $avg: "$value" },
ultimo: { $first: "$timestamp" } // note que ordenação por timestamp está descrescente
}
},
{ $project: { _id: 0, media: 1, ultimo: 1 } }
]];
return msg;
Continuando o Problema da aula anterior (Link), considere que a aplicação colete os dados da estação a cada segundo.
Implemente nesta aplicação o registro em banco de dados de uma coletânea com a seguinte estrutura:
| timestamp | longitude | latitude | distancia-percorrida |
|---|---|---|---|
| xxx | xxx | xxx | 0 |
Os registros deve ser inseridos no banco de dados na mesma taxa de aquisição dos dados da ISS.
Além de tudo o que já foi implementado neste projeto até aqui, o seu programa deve ser capaz de:
- Recuperar e printar no debug todos os registros do banco de dados.
- Recuperar e printar no debug os últimos 30 registros, somente de
timestamp,longitudeelatitude. - Calcular a média dos campos
longitudeelatitudedos últimos 60 registros. - Deletar automaticamente todas as leituras mais antigas que 3 minutos.