Ingestão de observações de cobertura (link_observations)¶
Runbook para parceiros e operações: como submeter medições reais de RSSI ao TTP para alimentar o re-treinamento do modelo Sionna.
A tabela link_observations é a fonte de verdade para o
re-treinamento supervisionado. Hoje o modelo em produção
(sionna_20260513_1727, schema v2) foi treinado 100% com dados
sintéticos — qualquer drive-test real entra com peso 10× maior nas
métricas de drift e é incorporado no próximo retrain mensal.
1. Provisionamento¶
- Tier mínimo:
PRO(PRO/BUSINESS/ENTERPRISE/ULTRA). Endpoints de ingestão fazemrequire_tier(...)— chaves FREE/STARTER recebem403 Forbidden. - Chave de API: emitida via console em
https://app.telecomtowerpower.com.br/keysou, para parceiros enterprise, viaPOST /admin/keys(uso interno). Prefixottp_identifica chaves de tenant;ttpci_é reservado para automação interna. - Onboarding mínimo do parceiro:
- hostname-pinning de
api.telecomtowerpower.com.br - rate-limit por padrão: 600 req/min (tier PRO) — solicitar
elevação em
partners@telecomtowerpower.com.brse for ingerir > 100k linhas/dia OBSERVATION_REQUIRE_SIGNATUREestá off em produção; opt-in de assinatura é recomendado (peso 1.0 vs 0.1).
2. Endpoints¶
| Método | Caminho | Uso |
|---|---|---|
POST |
/coverage/observations |
1 linha JSON (CoverageObservationInput). |
POST |
/coverage/observations/batch |
até 10.000 linhas/request, com dedup. |
POST |
/coverage/observations/drivetest |
upload de CSV (TEMS / G-NetTrack / QualiPoc / Anatel). |
GET |
/coverage/observations/stats |
counts de link_observations e cell_signal_samples. |
Schema de uma observação¶
Todos os campos são obrigatórios para source que começa com
drivetest_ — defaults só valem para source=api/sintéticos.
{
"tower_id": "ANATEL-12345",
"tx_lat": -23.5505,
"tx_lon": -46.6333,
"tx_height_m": 32.0,
"tx_power_dbm": 43.0,
"tx_gain_dbi": 17.0,
"rx_lat": -23.5612,
"rx_lon": -46.6401,
"rx_height_m": 1.5,
"rx_gain_dbi": 0.0,
"cable_loss_db": 1.8,
"freq_hz": 2600000000,
"observed_dbm": -92.3,
"source": "drivetest_tems",
"ts": 1747171800.0
}
Faixas validadas (Pydantic):
| Campo | Range |
|---|---|
tx_lat, rx_lat |
[-90, 90] |
tx_lon, rx_lon |
[-180, 180] |
tx_height_m, rx_height_m |
(0, 500] |
tx_power_dbm |
[0, 80] |
cable_loss_db |
[0, 20] |
freq_hz |
(1e6, 100e9] |
observed_dbm |
[-150, 30] |
source |
string ≤ 32 chars; convenção drivetest_<tool> |
3. Assinatura HMAC (opt-in mas recomendado)¶
O endpoint /batch verifica X-Observation-Signature sobre o corpo
cru da request. Chaves não assinadas entram com peso 0.1; assinadas
com peso 1.0.
Segredo: derivado da própria API key —
secret = sha256("obs-sign:" + api_key).
Não há key-rotation separado.
Assinatura sem timestamp (mais simples):
PAYLOAD='{"observations":[…]}'
SECRET=$(printf '%s' "obs-sign:${TTP_API_KEY}" | openssl dgst -sha256 -binary | xxd -p -c 64)
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$(printf '%s' obs-sign:${TTP_API_KEY} | openssl dgst -sha256 -binary)" -hex | awk '{print $2}')
Em Python (recomendado, evita problemas de binding HMAC do openssl CLI):
import hashlib, hmac, json, requests
api_key = os.environ["TTP_API_KEY"]
payload = {"observations": [obs1, obs2, ...]}
raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
secret = hashlib.sha256(("obs-sign:" + api_key).encode("utf-8")).digest()
sig = hmac.new(secret, raw, hashlib.sha256).hexdigest()
r = requests.post(
"https://api.telecomtowerpower.com.br/coverage/observations/batch",
data=raw,
headers={
"X-API-Key": api_key,
"Content-Type": "application/json",
"X-Observation-Signature": f"sha256={sig}",
},
timeout=30,
)
r.raise_for_status()
Com timestamp (recomendado em pipelines de produção;
janela de skew = 300 s, env OBSERVATION_SIGNATURE_MAX_SKEW_SEC):
ts = int(time.time())
payload_to_sign = f"{ts}.".encode("utf-8") + raw
sig = hmac.new(secret, payload_to_sign, hashlib.sha256).hexdigest()
headers = {
"X-API-Key": api_key,
"Content-Type": "application/json",
"X-Observation-Signature": f"sha256={sig}",
"X-Observation-Signature-Ts": str(ts),
}
4. Dedup¶
Rows são deduplicadas por chave
(tower_id, round(rx_lat,5), round(rx_lon,5), round(freq_hz),
int(ts // window)), onde window = OBSERVATION_DEDUP_WINDOW_SEC
(default 60 s). Dedup acontece tanto dentro do batch quanto
contra o intervalo [min_ts - window, max_ts + window] no store.
Resposta inclui:
{
"ingested": 2451,
"deduped_intra_batch": 12,
"deduped_existing": 38,
"submitted": 2501,
"signature_status": "verified"
}
5. Upload de CSV drive-test¶
POST /coverage/observations/drivetest (multipart/form-data):
curl -X POST https://api.telecomtowerpower.com.br/coverage/observations/drivetest \
-H "X-API-Key: $TTP_API_KEY" \
-F "csv_file=@drive_test_2026-05-13.csv" \
-F "tower_id=ANATEL-12345" \
-F "device=tems"
Aliases de coluna auto-detectados (lat/lon, signal_dbm/rsrp/rscp/rxlev,
freq_hz ou band_mhz). Mínimo: lat, lon, e uma coluna de RSSI.
TX context vem do tower_id se fornecido; senão exige
tx_lat/tx_lon/tx_height_m/tx_power_dbm como form fields.
6. Verificação¶
ADMIN_KEY=$(aws ssm get-parameter --region sa-east-1 \
--name /telecom-tower-power/ADMIN_API_KEYS --with-decryption \
--query 'Parameter.Value' --output text | cut -d, -f1)
curl -sS -H "X-API-Key: $ADMIN_KEY" \
https://api.telecomtowerpower.com.br/coverage/observations/stats
# {"link_observations": 0, "cell_signal_samples": 0}
7. Cadência de re-treinamento¶
O re-treinamento automático vive no workflow
residual-drift-retrain.yml (LightGBM residual, artefatos em MinIO):
- Cron diário +
workflow_dispatch; re-treina quando o monitor de drift residual dispara ou quando forçado manualmente. - Consome as observações reais ingeridas por este pipeline
(
link_observations) e publica o artefato residual promovido no bucket MinIO da plataforma. - Runbook:
docs-site/docs/operations/runbook.md.
Nota histórica: o workflow mensal
retrain-sionna.yml(Keras MLP → TFLite, upload paras3://telecom-tower-power-results/) foi retirado em 2026-07 na decomissão AWS (#410) — o bucket morreu com a conta encerrada. Treino Sionna hoje é manual viascripts/train_sionna.py.
8. Configuração relevante (env vars)¶
| Variável | Default | Uso |
|---|---|---|
OBSERVATION_DEDUP_WINDOW_SEC |
60 |
janela temporal de dedup |
OBSERVATION_SIGNED_WEIGHT |
1.0 |
peso de drift para rows assinadas |
OBSERVATION_UNSIGNED_WEIGHT |
0.1 |
peso para rows sem assinatura |
OBSERVATION_SIGNATURE_MAX_SKEW_SEC |
300 |
janela aceita para ts |
OBSERVATION_REQUIRE_SIGNATURE |
false |
se true, rejeita unsigned/invalid com 401 |
9. Troubleshooting¶
- 401
observation signature invalid— confira que o HMAC foi computado sobre o corpo cru (não o JSON re-serializado pelo cliente). Usedata=rawemrequests, nãojson=payload. - 422
source=drivetest_* requires explicit values for: …— todo row comsourcecomeçando emdrivetest_precisa populartx_gain_dbi,rx_gain_dbi,cable_loss_db,rx_height_mexplicitamente (defaults só servem para sintético). - 403
Tier insufficient— endpoint exige PRO+. Confirme comGET /v1/me/quotaqual o tier da chave. - 500 plain-text
Internal Server Error— bug interno (não exception handler do FastAPI). Reportar paraoncall@telecomtowerpower.com.brcomX-Request-Idda response.