Plataforma de aquisição industrial OT/IT para borda: coleta, valida, torna durável e expõe dados de chão de fábrica — sem perder nada, sem nuvem e sem depender de que o banco de consulta esteja de pé.
O SynkaCore é a ponte confiável entre o chão de fábrica (OT) e os sistemas de gestão (IT). Origens de dado — equipamento embarcado, ou o nó em software que acompanha o projeto — entregam remessas ao gateway por um contrato de fio versionado. O gateway valida, torna durável no disco local e confirma. Um projetor assíncrono alimenta o banco de séries temporais que os dashboards consultam.
flowchart LR
subgraph OT["🏭 Chão de fábrica"]
NO["synkacore-no<br/>origem do dado"]
end
subgraph GW["⚙️ synkacore-gateway"]
ING["ingestão"]
DIA[("diário SQLite<br/>registro autoritativo")]
PRO["projetor"]
APR["apresentação"]
end
subgraph IT["📊 Escritório"]
GRA["Grafana"]
OPE["operador"]
end
TS[("TimescaleDB<br/>modelo de leitura")]
NO -->|"remessa protobuf"| ING
ING -->|"grava ANTES de confirmar"| DIA
ING -.->|"confirma até a sequência N"| NO
DIA --> PRO --> TS
TS --> GRA
APR --> OPE
DIA --> APR
A remessa só é confirmada depois de estar durável no disco. Se a gravação falha, o nó não recebe confirmação e retransmite. Se o gateway cai no meio, o nó retransmite. Se o TimescaleDB cai, a aquisição não percebe — o dado continua entrando no diário e a projeção retoma sozinha quando o banco volta.
Isso não é um mecanismo de emergência; é o único caminho que existe.
Na V1.x, zero perda dependia de um caminho de exceção — um buffer que só era exercitado durante a falha. A auditoria da V1.2 encontrou esse caminho desligado por um erro de tipo no construtor: o buffer estava registrado e nunca era usado, e o dado se perdia como antes de ele existir. Um caminho que quase nunca roda é um caminho que não funciona. Ver V2.0.
Teste de ponta a ponta com o gateway derrubado por 14 segundos enquanto o nó continuava amostrando a 2 Hz:
registros gravados : 123
sequências faltando : NENHUMA
duplicatas : NENHUMA
lacunas de amostragem: NENHUMA
Capacidade sob carga, contra disco real — a pergunta que estava aberta desde a V1.x:
| Origens | Envelopes/s | p50 | p99 |
|---|---|---|---|
| 10 | 950 | 25 ms | 51 ms |
| 200 | 18.999 | 494 ms | 945 ms |
Teto de ~20.000 envelopes/s neste hardware. Acima disso nada quebra: a latência sobe e as origens bufferizam. Detalhes e o gargalo medido em V2.3.
O gateway não sabe o que existe do outro lado do fio — só o contrato. Hoje há duas implementações de nó, e o gateway não distingue as duas:
| Nó | O que é |
|---|---|
internal/no (Go) |
Simula uma câmara de vácuo de curtimento — temperatura, pressão, estado de máquina e contagem, em ciclo de 3 minutos. Roda sem hardware nenhum. |
no-micropython/ |
ESP32 com DHT11 real, medindo temperatura e umidade do ar. |
Serialização, lote, contrapressão, retransmissão, idempotência e ancoragem de tempo são exercitados de verdade nos dois. Trocar simulação por hardware deixa de ser uma integração: passa a ser uma troca de quem gera os números.
O codificador protobuf do nó MicroPython é gerado do .proto, e
internal/contrato/fidelidade compara byte a byte o
que o Python e o Go produzem para a mesma mensagem. Sem esse teste o gerador seria uma
esperança: protobuf não carrega nomes de campo, e um número de tag trocado vira outro
campo em silêncio.
O firmware de produção será C++ restrito sobre ESP-IDF — decisão registrada em
docs/NO-EMBARCADO.md, com o gatilho e as travas. Não está ativa.
A aquisição funciona completa e o dado fica durável. Só falta o gráfico.
make compilar
./bin/synkacore-gateway # terminal 1
./bin/synkacore-no # terminal 2
curl http://127.0.0.1:8080/saude
curl 'http://127.0.0.1:8080/leituras?limite=10'Sem configuração da instalação, o gateway grava canal 0 = 24,7 — verdade que não
responde nada. Com ela, cada leitura carrega o ponto de medição, a grandeza e a
unidade, e o /comissionamento denuncia canal trocado no painel.
Não escreva o arquivo do zero — o gateway o gera a partir do que as origens já declararam, e só sobra nomear os pontos:
./bin/synkacore-gateway &
./bin/synkacore-no &
curl http://127.0.0.1:8080/comissionamento/esboco > configuracao/instalacao.yaml
# edite: substitua cada AJUSTAR-... pelo nome real do ponto de medição
./bin/synkacore-gateway -instalacao configuracao/instalacao.yaml
curl http://127.0.0.1:8080/comissionamentomake infra # TimescaleDB + Grafana
make gateway-completo # terminal 1
make no # terminal 2Grafana em http://localhost:3000 (admin/admin), com a fonte de dados já provisionada.
- Go 1.26+ — só isso para compilar e rodar
- Docker ou Podman, apenas para o estágio de consulta
protoc+protoc-gen-go, apenas para regerar o contrato
Dois servidores, em interfaces separadas, porque o gateway fica entre duas redes.
Ingresso — lado de chão de fábrica (127.0.0.1:8443), com mTLS
| Endpoint | Descrição |
|---|---|
POST /ingestao |
Recebe uma remessa protobuf; devolve a confirmação com a faixa durável |
Com credencial configurada, o certificado de cliente é exigido e a identidade que
a remessa reivindica é confrontada com a que o certificado prova. Divergência é
recusada com 403. O gateway também serve tempo por UDP, para que origens sem
relógio de bateria consigam validar o certificado dele.
Apresentação — lado de escritório (127.0.0.1:8080), somente leitura
| Endpoint | Descrição |
|---|---|
GET /saude |
Estado do diário e da projeção, verificados de verdade |
GET /leituras?limite=N |
Registros recentes do diário, já decodificados |
GET /contrato |
Tipos de conteúdo que este gateway reconhece |
GET /comissionamento |
Desacordos entre o que as origens declaram e o que a instalação configura |
GET /comissionamento/esboco |
YAML de configuração gerado a partir do que as origens já declararam |
O /saude reporta os dois estágios separados, e a distinção decide se alguém é
acordado:
{"journal":"available","projection":"degraded","projection_since":"...","checked_at":"..."}journal falhando significa que o sistema está perdendo a capacidade de aceitar dado.
projection falhando significa que o dado está salvo e os dashboards estão atrasados.
Hexagonal enxuto, aplicado onde ele paga por si. A regra de dependência é única:
flowchart TD
ADA["adaptador<br/>HTTP, SQLite, TimescaleDB, codec"] --> APL["aplicação<br/>ingestão, projeção"]
APL --> DOM["domínio<br/>envelope, classes, identidades, tempo"]
ADA --> PLA["plataforma<br/>falha, relógio, resiliência"]
APL --> PLA
O domínio não importa HTTP, banco, arquivo nem relógio. Não é purismo: é o que permite testar a regra de tempo e a de idempotência sem subir Postgres.
Onde deliberadamente não abstraímos, porque abstração sem segunda implementação é só indireção:
| Não abstraído | Motivo |
|---|---|
| Diário SQLite | É a definição de durabilidade do sistema, não uma escolha. Nunca haverá um segundo. O teste usa arquivo temporário, que é mais fiel que um dublê. |
| Logging | log/slog direto. Envolver o logger só produziria uma API pior. |
| Relógio | Injetado como interface de dois métodos, e os dois existem por uma razão que custou um achado bloqueante — ver abaixo. |
Identificadores em português sem acento, porque o vocabulário do domínio já é português
em toda a documentação. Inglês fica onde a linguagem impõe (main, Error, String), onde
o compilador reconhece a estrutura (internal/ não é escolha: o Go impõe que pacotes
ali não sejam importáveis de fora) e nos identificadores que saem do processo — rótulo
de métrica e coluna de banco são consumidos por Prometheus, Grafana e SQL.
Não existe utils, helpers, common ou models: são gavetas onde código duplicado se
esconde, porque nenhum desses nomes diz o que não pertence ali.
Documentar "não duplique" não sustenta nada. Cada item abaixo é uma trava real no código.
| Invariante | Trava |
|---|---|
| Identidade provada, não afirmada | O id_do_dispositivo da remessa é confrontado com o nome comum do certificado que o TLS validou. Sem isso, um dispositivo com credencial legítima pode gravar dado sob a identidade do vizinho — e o resultado é plausível, indetectável depois. |
| Um ponto de validação por conceito | NovoEnvelope é o único construtor de mensagem. Campos não exportados ⇒ possuir um Envelope é prova de que ele é válido. Não existe "validar de novo por segurança". |
| Interface nomeada em vez de asserção anônima | TestConteudoEnderecadoCasaComOContrato lê o descritor e exige que conteúdo com campo endereco implemente ConteudoEnderecado, e vice-versa. Nasceu de um defeito real: uma asserção para interface anônima que nunca casava, deixando o enriquecimento inteiro como código morto. |
| Um catálogo que recusa duplicata na inicialização | NovoCatalogoDeConteudo rejeita tipo repetido. Dois arquivos definindo o mesmo tipo derrubam o gateway no boot, não em produção. |
| O catálogo cobre o contrato | TestTodoConteudoDoContratoTemDefinicao lê o descritor do protobuf por reflexão. Acrescentar uma mensagem ao contrato sem ensinar o gateway a interpretá-la reprova o build. |
| Exaustividade sobre enum | Os switch sobre ClasseDeDado, EstadoDeMaquina e falha.Categoria não têm default, e o linter roda com default-signifies-exhaustive: false. |
| Um projetor genérico, não um por tipo | Cada conteúdo declara o que contribui ao modelo de leitura; o projetor não conhece tipo nenhum. |
| Uma taxonomia de erro, um mapeador por adaptador | falha.Categoria é o vocabulário único; statusDe é o único condicional sobre erro no adaptador HTTP. |
| Nenhum código duplicado | dupl, goconst, gocognit e nestif no golangci-lint, verificados a cada make verificar. |
Em Go, time.Time carrega uma leitura monotônica — e qualquer .UTC() a descarta. A
partir dali, subtrair dois instantes usa o relógio de parede, que anda para trás quando o
NTP corrige. Numa trilha que precisa provar quando algo aconteceu, isso deixa de ser
corretude e vira conformidade.
plataforma/relogio mantém as duas leituras separadas e comparáveis entre si: um acerto
de hora move só a parede, então a divergência vira mensurável. O relogio.Falso tem
Avancar (move as duas juntas, que é o tempo normal) e DarDegrau (move só a parede) —
um degrau real é impossível de reproduzir em CI; aqui é uma chamada de método.
O teste de ponta a ponta encontrou o nó parando de amostrar por 15 segundos durante uma
queda de 12 segundos do gateway: amostragem e despacho dividiam um select, e o recuo do
despacho dormia bloqueando o temporizador.
A distinção que isso revela é a que importa. O buffer protege contra perder dado no caminho — e para isso funcionava. Mas dado que nunca foi medido não está em buffer nenhum, e nenhuma retransmissão o traz de volta.
Hoje são dois laços independentes, e um teste de regressão mede o espaçamento entre tempos ligados para garantir que continue assim.
SynkaCore/
├── contrato/proto/ # o .proto — fonte única de verdade do fio
├── cmd/ # raízes de composição: o único lugar com wiring
├── internal/
│ ├── contrato/v1/ # gerado do .proto, versionado de propósito
│ ├── dominio/ # regras. Sem I/O, sem framework, sem relógio.
│ ├── aplicacao/ # ingestão e projeção
│ ├── adaptador/ # entrada, saída, codec
│ ├── no/ # a origem do dado e a simulação de processo
│ └── plataforma/ # falha, relógio, resiliência, identificador
├── migracoes/ # esquema do modelo de leitura
├── implantacao/grafana/ # provisionamento como código
├── legado/java-v1.2/ # a implementação V1.x, preservada
└── docs/
make verificar # formatação, vet, testes com -race, linter, contrato em dia
make compilar # binários estáticos, sem cgo
make contrato # regera o Go a partir do .proto
make no-micropython # regera o codificador do nó a partir do .proto
make cobertura # relatório de cobertura em HTML
make medir # benchmarks do diário contra disco real
make carga # gerador de carga contra um gateway no armake verificar é o portão completo — qualquer falha derruba o build. Disciplina imposta
pela ferramenta, não pela boa vontade.
O binário é verdadeiramente estático, sem dependência de runtime. É o argumento que decidiu a linguagem: implantar é copiar um arquivo e uma unidade systemd, e reverter é manter o arquivo anterior. Numa planta sem internet, atualizada pelo notebook de um técnico, isso vale mais que qualquer vantagem de velocidade bruta.
- V2.0 — a reescrita: por que foi antecipada, o que mudou, o que foi encontrado no caminho.
- V2.2 — configuração da instalação: como o dado ganha significado, e a rede de proteção que denuncia canal trocado no painel.
- V2.1 — mTLS com CA interna, identidade autenticada contra reivindicada, e o servidor de tempo que torna a validação possível numa origem sem relógio.
- V2.3 — capacidade medida, onde está o gargalo, e os painéis do Grafana como código.
- Visão geral visual — diagramas de fluxo e cenários de queda.
- Trade-offs — decisões técnicas e seus custos.
- Qualidade — os portões do build.
- Propriedade intelectual — anterioridade, manifesto criptográfico e o caminho do registro no INPI.
- Nó embarcado — por que C++ restrito e não C, o subconjunto, e as travas que o tornam aceitável.
- Histórico V1.x — V1.0 · V1.1 · V1.2,
com o código em
legado/java-v1.2/.
| Versão | Status | Entrega |
|---|---|---|
| V1.0 | ✅ Concluída | Fundação em Java: coleta, persistência, API REST |
| V1.1 | ✅ Concluída | Resiliência, observabilidade, health check real |
| V1.2 | ✅ Concluída | Buffer local SQLite; auditoria estrutural |
| V1.3–V1.5 | ❌ Canceladas | Exigiam hardware físico — ver V2.0 |
| V2.0 | 🚧 Em desenvolvimento | Reescrita em Go, contrato de fio, durabilidade estrutural, nó em software |
| V2.2 | 🚧 Em desenvolvimento | Configuração da instalação: canal → ponto de medição, catálogo de motivos, comissionamento |
| V2.1 | 🚧 Em desenvolvimento | mTLS com CA interna, identidade autenticada vs. reivindicada, servidor de tempo |
| V2.3 | 🚧 Em desenvolvimento | Capacidade medida, gargalo identificado, painéis do Grafana como código |