O modelo de confiança
Duas barreiras, cada uma a fechar uma porta diferente:
| barreira | prova | verificada por |
|---|---|---|
| PIN do node | que você está fisicamente na rede do dono | o node |
| grant do backend | que o node existe, tem dono, tem uma vaga livre | o backend |
Três perguntas são mantidas separadas em cada pedido:
| pergunta | transportada por |
|---|---|
| quem é você | token (sma_…) |
| o que pode fazer | scopes (linhas no servidor, não bytes no token) |
| que formato envia | o URL |
Um token são 24 bytes aleatórios com o prefixo sma_ — não codifica nada. As permissões vivem do lado do servidor e podem ser editadas ou revogadas sem reemitir nada.
Emparelhamento — como obter um token
Tudo isto acontece na LAN, contra o servidor HTTP local do node (porta 80). Precisa do IP do node e do seu PIN. (Para descobrir nodes, consulte /info por toda a sub-rede — o que responder é um node.)
1 — verificar o node
GET http://<node-ip>/info
→ 200 { "device_id": "…", "firmware": "1.00", "ws_connected": true }
ws_connected tem de ser true — o node retransmite o seu pedido ao backend pela sua própria ligação.
2 — pedir autorização
POST http://<node-ip>/ext/authorize
Authorization: Bearer <PIN>
{ "kind": "lora-gw", "name": "rooftop RAK", "scopes": "radio.frames,radio.spectrum" }
scopes é uma string separada por vírgulas, não um array JSON. Respostas: 202 {"req":"<id>"} retransmitido, consulte para obter a resposta · 403 PIN errado · 400 falta kind/scopes.
3 — consultar o grant
GET http://<node-ip>/ext/authorize?req=<id>
Authorization: Bearer <PIN>
→ { "status": "pending" }
→ { "status": "granted", "token": "sma_…", "id": "<attachment-id>", "exp": "<iso>" }
→ { "status": "denied", "reason": "…" }
Consulte uma vez por segundo; o node guarda a resposta durante cerca de 5 minutos. Guarde o token com modo 0600 — a partir daí fala diretamente com o backend e o node fica fora do circuito.
Um token concedido mas não usado expira ao fim de 30 minutos. A sua vida completa (30 dias, renovável) começa no primeiro uso. Um node responde por no máximo quatro attachments; o dono pode revogar qualquer um deles a partir da app, o que torna o token inerte de imediato.
Falar com o backend
URL base https://sensmos.com/v1/ext, cada pedido com Authorization: Bearer sma_….
| chamada | finalidade |
|---|---|
GET /v1/ext/me | ainda está vivo, o que pode enviar, que integrações/formatos o servidor aceita (catalog) — chame no arranque |
POST /v1/ext/renew | prolongar a validade do token; uma vez por dia chega |
POST /v1/ext/<integration>/<format> | enviar dados → 200 {"ok":true,"accepted":n} |
Endpoints ativos:
| URL | scope | corpo |
|---|---|---|
/v1/ext/radio/rxpk | radio.frames | JSON PUSH_DATA do packet forwarder Semtech, tal como está |
/v1/ext/radio/frames | radio.frames | formato de frame nativo (abaixo) |
/v1/ext/radio/channel | radio.spectrum | uma leitura de espectro (abaixo) |
Formatos de dados
rxpk — packet forwarder Semtech, um para um. Envie o objeto exatamente como o forwarder o produziu em PUSH_DATA:
{ "rxpk": [ {
"time": "2026-08-21T20:48:11Z",
"freq": 867.3, "datr": "SF9BW125", "modu": "LORA",
"rssi": -44, "lsnr": 12.0, "size": 38, "stat": 1,
"data": "4FNNT1MgY2Nk…"
} ] }
datr só é interpretado para LoRa; a modulação vem de modu. stat segue a Semtech (1 CRC OK, -1 falha, 0 nenhum). tmst é ignorado; sem time o servidor marca a hora de chegada.
frames — formato nativo, para attachments escritos para nós (recetor próprio, SDR, bridge Meshtastic):
{ "frames": [ {
"ts": 1755808091, "freq": 867.3, "sf": 9, "mode": 0,
"rssi": -44, "snr": 12.0, "len": 38, "crc": true,
"hex": "e0534d4f53…", "dir": "rx"
} ] }
mode 0 LoRa / 1 FSK; sf 0 para FSK; hex = payload PHY, os primeiros 128 bytes são guardados. dir: "tx" é para transmissões que o seu gateway faz e que outros conseguem ouvir (downlinks normais com IQ invertido não provam nada — ignore-os); para tx, rssi transporta a potência de TX.
channel — uma leitura de espectro:
{ "freq": 869.618, "bw": 62.5, "sf": 8, "sync": 18, "noise": -118, "peak": -97 }
freq obrigatório, o resto tem 0 por padrão; noise/peak em dBm.
Limites e erros
| código | corpo | significado |
|---|---|---|
| 401 | invalid_token | token desconhecido, expirado ou revogado |
| 403 | missing_scope | token válido, mas não para este endpoint |
| 400 | mensagem do parser | o corpo não corresponde ao formato |
| 404 | unknown_endpoint + catalog | não existe essa integração/formato |
| 413 | too_many | acima do limite do lote — 200 frames por pedido |
Agrupe do seu lado — uma vez por segundo, ou quando acumular 50 frames, o que acontecer primeiro. Os payloads de aplicação permanecem encriptados de ponta a ponta: o servidor trabalha com metadados de rádio e reconhece frames Sensmos no meio do tráfego; não tem chaves para os payloads LoRaWAN de ninguém e não as quer.
O que os dados fazem
Os frames chegam às mesmas tabelas e ao mesmo mapa que os frames ouvidos pelo rádio do próprio node — um attachment não é um ser à parte, é uma antena melhor para o node que respondeu por ele. As transmissões de emergência e os frames de sensores Sensmos são reconhecidos e entregues aos seus donos; os beacons Sensmos verificados constroem arestas de cobertura de rádio entre nodes; o perfil de dados do node que responde ganha a categoria RF.
Attachments prontos a usar: Gateway LoRaWAN (ouvinte passivo). O código de ambos os agentes está no GitHub.
Adicionar um novo tipo de integração
O lado do servidor de cada integração é uma entrada declarativa num registo (scopes, formatos, parser, handler). Candidatos no mesmo padrão, nenhum deles ativo ainda: vista do céu GNSS e interferência (gnss.sky, deliberadamente sem posição), resumos de receção ADS-B e AIS por janela, rtl_433, contadores de energia. Se tem hardware e quer um formato aceite, fale connosco no Discord — uma entrada de formato é uma alteração pequena e fácil de rever.