Skip to content

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

  1. Tier mínimo: PRO (PRO/BUSINESS/ENTERPRISE/ULTRA). Endpoints de ingestão fazem require_tier(...) — chaves FREE/STARTER recebem 403 Forbidden.
  2. Chave de API: emitida via console em https://app.telecomtowerpower.com.br/keys ou, para parceiros enterprise, via POST /admin/keys (uso interno). Prefixo ttp_ identifica chaves de tenant; ttpci_ é reservado para automação interna.
  3. Onboarding mínimo do parceiro:
  4. hostname-pinning de api.telecomtowerpower.com.br
  5. rate-limit por padrão: 600 req/min (tier PRO) — solicitar elevação em partners@telecomtowerpower.com.br se for ingerir > 100k linhas/dia
  6. OBSERVATION_REQUIRE_SIGNATURE está 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 para s3://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 via scripts/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). Use data=raw em requests, não json=payload.
  • 422 source=drivetest_* requires explicit values for: … — todo row com source começando em drivetest_ precisa popular tx_gain_dbi, rx_gain_dbi, cable_loss_db, rx_height_m explicitamente (defaults só servem para sintético).
  • 403 Tier insufficient — endpoint exige PRO+. Confirme com GET /v1/me/quota qual o tier da chave.
  • 500 plain-text Internal Server Error — bug interno (não exception handler do FastAPI). Reportar para oncall@telecomtowerpower.com.br com X-Request-Id da response.