Profissionalização: CI que roda, ingestão em lote real, OPC-UA plugado e vitrine no padrão - #4
Conversation
- Adapters Modbus/OPC-UA/Simulator sob a ABC TelemetrySource - UniqueConstraint(sensor, timestamp) substitui o indice nao-unico - simulate_telemetry substitui o comando duplicado telemetry_simulate - +184 linhas de teste (72 testes verdes) - Remove .last-handoff.json do versionamento (artefato de processo interno)
CI: o workflow disparava em `main` mas o branch default e `master` — os jobs de lint e de teste com Postgres nunca executaram. Consolida os dois workflows em um, no branch certo, com check --deploy informativo. Ingestao: `evaluate_and_alert()` fazia um INSERT por leitura e o `bulk_create()` seguinte virava no-op silencioso sobre objetos ja com pk. Pior, reprocessar a mesma janela estourava IntegrityError na UniqueConstraint. Extrai `raise_alert()` de `evaluate_and_alert()`; o loop agora avalia em memoria, persiste o lote uma vez e alerta depois. Contador renomeado para `total_processed` — `ignore_conflicts` nao devolve pks confiaveis. Docs: `replay-idempotency.md` afirmava "deduplicacao nao existe" com a UniqueConstraint ja no modelo desde a migration 0004. Reescrito para descrever a garantia real e seus limites. Mesma correcao em `data-contract.md`. Teste negativo: `test_replay_same_window_is_idempotent` verificado falhando com IntegrityError sem o fix. 73 testes verdes.
- LICENSE MIT (o repo era publico sem licenca — todos os direitos reservados) - README reescrito em pt-BR nas 10 secoes do padrao: header centralizado com badges reais (incluindo CI, agora que ele roda), diagrama Mermaid nativo no lugar do bloco de texto, tabela de endpoints, estrutura do projeto e fronteiras de escopo explicitas - Wiki alinhada: pt-BR, Home com Sumario de Documentacao, pagina nova Idempotencia-e-Replay, mencao a OPC-UA que faltava nos adapters - Remove o caminho local absoluto vazado 4x no manual de validacao Comandos de setup executados de verdade antes de documentar: migrate, ingest, /api/summary/, /api/health/sources/ e dashboard (HTTP 200).
Validando `docker compose up --build` numa maquina com Postgres e collector
OTLP locais, o stack nao subia: colisao em 5432, depois em 4317. Com o Jaeger
fora do ar, ele nunca entrava na rede do compose e todo comando `manage.py`
dentro do container inundava a saida com erros de export de span.
Portas publicadas agora usam `${VAR:-default}` — comportamento identico por
padrao, sobrescrevivel sem editar o compose. Documentado no README e no
.env.example.
Verificado end-to-end em portas alternativas: dashboard HTTP 200, ingestao
limpa, /api/summary/ com dados e Jaeger reportando o servico `labtelemetry`.
O OpcUaAdapter existia, tinha testes e servidor de teste, mas `ingest_telemetry` tinha `choices=["modbus", "simulator"]` — era inalcancavel na pratica, enquanto README e wiki anunciavam OPC-UA como fonte. O que impedia plugar direto: o adapter emitia `sensor_id=idx` (indice posicional do node), e `_sample_to_reading` trata esse campo como chave primaria de TelemetrySensor. Node 0 vira "sensor 0", que ou nao existe ou e o sensor errado — dado plausivel e silenciosamente incorreto. - `OpcUaAdapter` aceita `sensor_ids` paralelo a `node_ids`, validado 1:1. Sem ele, mantem o indice posicional (compativel com os testes existentes). - `--source opcua` com `--opcua-url`, `--opcua-timeout` e `--opcua-node` repetivel no formato `NODE_ID:SENSOR_ID`. Split no ultimo `:`, ja que node ids contem `=` e `;`. Sem mapeamento, o comando recusa iniciar. - `_sample_to_reading` avisa quando o parametro da fonte diverge do sensor, sem descartar a leitura. Vale para as tres fontes. - `/api/health/sources/` passa a incluir opcua — README e wiki afirmavam cobertura das tres fontes, o endpoint so devolvia duas. Verificado contra servidor OPC-UA real, fora dos testes: recusa sem mapeamento, grava PH=7.0 no sensor 1 e TOC=5.0 no sensor 5 com lineage, e o guard dispara ao apontar o node de pH para o sensor de TOC. 79 testes verdes (6 novos), ruff limpo.
Adendo: OPC-UA plugado (
|
| Cenário | Resultado |
|---|---|
Sem --opcua-node |
Recusa iniciar com mensagem acionável |
Spec malformado (ns=2;i=101) |
Recusa, aponta o formato esperado |
| Mapeamento correto | PH=7.0 → sensor 1, TOC=5.0 → sensor 5, com lineage opcua:opc.tcp://... |
| Node de pH → sensor de TOC | Sensor 5 e TOC, mas a fonte enviou PH — verifique o mapeamento |
79 testes verdes (6 novos, incluindo integração contra servidor OPC-UA real, rodada 3× para checar flakiness), ruff limpo, CI verde nos dois jobs.
…scala Mesma classe de bug corrigida no OPC-UA: o adapter emitia `sensor_id` igual ao indice do registrador (0/1/2 via PARAMETER_MAP), e `_sample_to_reading` trata esse campo como chave primaria de TelemetrySensor. Contra um CLP real, o registrador 0 tentaria gravar no "sensor 0", que nunca existe — o autoincrement do Django comeca em 1. Junto vinha um segundo erro, esse silencioso: holding register e uint16 e o valor era usado cru. Um pH de 7.40 e publicado pelo CLP como 740, entrava no banco como pH 740 e caia em OUT_OF_BOUNDS — errado de um jeito que so aparece no grafico. A escala e o mesmo tipo de ajuste que `calibration_factor` faz um nivel acima: uma corrige o protocolo, a outra o sensor. - `RegisterSpec(address, sensor_id, scale)` e o parametro `registers` no ModbusTCPAdapter. Sem ele, cai no caminho legado (bloco 0-2), preservado so para inspecao rapida de um CLP e documentado como tal. - `--modbus-register "ADDRESS:SENSOR_ID[:SCALE]"`, repetivel. Escala omitida vale 1.0. Sem mapeamento, o comando recusa iniciar. - Le um registrador por vez em vez de um bloco: enderecos esparsos tornam a leitura em bloco invalida em muitos CLPs. Ceiling e upgrade anotados no codigo. - Registrador que falha e pulado sem derrubar os demais do ciclo. 85 testes verdes (6 novos), ruff limpo.
Adendo 2: Modbus corrigido (
|
| Leitura bruta | Escala | Valor gravado | Dentro de 6.0–8.5? |
|---|---|---|---|
740 |
— (bug) | pH 740.0 | ❌ |
740 |
0.01 |
pH 7.40 | ✅ |
Esse é o pior dos dois: não falha, só produz série temporal errada. A escala é o mesmo tipo de ajuste que calibration_factor faz um nível acima — uma corrige o protocolo, a outra o sensor.
O que foi feito
RegisterSpec(address, sensor_id, scale)e o parâmetroregistersnoModbusTCPAdapter. Sem ele, cai no caminho legado (bloco 0–2), preservado só para inspeção rápida de um CLP e documentado como tal.--modbus-register "ADDRESS:SENSOR_ID[:SCALE]", repetível. Escala omitida vale1.0. Sem mapeamento, o comando recusa iniciar — simétrico ao--opcua-node.- Uma leitura por registrador em vez de um bloco
0..N: endereços esparsos tornam a leitura em bloco inválida em muitos CLPs (registrador não mapeado no meio do span). O ceiling e o caminho de upgrade estão anotados no código. - Registrador que falha é pulado sem derrubar os demais do ciclo.
85 testes verdes (6 novos), ruff limpo, CI verde nos dois jobs.
Com isso as três fontes têm o mesmo contrato: mapeamento explícito, recusa de iniciar sem ele, e guard de coerência de parâmetro na ingestão.
Contexto
O repositório já tinha substância técnica — adapters Modbus/OPC-UA, regras de qualidade, dashboard HTMX, OTel opcional. O problema era que o que estava público não correspondia ao que o código fazia.
O que estava quebrado
ci.ymldisparava emmain; o branch default émaster. Os jobs de lint e de teste com Postgres nunca executaram uma única vez.evaluate_and_alert()fazia um INSERT por leitura; obulk_create()seguinte era no-op silencioso sobre objetos já com pk. Reprocessar a mesma janela estouravaIntegrityError.replay-idempotency.mdafirmava "deduplicação não existe" com aUniqueConstraint(sensor, timestamp)no modelo desde a migration 0004.sources/+ 184 linhas de teste só existiam no disco local.manual_validacao_ponta_a_ponta.mdexpunha o caminho absoluto do disco do autor, 4×.O que mudou
CI (
3c85544,6a4d07f) — dois workflows sobrepostos consolidados em um, no branch certo: ruff + Postgres 16 +manage.py check+makemigrations --check+check --deployinformativo.Ingestão (
6a4d07f) —raise_alert()extraído deevaluate_and_alert(). O loop agora avalia em memória (evaluate_reading()é pura), persiste o lote uma vez e alerta depois.ignore_conflicts=Truefinalmente faz o que promete.Teste negativo —
test_replay_same_window_is_idempotentfoi verificado falhando sem o fix, comIntegrityError: UNIQUE constraint failed. Guardrail só existe depois de observar o bloqueio.Vitrine (
30373fb) — LICENSE MIT; README em pt-BR nas 10 seções do padrão, com diagrama Mermaid nativo e badge de CI (agora honesto); wiki alinhada, incluindo página nova de Idempotência e a menção a OPC-UA que faltava.Compose (
eff8155) — validandodocker compose up --buildnuma máquina com Postgres e collector OTLP locais, o stack não subia (colisão em 5432, depois 4317), e com o Jaeger fora do ar todomanage.pyno container inundava a saída com erros de export. Portas agora sobrescrevíveis por variável.Verificação
/api/summary/com dados, Jaeger reportando o serviçolabtelemetryFora de escopo (registrado, não feito)
OpcUaAdapterexiste e tem testes, masingest_telemetrytemchoices=["modbus", "simulator"]— inalcançável na prática