Skip to Content

SDK do IronFlock

O SDK do IronFlock permite que suas aplicações de borda interajam com a plataforma IronFlock. Ele existe em três linguagens — Python, JavaScript e Go — com a mesma funcionalidade, documentadas lado a lado abaixo. Ele cuida da autenticação automaticamente quando executado em um dispositivo registrado e fornece funções para publicar dados, consultar histórico, chamar procedimentos remotos entre dispositivos e atualizar os metadados do dispositivo.

SDKPacoteRequer
Pythonironflock no PyPIPython 3.8+
JavaScriptironflock no npmNode.js 18+ ou navegador moderno
Gogithub.com/RecordEvolution/ironflock-goGo 1.27.2+

Instalação

pip install ironflock

Ou adicione ironflock ao requirements.txt da sua aplicação.

Início Rápido

import asyncio from datetime import datetime, timezone from ironflock import IronFlock async def main(): while True: await ironflock.publish_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60 }) await asyncio.sleep(5) ironflock = IronFlock(mainFunc=main) ironflock.run()

Quando usado dentro de um contêiner de aplicação do IronFlock, o SDK lê as credenciais de conexão do ambiente automaticamente — nenhuma configuração manual é necessária. Toda linha que você escreve em uma tabela carrega um tsp, o seu timestamp (veja publishToTable).

Opções do Construtor

ironflock = IronFlock( mainFunc=main, # async function to run after connecting serial_number="abc123", # override device serial (optional) reconnect_window=60, # seconds a table operation rides out a platform restart (optional) )
ParâmetroDescrição
mainFuncUma função assíncrona que é executada assim que a conexão é estabelecida
serial_numberSobrescreve o número de série do dispositivo enviado com publicações e escritas em tabelas. O padrão é a variável de ambiente DEVICE_SERIAL_NUMBER. Não altera a identidade com a qual a conexão se autentica enquanto o agente de dispositivo injeta uma credencial por app
reconnect_windowSegundos que uma operação de tabela espera a plataforma voltar após uma reinicialização antes de levantar um erro. O padrão é 60; 0 desativa a espera (veja Confiabilidade da Conexão)

Publicando Dados

publishToTable / publish_to_table

Publica um registro de dados em uma tabela da frota. O nome da tabela deve corresponder a uma tabela definida no data-template.yml da sua aplicação. O SDK roteia automaticamente os dados para o banco de dados correto do projeto.

from datetime import datetime, timezone await ironflock.publish_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60, "device_id": "sensor-001" })

O backend de dados lê a linha a partir do primeiro argumento: um dict em Python, o primeiro elemento do array em JavaScript, o primeiro argumento posicional em Go (um map ou uma struct). Argumentos nomeados extras — em Go, um valor ironflock.Kwargs entre os argumentos — são lidos apenas para colunas que o data template mapeia para eles (kwargs.<name>).

Toda linha precisa de um tsp. Toda tabela tem uma coluna tsp obrigatória, o timestamp da linha — por exemplo, uma string ISO 8601 ou, em Go, um time.Time, que é enviado como RFC 3339 em UTC —, que o backend de dados lê da linha, a menos que o data template a mapeie para outro lugar. Ele descarta uma linha publicada sem ela, e o único rastro disso é uma entrada na tabela error-logs do app — a publicação confirmada parece bem-sucedida. Uma linha que ele recusa por falta de armazenamento é descartada sem nem esse rastro. Use appendToTable / append_to_table quando você precisar saber se uma linha foi armazenada.

Linhas parciais em tabelas de entidade. Em uma tabela com chave de entidade (maintainLatestFlagFor), uma linha pode trazer apenas a chave de entidade, tsp e as colunas que mudam. Uma coluna que a linha omite mantém o valor anterior da entidade; uma coluna enviada como None / null / nil é limpa. Se um campo conta como omitido depende de como você monta a linha:

  • Python: uma chave cujo valor é None é enviada como null e limpa a coluna. Remova a chave do dict para manter o valor antigo.
  • JavaScript: uma propriedade cujo valor é undefined é descartada do payload, então mantém o valor antigo. Defina-a como null para limpar a coluna.
  • Go: um campo de struct com a tag omitempty ou omitzero é omitido quando está vazio, então mantém o valor antigo. Caso contrário, um campo ponteiro nil, assim como um valor nil em um ironflock.Row, é enviado como null e limpa a coluna.

O mesmo vale para os quatro métodos de escrita. Em uma tabela sem chave de entidade, uma coluna que a linha omite é armazenada como NULL.

appendToTable / append_to_table

Acrescenta dados a uma tabela da frota usando uma chamada de procedimento remoto em vez de pub/sub. Use isto quando você precisar de confirmação de que os dados foram persistidos: uma linha recusada é um erro.

result = await ironflock.append_to_table("sensordata", { "tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5, "humidity": 60 })

A linha precisa de um tsp, assim como em publishToTable. O backend de dados recusa uma linha sem ele com wamp.error.runtime_error e nenhum motivo; o erro do SDK então acrescenta “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table”. As recusas tipadas, como um disco cheio em um appliance, estão listadas em Recusas do backend de dados.

publishRowsToTable / publish_rows_to_table

Publica várias linhas em uma única mensagem (inserção em lote) em uma tabela da frota. A plataforma insere todo o lote atomicamente (tudo ou nada) em uma única operação. Use isto para dados de alta frequência, onde uma ida e volta por linha seria custosa demais. Assim como publishToTable, isto é fire-and-forget — a confirmação atesta a entrega ao roteador, não a inserção no banco de dados.

await ironflock.publish_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ])

O segundo argumento é uma lista não vazia de objetos de linha a inserir, cada um com o seu próprio tsp — em Go, uma slice de linhas ([]ironflock.Row, []map[string]any ou uma slice de structs). Um lote que o backend de dados recusa — porque uma linha não tem o seu tsp, por exemplo — é descartado sem nenhum aviso a quem publicou, assim como em publishToTable.

appendRowsToTable / append_rows_to_table

Acrescenta várias linhas em uma única chamada de procedimento remoto (inserção em lote) a uma tabela da frota. A plataforma insere todo o lote atomicamente (tudo ou nada): se qualquer linha for inválida, todo o lote é rejeitado e nada é persistido. Prefira isto a publishRowsToTable / publish_rows_to_table quando você precisar do resultado da inserção. Toda linha precisa do seu próprio tsp; as recusas são as de appendToTable, incluindo a dica para um lote recusado sem motivo.

result = await ironflock.append_rows_to_table("sensordata", [ {"tsp": "2024-01-15T10:30:00.000Z", "temperature": 22.5}, {"tsp": "2024-01-15T10:30:01.000Z", "temperature": 22.7}, ]) # result -> {"success": True, "count": 2}

reportError / report_error

Reporta um erro da aplicação na tabela error-logs da sua frota. Isto é um invólucro de conveniência sobre publishToTable / appendToTable: ele carimba a linha com source: "app", um nível de severidade level e um timestamp, e então a escreve como qualquer linha de tabela normal. O erro chega na mesma tabela error-logs que os erros de sistema do fleetdb usam (marcados com source: "system"), de modo que ele pode ser consultado com getHistory, transmitido com subscribeToTable / subscribe_to_table, usado em board-templates e entregue em tempo real em transformed.error-logs — sem disparar o toast de erro de sistema da plataforma.

# Fire-and-forget (default): publishes to the error-logs table await ironflock.report_error("Sensor read timed out", level="warn") # Pass an exception to capture its traceback (falls back to the message) try: risky_operation() except Exception as err: await ironflock.report_error(err) # Use the append RPC when you want to await the insert outcome, and show the # operator a friendlier line than the raw error await ironflock.report_error( "Calibration failed: offset 0.42 out of range", append=True, user_message="Please recalibrate the sensor", )

A linha é { tsp, msg, user_message, source: "app", level }: msg contém o texto técnico (um traceback ou stack), user_message a linha que o toast de erro do board exibe — a mensagem de usuário que você passar ou, sem ela, a própria mensagem.

Parâmetros:

ParâmetroTipoDescrição
errorstr / string ou exceção / ErrorA mensagem de erro, ou uma exceção cujo traceback/stack (ou mensagem) é registrado
levelstr / string, opcionalSeveridade: "error", "warn", "info" ou "debug". O padrão é "error"
appendbool / boolean, opcionalQuando true, usa a RPC de acréscimo (retorna o resultado da inserção). O padrão é false (publicação fire-and-forget)
tspstr / string, opcionalSobrescrita de timestamp ISO-8601. O padrão é o horário atual
user_message / userMessagestr / string, opcionalTexto voltado ao operador, armazenado como user_message. O padrão é a mensagem registrada

Em Python as opções são argumentos nomeados (report_error(error, level=..., append=..., tsp=..., user_message=...)); em JavaScript elas são passadas via um objeto de opções (reportError(error, { level, append, tsp, userMessage })); em Go, via uma struct (ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), onde errOrMsg é um error ou uma string, Level é um de ironflock.LevelError, LevelWarn, LevelInfo, LevelDebug, e o *Result retornado é nil, a menos que Append esteja definido.

publish

Publica uma mensagem em um tópico WAMP próprio. Use isto para mensagens ou eventos personalizados que não correspondem a uma tabela do banco de dados. O roteador reserva alguns nomes para a plataforma — veja URIs que um App Pode Usar.

await ironflock.publish("com.myapp.alerts", { "level": "warning", "message": "Temperature threshold exceeded" })

Consultando Dados Históricos

getHistory

Recupera dados históricos de uma tabela da frota ou de um transform: as linhas mais recentes que atendem à consulta, em ordem crescente de tsp. Suporta filtragem, intervalos de tempo e paginação.

# Simple query data = await ironflock.getHistory("sensordata", {"limit": 100}) # Query with time range and filters data = await ironflock.getHistory("sensordata", { "limit": 500, "offset": 0, "timeRange": ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"], "filterAnd": [ {"column": "temperature", "operator": ">", "value": 20}, {"column": "humidity", "operator": "<=", "value": 80} ] }) # Current value(s) only: the "latest" marker returns the newest row per entity current = await ironflock.getHistory("sensordata", { "limit": 100, "filterAnd": [{"latest": True}] }) # One field of a large json column: the column comes back pruned to its paths readings = await ironflock.getHistory("sensordata", { "limit": 100, "columns": ["json_data"], "columnPaths": ["json_data.sensor.temp"], })

Parâmetros da consulta:

CampoTipoDescrição
limitint / numberNúmero máximo de linhas a retornar (1–10.000, obrigatório; 1–3.000 para um transform)
offsetint / numberDeslocamento para paginação
timeRangelist / array[start, end] — strings ISO de data e hora ou números de milissegundos epoch, nunca misturados; None / null para uma extremidade aberta. Um objeto legado {"start": ..., "end": ...} é convertido no par
filterAndlist / arrayEntradas de filtro combinadas com AND: condições, grupos e/ou o marcador latest (veja abaixo)
columnslist / arrayColunas a retornar (opcional). tsp, device_key e authid são sempre incluídas; omita para obter todas as colunas
columnPathslist / arrayPoda colunas json (opcional): caminhos JSON na notação que os filtros usam ("json_data.a.b", "json_data['key']"). Uma coluna json listada em columns volta contendo apenas a união dos seus caminhos; uma sem caminho aqui volta inteira. Ignorado por transforms e por backends de dados anteriores ao fleetdb v1.0.52

Em Go, estes são os campos Limit, Offset, TimeRange, FilterAnd, Columns e ColumnPaths de ironflock.TableQueryParams. TimeRange é um *ironflock.TimeRange{Start, End}: cada limite é um time.Time, uma string ISO 8601, um número em milissegundos epoch ou nil para um extremo aberto (ironflock.Between, Since e Until constroem um a partir de valores time.Time). FilterAnd é um []ironflock.Filter. Não existe a forma de objeto legada.

Operadores de filtro: =, !=, <>, >, <, >=, <=, LIKE, ILIKE, NOT LIKE, NOT ILIKE, IN, NOT IN, IS NULL, IS NOT NULL

Cada condição de filtro é um objeto com as chaves column, operator e value — em Go, construído com ironflock.Where(column, operator, value), ironflock.IsNull(column) e ironflock.IsNotNull(column). Os operadores são reconhecidos com qualquer combinação de maiúsculas e minúsculas, com os espaços ao redor ignorados, e enviados na forma listada acima — a única que o backend de dados aceita —, de modo que "like" sai como "LIKE", também dentro de grupos. Um operador fora da lista é enviado como foi dado, com um aviso. IS NULL e IS NOT NULL não recebem value e são a única forma de consultar NULL: todos os outros operadores excluem uma linha cuja coluna seja NULL, exatamente como em SQL. IN e NOT IN recebem uma lista (uma slice em Go). LIKE diferencia maiúsculas de minúsculas; use ILIKE para comparar sem diferenciá-las.

Combinando com OR. As entradas de filterAnd são combinadas com AND. Uma entrada de grupo combina as suas próprias entradas — condições, o marcador latest ou outros grupos — com um único operador, "AND" ou "OR":

# Rows that are not soft-deleted: deleted IS NULL OR deleted = false "filterAnd": [ { "combinator": "OR", "filters": [ {"column": "deleted", "operator": "IS NULL"}, {"column": "deleted", "operator": "=", "value": False}, ], } ]

Um backend de dados anterior aos grupos (fleetdb antes da v1.2.0) rejeita esse formato em vez de aplicar parte dele, de modo que falha de forma explícita em vez de retornar silenciosamente linhas que o filtro exclui.

Lendo os valores atuais. Uma entrada {"latest": true} em filterAnd (ironflock.Latest() em Go) não é uma condição de filtro, e sim uma troca de modo: o backend de dados retorna apenas a linha mais recente de cada entidade, derivada em SQL a partir da chave de entidade que a tabela declara com maintainLatestFlagFor. Uma tabela sem chave de entidade retorna a sua única linha mais recente.

As demais condições se combinam com o marcador como você esperaria: condições sobre colunas da chave de entidade restringem quais entidades são retornadas, enquanto todas as outras condições e o timeRange são aplicados às linhas mais recentes resultantes. Assim, combinar {"latest": true} com um filtro de deleted oculta as entidades excluídas em vez de fazer a linha anterior delas reaparecer.

Versões anteriores do IronFlock armazenavam uma coluna física latest_flag. Ela não existe mais — um filtro legado latest_flag = true continua sendo aceito e tratado como o marcador, mas código novo deve usar {"latest": true}. O marcador latest não está disponível em getSeriesHistory.

Intervalos de tempo. O backend de dados lê um limite em string com o Date do JavaScript, portanto escreva uma data ou data e hora ISO 8601 (2026-07-01, 2026-07-01T12:30:15.250Z) e separe data e hora com T. Os SDKs recusam um espaço entre data e hora nos anos 0–99, que o Date interpreta errado: "0001-01-01 00:00:00" — o que o str(datetime.min) do Python produz — seria lido como 2001. Um horário sem fuso é lido como UTC. Um limite numérico é em milissegundos epoch e deve ser finito: NaN e ±Infinity chegariam ao backend de dados como um limite aberto e descartariam silenciosamente o filtro de tempo, por isso os SDKs os recusam antes do envio. Em Go, um limite time.Time é enviado como RFC 3339 em UTC, e um limite em string deve ser uma das formas ISO 8601 que o Date lê corretamente — uma data, ou uma data e uma hora do dia com um Z ou um deslocamento UTC opcional (+02:00 ou +0200); outras strings, como o formato básico 20260701T123000Z, um deslocamento só em horas ou um dia que o mês não tem (2026-02-30), falham com ironflock.ErrInvalidArgument antes que qualquer coisa seja enviada.

Transforms. Um transform (uma view SQL do data template, veja Tabelas de Transformação) é lido de forma diferente de uma tabela. O backend de dados aplica apenas limit — no máximo 3000; um valor maior é recusado com URI_INVALID_LIMIT —, offset e filterAnd. Ele ignora timeRange, columns, columnPaths e o marcador latest, e descarta um filtro sobre uma coluna que o transform não declara (um transform não tem tsp, device_key nem authid implícitos), o que amplia o resultado. As suas linhas vêm na ordem da própria view, invertida: você recebe as linhas mais recentes, das mais antigas para as mais novas, apenas quando a view ordena das mais novas para as mais antigas. Limite o intervalo de tempo de um transform no SQL dele.

Resultados grandes. Um resultado grande demais para uma única mensagem do backend de dados (8 MiB de JSON) chega em partes (chunks), que o SDK remonta na memória: você recebe as linhas de uma única leitura, na mesma ordem. Partes que não fecham — uma ausente ou duplicada, ou uma contagem de linhas diferente do resumo do backend de dados — fazem a leitura falhar em vez de retornar parte das linhas. Uma única linha acima de 8 MiB, ou um resultado acima do limite de proteção de 1 GiB do backend de dados, é recusado com URI_RESULT_TOO_LARGE: reduza o limit, estreite o timeRange, selecione menos columns ou pode com columnPaths.

getSeriesHistory / get_series_history

Recupera dados de séries temporais reduzidos (down-sampled) de uma tabela de frota: colunas agregadas por bucket (intervalo de tempo), por exemplo médias por hora. Ideal para gráficos que abrangem longos intervalos de tempo. Disponível para tabelas (não para transforms), em backends de dados a partir do fleetdb v1.0.58.

from datetime import datetime, timedelta, timezone from ironflock import SeriesMetric now = datetime.now(timezone.utc) avg = SeriesMetric(ref="temperature", method="AVG") # AVG, SUM, COUNT, MIN, MAX, FIRST, LAST series = await ironflock.get_series_history("sensordata", { "metrics": [avg, {"ref": "humidity", "method": "MAX"}], "bucketMs": 3_600_000, # one-hour buckets; omit to divide the range into `limit` buckets "limit": 24, # the bucket budget, 1–10000 "timeRange": [(now - timedelta(hours=24)).isoformat(), now.isoformat()], # required, with a start "groupBy": ["device_key"], }) for row in series: print(row["tsp"], row["device_key"], row[avg.column]) # avg.column is "AVG:temperature"

Parâmetros da consulta:

CampoTipoDescrição
metricslist / arrayOs agregados calculados por bucket, pelo menos um: {"ref": ..., "method": ...}. ref é uma coluna ou um caminho JSON dentro de uma coluna json ("json_data.temp"); method é "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" ou "LAST". Cada métrica volta sob "<METHOD>:<ref>", que deve caber em 63 bytes
bucketMsint / numberLargura do bucket em milissegundos inteiros, no mínimo 1000 (opcional). Omita-o para dividir o intervalo de tempo em limit buckets
limitint / numberO orçamento de buckets (1–10.000) — não um limite de linhas
timeRangelist / array[start, end] — strings ISO de data e hora ou números epoch-ms. Obrigatório, com um início; None / null como fim significa agora. O fim deve ser posterior ao início
groupBylist / arrayColunas que dividem cada bucket em uma linha por grupo (opcional)
filterAndlist / arrayCondições e grupos combinados com AND, como em getHistory (opcional). O marcador latest é recusado aqui, também dentro de um grupo; use getHistory para ler os valores atuais

Linhas. Cada linha é um bucket — de um grupo, com groupBy. tsp é o início do bucket em milissegundos epoch, cada coluna de groupBy volta sob o seu próprio nome e cada métrica sob "<METHOD>:<ref>" (SeriesMetric.column em Python, seriesColumn(metric) em JavaScript, SeriesMetric.Column() em Go), por exemplo row["AVG:temperature"]. As linhas vêm em ordem crescente de tsp; um bucket sem linhas fica ausente (sem preenchimento de lacunas). AVG e SUM aceitam colunas numéricas, MIN e MAX também strings e timestamps; FIRST e LAST dão o valor da linha mais antiga e da mais recente do bucket; COUNT conta os valores que não são nulos (de tsp: as linhas). Um resultado grande é remontado a partir de partes, como em getHistory.

Buckets e uma extremidade aberta. limit conta buckets, não linhas. Sem bucketMs, o intervalo de tempo é dividido em limit buckets. Com ele, o backend de dados alarga os buckets para um múltiplo de bucketMs quando o intervalo contém mais do que limit deles — e uma extremidade aberta vai até o “agora” do próprio backend de dados, um pouco depois do seu. As últimas 24 horas com uma extremidade aberta contêm 25 buckets de uma hora, então limit: 24 recebe buckets de duas horas. Limite o fim, como no exemplo, ou permita um bucket a mais: com bucketMs e uma extremidade aberta, podem voltar limit + 1 buckets.

Os SDKs verificam, antes do envio, o que o backend de dados recusaria: pelo menos uma métrica, refs não vazias, métodos conhecidos, nomes de coluna de até 63 bytes, um início, um fim posterior ao início e bucketMs como um inteiro de no mínimo 1000 (em Go, Bucket de no mínimo 1 segundo, em milissegundos inteiros). O backend de dados recusa o que não consegue responder com URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY ou URI_SERIES_TOO_MANY_GROUPS (mais de 50.000 linhas) — veja Recusas do backend de dados.

Forma obsoleta. As versões do SDK até a 1.9.0 enviavam metrics como nomes de coluna mais um method de nível superior ({"metrics": ["temperature"], "method": "AVG", ...}), o que os backends de dados recusam desde o fleetdb v1.0.58. Essa forma ainda funciona: o SDK envia uma métrica {ref, method} por coluna, emite um aviso (um DeprecationWarning em Python, um aviso no console uma vez por processo em JavaScript) e renomeia as colunas do resultado de volta para os nomes das colunas, de modo que código antigo continua lendo row["temperature"] — a menos que a linha já tenha uma coluna com esse nome, como tsp para COUNT de tsp. Migre para métricas {ref, method} e leia row["AVG:temperature"]. O SDK Go tem apenas a forma atual: um método por métrica (SeriesMetric).

Inscrevendo-se em Dados

subscribeToTable / subscribe_to_table

Inscreve-se em atualizações em tempo real de uma tabela da frota. O handler é chamado sempre que o backend de dados armazena uma nova linha, recebendo como primeiro argumento a linha tal como foi armazenada — valores tipados conforme as colunas do data template, colunas secretas mascaradas. As linhas escritas pelo caminho de inserção em lote (publishRowsToTable / appendRowsToTable) são entregues ao seu handler uma de cada vez, de modo que o código do handler permanece o mesmo independentemente de como os dados foram escritos.

def on_sensor_data(*args, **kwargs): row = args[0] print("New reading:", row["tsp"], row["temperature"]) await ironflock.subscribe_to_table("sensordata", on_sensor_data)

Um transform não tem linhas próprias: a cada execução do seu agendamento, o backend de dados publica a view inteira — no máximo 3000 linhas, na ordem da view invertida — como um único evento cujo primeiro argumento é a lista de linhas. event_rows(args[0]) em Python, eventRows(args) em JavaScript e ev.Rows() em Go leem os dois formatos: a linha de uma tabela dá [row], a lista de um transform dá a própria lista ([] para uma view vazia), e qualquer outra coisa dá None / undefined / nil. Em Go, ev.Row() é nil para o evento de um transform.

from ironflock import event_rows def on_hourly(*args, **kwargs): for row in event_rows(args[0]) or []: print(row) await ironflock.subscribe_to_table("hourly_averages", on_hourly)

subscribe

Inscreve-se em um tópico WAMP próprio para mensagens personalizadas em tempo real. Para linhas de tabela, use subscribeToTable / subscribe_to_table: o roteador recusa inscrições nos tópicos brutos de escrita das tabelas (veja URIs que um App Pode Usar).

def on_alert(*args, **kwargs): print("Alert received:", args, kwargs) await ironflock.subscribe("com.myapp.alerts", on_alert)

Colunas Secretas

Uma coluna declarada como secret: true no data template do app é criptografada em repouso. Nenhum caminho de leitura comum retorna o valor em claro, nem mesmo para o app que o escreveu:

Caminho de leituraComo aparece uma coluna secreta
getHistory, subscribeToTable / subscribe_to_table e todo o resto através do roteador de mensagenso marcador de posição __secret__, exportado como SECRET_PLACEHOLDER (ironflock.SecretPlaceholder em Go)
Acesso SQL (o login Postgres do FleetDB Access)o texto cifrado armazenado, ifsec:1:…
revealSecrets / reveal_secretso valor em claro descriptografado

NULL é repassado como NULL em todos os caminhos, de modo que “definido, porém oculto” continua distinguível de “nunca escrito”.

Em uma tabela de entidade, o ciclo ler-modificar-escrever é seguro: republicar uma linha que ainda carrega o marcador de posição __secret__ — ou omitir a coluna secreta por completo — mantém o segredo armazenado em vez de sobrescrevê-lo, e um null explícito o limpa. Veja Colunas Secretas no guia do backend de dados para as regras exatas.

from datetime import datetime, timezone # Written like any other column — the data backend encrypts it on insert await ironflock.publish_to_table("credentials", { "tsp": datetime.now(timezone.utc).isoformat(), "device_id": "d-1", "api_token": "hunter2", }) # A normal read redacts it rows = await ironflock.getHistory("credentials", {"limit": 1}) # [{"tsp": "...", "device_id": "d-1", "api_token": "__secret__"}]

As duas funções abaixo só podem ser chamadas a partir dos próprios contêineres do app. O roteador de mensagens as nega a todos os papéis de navegador e aos apps consumidores, e é por isso que um handle ConsumedApp não possui equivalente: as colunas secretas de outro app chegam ocultadas e assim permanecem.

revealSecrets / reveal_secrets

Lê linhas de uma das suas próprias tabelas com as colunas secretas descriptografadas.

rows = await ironflock.reveal_secrets("credentials", { "limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}], }) # [{"tsp": "...", "device_id": "d-1", "api_token": "hunter2"}]

Os parâmetros da consulta são os mesmos de getHistory, com uma diferença: limit não pode exceder 100. Esse teto existe para impedir que uma única chamada descriptografe uma tabela inteira, e o data backend rejeita um valor maior em vez de silenciosamente reduzi-lo.

O backend de dados responde a no máximo 30 revelações por minuto por credencial de app em um dispositivo; além disso, a chamada falha com URI_RATE_LIMITED. Revele um segredo uma vez e guarde-o, em vez de revelá-lo a cada uso. Uma tabela sem colunas secretas é recusada com URI_NOT_A_SECRET_COLUMN, e um filtro sobre uma coluna secreta com URI_SECRET_COLUMN.

verifySecret / verify_secret

Confere um valor candidato contra uma coluna secreta sem revelá-la — a contraparte de verificação de senha da função de revelação. A comparação roda dentro do data backend, em tempo constante, e apenas a resposta volta.

ok = await ironflock.verify_secret( "credentials", "api_token", submitted_token, {"limit": 1, "filterAnd": [{"column": "device_id", "operator": "=", "value": "d-1"}]}, ) if ok: ...

Parâmetros:

ParâmetroTipoDescrição
tablenamestr / stringA tabela contra a qual verificar
columnstr / stringA coluna secreta a comparar
candidatestr / stringO valor em claro sendo testado. Comparado no servidor e nunca escrito na tabela
query_params / queryParamsSecretQueryParamsQuais linhas participam. Por padrão, a linha mais recente; limit não pode exceder 100

Em Go, os parâmetros são strings posicionais após o contexto, e a consulta é um *ironflock.TableQueryParams (nil para a linha mais recente).

O JavaScript retorna { match, checked } e o Go um *ironflock.SecretVerifyResult com Match e Checked; o Python retorna um bool simples. Onde você tiver checked, vale usá-lo: ele separa um candidato errado (checked > 0) de um seletor que não encontrou nenhuma linha (checked === 0), que são falhas bem diferentes de reportar a um usuário.

Como cada linha é criptografada com seu próprio valor aleatório, duas linhas que contenham o mesmo segredo armazenam textos cifrados diferentes. É por isso que um filtro = sobre uma coluna secreta não pode funcionar — o backend de dados recusa qualquer filtro sobre uma delas com URI_SECRET_COLUMN — e esta função existe em seu lugar.

O backend de dados responde a no máximo 120 verificações por minuto por credencial de app em um dispositivo; além disso, a chamada falha com URI_RATE_LIMITED. Para uma verificação a cada requisição (uma chave de API, por exemplo), armazene brevemente em cache um resultado positivo. Uma coluna que não é secreta é recusada com URI_NOT_A_SECRET_COLUMN.

Acesso a Dados Entre Apps

Leia os dados de frota de outro app de dentro do seu próprio app, no mesmo projeto. O seu app deve declarar o app provedor na seção consumes: do seu próprio data-template.yml (ou possuir a concessão curinga, abaixo), e o usuário do projeto deve conceder o acesso. O acesso é somente leitura: você pode consultar o histórico e se inscrever em tempo real nas linhas das tabelas e transforms que o provedor compartilha, mas não pode escrever nelas. As conexões com apps consumidos são armazenadas em cache por app e fechadas automaticamente quando a sua instância para.

A conexão precisa da credencial por app que o agente de dispositivo injeta (APP_AUTH_ID, APP_AUTH_SECRET). A plataforma recusa a credencial legada do dispositivo — o fallback por número de série de um app que não recebeu credencial por app, por exemplo sob um agente de dispositivo mais antigo — no realm de outro app (NOT_AUTHORIZED, com uma mensagem que diz isso), a menos que o app provedor rode no mesmo dispositivo; nesse caso, ela entra no realm dele com todos os direitos do próprio app provedor, em vez de somente leitura.

Se o seu app possuir a concessão curinga (consumes: [{ app: "*" }]), você pode descobrir e abrir provedores dinamicamente com listConsumableApps / list_consumable_apps e connectToAllApps / connect_to_all_apps (abaixo).

connectToApp / connect_to_app

Abre uma conexão somente leitura ao backend de dados de outro app e retorna um handle. O handle expõe getHistory / get_history, subscribeToTable / subscribe_to_table e getSeriesHistory / get_series_history (apenas tabelas) — as mesmas consultas e inscrições que você usa nas suas próprias tabelas, com os mesmos parâmetros e resultados — além de close e dos catálogos compartilhados tables / transforms.

# Open a read-only handle on another app's data backend weather = await ironflock.connect_to_app("weather-app") # Inspect what the provider shares print([t["tablename"] for t in weather.tables]) # Query history and subscribe, just like your own tables rows = await weather.get_history("forecasts", {"limit": 100}) def on_forecast(*args, **kwargs): print("New forecast:", args) await weather.subscribe_to_table("forecasts", on_forecast)

Parâmetros:

ParâmetroTipoDescrição
app_name / appNamestr / stringNome do app provedor, conforme declarado na sua seção consumes:
stagestr / string, opcionalStage do provedor: "dev" ou "prod". O padrão é o stage do seu próprio app
on_error / onErrorcallable, opcionalChamado com um CrossAppAccessError quando a conexão aberta é negada de vez (por exemplo, se a concessão for revogada mais tarde — veja abaixo quando uma negação conta como definitiva)

Se o acesso for negado ou usado incorretamente, um CrossAppAccessError é levantado (Python) / lançado (JavaScript) / retornado (Go, um *ironflock.CrossAppAccessError a ser testado com errors.As) com um campo code (Code em Go, com constantes como ironflock.CodeNoGrant): NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (uma consulta que filtra — também dentro de um grupo — ou seleciona uma coluna que o provedor marca como secreta) ou NOT_AUTHORIZED (a concessão foi revogada ou não pôde ser verificada naquele momento — ou o seu app apresentou a credencial legada do dispositivo, caso em que a mensagem diz isso).

Uma negação enquanto connectToApp / connect_to_app aguarda faz a chamada falhar imediatamente. Depois que um handle está aberto, uma negação só conta como definitiva quando persiste — 3 recusas ou mais desde a última vez em que a conexão esteve ativa (o roteador encerrar a sessão por um motivo de autenticação conta como uma, assim como cada reconexão recusada), a primeira há pelo menos 60 segundos —, porque a plataforma recusa da mesma forma enquanto, por um breve período, não consegue verificar o acesso. Até lá, o handle continua se reconectando e se recupera se uma reconexão for bem-sucedida. Quando a negação é definitiva, on_error / onError / OnError é chamado e o handle é removido do cache: o próximo connectToApp / connect_to_app / ConnectToApp abre uma conexão nova. A partir daí, as leituras e as inscrições do handle falham imediatamente, com o erro que on_error / onError recebeu — em Go, com um erro que envolve wamp.ErrStopped.

Em Python, stage e on_error são argumentos nomeados; em JavaScript eles são passados via um objeto de opções (connectToApp(appName, { stage, onError })); em Go, via uma struct (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), onde OnError é uma func(*ironflock.CrossAppAccessError). Em Go, ctx limita apenas a espera de quem chama: chamadas concorrentes para o mesmo app compartilham uma única tentativa, e Close(ctx) fecha um handle.

listConsumableApps / list_consumable_apps

Lista todos os provedores não privados do projeto — a primitiva de descoberta para apps que possuem a concessão de consumo curinga (consumes: [{ app: "*" }] no seu data-template.yml, concedida pelo usuário do projeto). Realiza uma única chamada e não abre nenhuma conexão: renderize os catálogos retornados em um seletor, então chame connectToApp / connect_to_app para os que você deseja — ou connectToAllApps / connect_to_all_apps para abrir todos de uma vez.

Nota: Declare a concessão no data-template.yml do seu app, e coloque o * entre aspas — um * sem aspas é um alias YAML e não será interpretado:

consumes: - app: "*"
providers = await ironflock.list_consumable_apps() for p in providers: print(p["app"], list(p["stages"].keys())) # e.g. "weather-app" ["dev", "prod"]

Cada entrada descreve um provedor:

CampoTipoDescrição
appstr / stringNome do app provedor
provider_app_keyint / numberA chave de app do provedor
stagesdict / objectCatálogo por stage { dev?, prod? }; um stage está presente apenas se o provedor tiver um backend de dados para ele. Cada catálogo contém as tables e transforms não privadas que ele compartilha

Em Go, cada entrada é um ironflock.ConsumedAppInfo com App, ProviderAppKey e Stages.Dev / Stages.Prod (um *StageCatalog com Tables e Transforms, nil para um stage ausente); Catalog(stage) retorna um deles pelo nome.

Levanta (Python) / lança (JavaScript) / retorna (Go) um CrossAppAccessError com code: NO_GRANT se o seu app não possuir a concessão curinga.

connectToAllApps / connect_to_all_apps

Abre handles somente leitura para todos os provedores não privados do projeto em uma única chamada (apenas consumidores curinga). Enumera os provedores via listConsumableApps / list_consumable_apps e abre cada um, ignorando qualquer um sem um backend de dados para o stage solicitado. Cada handle é armazenado em cache sob a mesma chave que connectToApp / connect_to_app, de modo que uma chamada posterior a connectToApp(name) retorna o handle já aquecido. Os handles retornados são fechados em conjunto quando a sua instância para.

apps = await ironflock.connect_to_all_apps( on_error=lambda err: print("Provider skipped:", err) ) for app in apps: rows = await app.get_history(app.tables[0]["tablename"], {"limit": 10}) print(app.app, rows)

Parâmetros:

ParâmetroTipoDescrição
stagestr / string, opcionalStage do provedor: "dev" ou "prod". O padrão é o stage do seu próprio app
continue_on_error / continueOnErrorbool / boolean, opcionalQuando true (o padrão), um provedor que falha ao abrir é reportado para on_error / onError e omitido do resultado. Quando false, a primeira falha é levantada/lançada
on_error / onErrorcallable, opcionalChamado com cada provedor que não pôde ser aberto (enquanto continue_on_error / continueOnError for true), e com um CrossAppAccessError se uma conexão já aberta for negada de vez mais tarde (por exemplo, se a concessão for revogada)

Retorna os handles de provedor abertos com sucesso (o mesmo tipo de handle que connectToApp / connect_to_app). Levanta (Python) / lança (JavaScript) / retorna (Go) um CrossAppAccessError com code: NO_GRANT se o seu app não possuir a concessão curinga.

Em Python, stage, on_error e continue_on_error são argumentos nomeados; em JavaScript eles são passados via um objeto de opções (connectToAllApps({ stage, onError, continueOnError })); em Go, via uma struct (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), onde OnError é uma func(error) e StopOnError: true corresponde a continueOnError: false.

Armazenamento Gerenciado de Arquivos

Todo backend de dados de app recebe um armazenamento de objetos privado ao lado das suas tabelas, acessível através da propriedade files (ifl.Files() em Go, um *filestore.FileStore que pode ser obtido antes de Start). Use-o para imagens, PDFs, frames de câmera, blobs de firmware — qualquer coisa que não pertença a uma linha de tabela. Nenhuma configuração é necessária: um app sem uma seção files: no seu data template ainda recebe um namespace chamado default (um template que declara os seus próprios namespaces o substitui — veja Namespaces).

Esta seção é a referência do lado do cliente. Para o lado da plataforma — declarar namespaces, cotas e retenção, compartilhar arquivos entre apps, credenciais S3 por app e o comportamento on-premises — veja Armazenamento de Arquivos.

A ideia central é que armazenar um objeto lhe devolve uma URL permanente que você pode escrever diretamente em uma coluna de tabela, de modo que um widget de dashboard consiga renderizá-la sem nenhum trabalho adicional:

from datetime import datetime, timezone # Store an object and get a permanent URL back in the same call info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg") # The URL is safe to store in a table column — a dashboard widget can then # render <img src="{{photo_url}}"> without any extra round trip await ironflock.publish_to_table("inspections", { "tsp": datetime.now(timezone.utc).isoformat(), "part_id": "1", "photo_url": info.url, }) # Read it back data = await ironflock.files.get("part-1.jpg") # Every object in the folder 2026/, in its subfolders too (pages are fetched for you) async for obj in ironflock.files.iter(prefix="2026/"): print(obj.key, obj.size)

Essa URL nunca expira, mas ela não é um link público: ela permanece legível apenas para um solicitante autenticado que possua acesso READ neste backend de dados, e um proxy de autenticação reverifica isso a cada requisição. Por isso, ela é segura para armazenar no banco de dados.

Namespaces

Um namespace é um prefixo de chave que carrega política — retenção, regras de compartilhamento, tipos de conteúdo permitidos. Ele não é um bucket separado; todo namespace de um app vive dentro da única área de armazenamento daquele app. Declare um apenas quando um conjunto de objetos precisar de regras diferentes; caso contrário, permaneça em default e organize os seus objetos com caminhos de chave como 2026/03/part-1.jpg.

Declare namespaces no data-template.yml. Declarar qualquer um substitui o default implícito: liste também um chamado default, ou toda chamada sem namespace falha com NO_SUCH_NAMESPACE.

files: # Storage budget the app suggests for itself. The project user can change it, # and their setting is the one that gets enforced. quotaBytes: 5368709120 namespaces: # Keep default: declaring namespaces replaces the implicit one. - name: default - name: frames description: Raw camera frames, one JPEG per inspected part. contentTypes: ["image/jpeg"] maxObjectBytes: 20971520 retention: { deleteAfter: 30 days }

Note que o orçamento é declarado uma única vez para o app inteiro, e não por namespace. Um namespace é apenas um prefixo de chave dentro da única área de armazenamento do app, então não há nada contra o que um orçamento por prefixo pudesse ser imposto. maxObjectBytes é por namespace — ele limita um único objeto, não um total: 100 MiB se não for definido, no máximo 5 GiB (OBJECT_TOO_LARGE acima disso).

Todos os métodos abaixo aceitam o namespace como um argumento opcional e usam default como padrão; um namespace vazio ("") também significa default.

Armazenando e lendo objetos

# Bytes in, bytes out info = await ironflock.files.put("reports/march.pdf", pdf_bytes, content_type="application/pdf") data = await ironflock.files.get("reports/march.pdf") # Or straight from/to a local file — these stream on the large-object path, # so a multi-gigabyte file never has to fit in memory await ironflock.files.put_file("firmware/v2.bin", "/data/build/v2.bin") await ironflock.files.get_to_file("firmware/v2.bin", "/tmp/v2.bin")
MétodoDescrição
put(key, data, …)Armazena um objeto (bytes em Python, Uint8Array em JavaScript, []byte em Go). Retorna os metadados do objeto, incluindo a sua url
get(key, namespace?)Retorna o conteúdo do objeto. Um objeto acima do limite inline vem diretamente do armazenamento de objetos ou, onde isso não for possível, em faixas de bytes através do roteador (veja Objetos grandes)
put_file(key, path, …) / get_to_file(key, path, …)Python e Go (PutFile / GetToFile; o Go também faz streaming a partir de um io.Reader com PutReader e para um io.Writer com GetTo). Armazena a partir de, ou escreve para, um arquivo local. Faz streaming no caminho de objetos grandes
delete(key, namespace?)Exclui um objeto
copy(key, to, …)Copia um objeto, opcionalmente para outro namespace
move(key, to, …)Copiar e depois excluir. Não é atômico — o serviço não tem um verbo de movimentação, então uma exclusão que falhe deixa as duas cópias

O JavaScript não possui helpers de caminho de arquivo porque o pacote entrega um único build tanto para o Node quanto para o navegador — leia e escreva os arquivos locais você mesmo com fs.

Em Python, o trabalho com arquivos de put_file e get_to_file roda fora do event loop. get_to_file cria os diretórios pais que faltam e escreve o arquivo no próprio local assim que os primeiros bytes do objeto chegam: uma leitura recusada (objeto inexistente, não autorizado) deixa o caminho intocado, enquanto uma leitura que falha no meio deixa o que foi escrito até então. Um FIFO é escrito à medida que o objeto chega em streaming, e abri-lo espera por um leitor, de modo que um FIFO que ninguém lê bloqueia a chamada (não o event loop); asyncio.wait_for pode desistir dela. put_file lê um FIFO até o fim antes de armazená-lo.

Em Go, GetToFile também cria os diretórios pais ausentes, mas baixa para um arquivo temporário ao lado do destino e o renomeia para o lugar certo quando concluído: um download que falha ou é cancelado nunca deixa um arquivo parcial e mantém o anterior. Links simbólicos são seguidos (o arquivo para o qual eles apontam é escrito, e os links permanecem); o substituto recebe, na medida em que o processo puder, o dono, o grupo e os bits de permissão do arquivo antigo. Um FIFO ou um dispositivo é escrito no próprio lugar à medida que o objeto chega por streaming. Para um destino que não aceita um novo arquivo ao lado, como um em um sistema de arquivos somente leitura ou pseudo, use GetTo com um arquivo que você mesmo abre.

put aceita: content_type / contentType (o tipo MIME; o namespace pode restringir quais são permitidos) e namespace. Em Python estes são argumentos nomeados; em JavaScript eles vão em um objeto de opções; em Go são opções (filestore.ContentType(…), filestore.Namespace(…)).

Listando e inspecionando

MétodoDescrição
list(…)Uma página da listagem do serviço de arquivos, no estilo de pastas por padrão: objects contém os objetos cujas chaves continuam o prefixo sem uma /, e prefixes as pastas abaixo dele. Um delimiter igual a "" lista de forma plana — todos os objetos sob o prefixo, também em subpastas. Retorna objects, prefixes, is_truncated / isTruncated / IsTruncated e um cursor para ser passado de volta na próxima página
iter(…) / iterate(…)Iterador assíncrono sobre todos os objetos sob um prefixo, também em subpastas, paginando automaticamente. Chamado iter em Python, iterate em JavaScript e Iter em Go (um iter.Seq2[*ObjectInfo, error] para um laço range)
stat(key, namespace?)Metadados de um objeto sem transferir o seu conteúdo
exists(key, namespace?)Se um objeto existe
namespaces()Os namespaces que este app pode usar
usage(…)Quanto armazenamento o app está usando — veja abaixo
catalog()Namespaces mais os limites e as cotas emitidos pelo servidor. Buscado uma vez e armazenado em cache: catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) o busca novamente

Pastas. Um prefixo que termina em / nomeia uma pasta: iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) produz 2026/a.txt e 2026/sub/c.txt, mas não 2026-notes.txt. list não consegue abrir uma pasta pelo nome, porque o serviço de arquivos recusa um prefixo de listagem que termine em / (o FleetFiles v0.2 responde INTERNAL): list com o prefixo 2026 retorna 2026-notes.txt entre os objetos e a pasta 2026/ entre os prefixos. O serviço armazena as chaves na forma de normalização Unicode C, então informe também um prefixo de pasta em NFC; uma iteração que encontra uma chave sob outra forma do prefixo falha em vez de pular todas as chaves.

Os objetos são descritos pelos mesmos campos nos três SDKs, no estilo de nomenclatura de cada linguagem: namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 e url — em Go, os campos Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256 e URL de filestore.ObjectInfo.

Uso de armazenamento e cota

usage responde a partir do object store em uma única chamada, então os totais são exatos em vez de somados pelo SDK:

u = await ironflock.files.usage() print(u.size_bytes, u.object_count, u.quota_bytes, u.free_bytes) # Break the total down per namespace (costs one listing per namespace) detailed = await ironflock.files.usage(detail=True) print(detailed.per_namespace) # {"default": 1048576, "frames": 73400320}

Em Go, os campos são SizeBytes, ObjectCount, QuotaBytes, FreeBytes e PerNamespace de filestore.StorageUsage.

CampoSignificado
size_bytes / sizeBytesBytes atualmente armazenados
object_count / objectCountNúmero de objetos armazenados
quota_bytes / quotaBytesO orçamento imposto. 0 significa ilimitado
free_bytes / freeBytesBytes restantes. -1 significa ilimitado — reportar 0 aí seria lido como “cheio”
per_namespace / perNamespaceBytes por namespace. Presente apenas quando você pede o detalhamento por namespace

O detalhamento por namespace vem desligado por padrão porque o store não consegue respondê-lo diretamente: ele contabiliza por área de armazenamento, e um namespace é apenas um prefixo, então o SDK precisa listar cada namespace para somar os tamanhos. Peça-o quando você quiser, não em um caminho crítico de desempenho.

Duas cotas diferentes aparecem, e vale a pena mantê-las separadas. catalog() reporta as duas:

CampoSignificado
quota_bytes / quotaBytesO que é de fato imposto, lido do object store — a configuração do usuário do projeto
suggested_quota_bytes / suggestedQuotaBytesO que o data template do app pediu. 0 se ele não pediu nada

Em Go, Catalog(ctx) retorna um *filestore.Catalog com QuotaBytes e SuggestedQuotaBytes.

Elas divergem sempre que um usuário aumentou ou diminuiu o orçamento do app, e é por isso que o valor imposto é lido do store em vez do template — reimplantar o app não pode redefinir silenciosamente a escolha de um usuário. Uma UI pode mostrar ambos (“o app sugere X, você definiu Y”). A imposição sempre usa o primeiro. O usuário pode alterar o orçamento a qualquer momento, e catalog() o reporta conforme a sua última busca: leia o valor atual de usage().

Compartilhando objetos

Existem dois tipos de link, e a diferença importa:

MétodoTempo de vidaQuem pode lê-lo
url(key, …)PermanenteApenas um solicitante autenticado com READ neste backend de dados — reverificado a cada requisição. Seguro para armazenar em uma coluna de tabela
share_url / shareUrl / ShareURLExpirável (padrão de 15 min, limitado pelo servidor)Qualquer pessoa que possua o link. Nada reverifica a autorização quando ele é usado

share_url / shareUrl é uma capacidade ao portador (bearer): entregue-o a uma pessoa que precise de acesso temporário, e não o armazene no banco de dados. Use url para qualquer coisa que um dashboard renderize.

url retorna None / undefined / "" (Go) onde a implantação não possui uma borda HTTP (por exemplo, um appliance em HTTP puro) — esse é o sinal para recorrer a get. Passar o etag de um objeto como argumento version (filestore.Version(etag) em Go) permite que os navegadores armazenem a resposta em cache de forma imutável.

Em Python e Go, cloud_url(key, …) / CloudURL(ctx, key, …) retorna a URL permanente através do túnel de arquivos na nuvem de um appliance (https://i<instanceKey>-files.<cloud-edge>): acessível pela internet enquanto o appliance estiver online, com a mesma autorização que url. Ele retorna None ("" em Go) em implantações hospedadas na nuvem (onde url já é acessível pela internet), onde não há borda HTTP e enquanto o compartilhamento na nuvem do backend de dados estiver desligado. O usuário pode alterná-lo a qualquer momento, por isso cloud_url / CloudURL relê o catálogo quando o que está em cache tem mais de 30 segundos.

upload_url / uploadUrl / UploadURL gera uma URL expirável que aceita um upload direto, retornando url, method, headers e expires_in / expiresIn (em Go, um *filestore.UploadTarget com URL, Method, Headers e ExpiresIn). Envie os headers como foram dados, para que o objeto seja armazenado com o tipo de conteúdo definido na geração da URL. Os tipos de conteúdo e o limite de tamanho do namespace, um size declarado e a cota são verificados quando a URL é gerada, não quando é usada: a URL é assinada apenas para o host, então qualquer pessoa que a possua pode escrever qualquer conteúdo na sua chave até ela expirar. Assim como share_url / shareUrl, ela é uma capacidade ao portador (bearer) — uma que escreve.

Objetos grandes

O SDK escolhe o transporte pelo tamanho, automaticamente — não há nada para configurar:

Tamanho do objetoComo ele trafega
Até o limite inline (atualmente 6 MiB)Uma única chamada através do roteador de mensagens
MaiorDiretamente para o armazenamento de objetos via HTTPS, contornando o roteador
Maior, lido onde não há caminho diretoEm faixas de bytes através do roteador de mensagens

O limite exato é reportado pelo servidor em tempo de execução como inline_max_bytes / inlineMaxBytes / InlineMaxBytes em catalog(), de modo que ele pode ser elevado sem um novo release do SDK.

O caminho direto precisa de uma implantação capaz de pré-assinar URLs e de um dispositivo que alcance o host do armazenamento de objetos, não apenas o roteador. Onde falta um dos dois — o catálogo diz que a implantação não consegue pré-assinar (um appliance on-premises, por padrão), a geração da URL de leitura responde NOT_SUPPORTED, ou o armazenamento de objetos não pode ser alcançado antes de qualquer byte chegar —, get (e o get_to_file do Python, e GetTo e GetToFile do Go) leem o objeto em faixas através do roteador (FleetFiles v0.1.33 e posteriores), cada uma de no máximo o limite inline. Cada faixa é uma leitura própria: um objeto substituído durante uma leitura dessas a faz falhar com INTERNAL (“object changed while it was read”). As escritas não têm esse fallback.

Dois tetos permanecem para as escritas, e ambos reportam TOO_LARGE com um motivo que nomeia qual deles você atingiu:

  • 5 GiB — o limite de upload único do armazenamento de objetos. O upload multipart ainda não foi implementado.
  • O limite inline, onde não há endpoint direto — uma implantação que não consegue pré-assinar (um appliance, por padrão) não consegue armazenar um objeto maior de forma alguma. Nenhuma nova tentativa nem chunk menor vai ajudar, e a mensagem diz isso.

Duas falhas do caminho direto comuns em campo recebem os seus próprios códigos em vez de parecerem problemas de autorização: PRESIGN_UNREACHABLE (um proxy que permite apenas o roteador — uma leitura recorre às faixas, uma escrita falha) e CLOCK_SKEW (o armazenamento de objetos rejeita requisições com mais de 15 minutos de defasagem — verifique o NTP no dispositivo).

Erros de armazenamento de arquivos

Uma falha do serviço de arquivos ou de uma transferência direta levanta (Python) / lança (JavaScript) / retorna (Go) um FileStoreError que carrega um code estável e um reason legível por humanos. Ramifique pelo code, nunca pelo reason. Erros de arquivos locais, uma chamada cancelada e erros WAMP no nível do roteador além dos mapeados abaixo são levantados como estão.

from ironflock.filestore import FileStoreError try: await ironflock.files.put("huge.bin", payload) except FileStoreError as e: if e.code == "QUOTA_EXCEEDED": print("Filestore is full:", e.reason) else: raise
CódigoSignificado
NOT_AUTHORIZEDO chamador não pode realizar esta operação
NO_SUCH_NAMESPACEO namespace não está declarado no data template
NO_SUCH_OBJECTA chave não existe
TOO_LARGEExcede o limite de transferência em uma única chamada
OBJECT_TOO_LARGEExcede o maxObjectBytes do próprio namespace
QUOTA_EXCEEDEDO filestore está cheio
CONTENT_TYPE_NOT_ALLOWEDO namespace restringe os contentTypes
INVALID_KEYUma chave ou um prefixo malformado (veja abaixo; o FleetFiles v0.2 ainda o reporta como INTERNAL)
INVALID_RANGEUma faixa de bytes fora do objeto; os SDKs nunca pedem uma
NOT_SUPPORTEDO backend não consegue fazer isto
NOT_AVAILABLEEsta implantação não possui serviço de arquivos — ou ainda não: após uma reinicialização da plataforma, ou enquanto o serviço de arquivos registra os seus procedimentos. A chamada não foi executada em lugar nenhum e pode ser repetida
PRESIGN_UNREACHABLEO armazenamento de objetos não é alcançável diretamente (um proxy?)
CLOCK_SKEWO relógio do dispositivo está defasado demais
INTERNALQualquer outra coisa — também um objeto que mudou durante uma leitura em faixas

Um servidor mais novo pode introduzir códigos que este release do SDK não conhece. Eles são repassados como code em vez de serem colapsados, portanto trate um valor não reconhecido como uma falha genérica.

As chamadas de arquivo aguardam a conexão (até 10 segundos em Python e Go, 6 em JavaScript) e não são repetidas: ao contrário das operações de tabela, elas não atravessam uma reinicialização da plataforma, de modo que durante uma delas uma chamada pode falhar com NOT_AVAILABLE até o serviço de arquivos ter se registrado novamente. Uma chamada assim não foi executada em lugar nenhum, então o seu app pode repeti-la.

Chaves. O serviço de arquivos é a autoridade sobre as chaves; os SDKs enviam chaves e prefixos como foram dados. Uma chave ou um prefixo malformado — vazio; com / no início ou no fim (exceto um prefixo de pasta de iter / iterate); com um segmento vazio, . ou .., ou um segmento que termina em . ou começa ou termina com um espaço; com um de \ : * ? " < > | ^ ~, um caractere de controle ou um caractere de formatação Unicode; acima de 900 bytes, 20 segmentos ou 255 bytes por segmento; ou sob o prefixo reservado _sys/ — é recusado com INVALID_KEY por um serviço de arquivos que classifica erros de chave, e com INTERNAL pelo FleetFiles v0.2. Atenção aos timestamps: 2026-10-09T12:00:00Z não é um segmento de chave válido, por causa do :.

Em Python, FileStoreError é importado de ironflock.filestore; em JavaScript ele é exportado a partir da raiz do pacote (import { FileStoreError } from "ironflock"); em Go, ele é *filestore.Error (também ironflock.FileStoreError), com os códigos como constantes, como filestore.CodeQuotaExceeded.

Comunicação Entre Dispositivos

registerDeviceFunction / register_device_function

Registra um procedimento que outros dispositivos que rodam o mesmo app (no mesmo stage) podem chamar, assim como ações de widgets de dashboard. O SDK monta para você o URI completo <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>, onde STAGE é DEV ou PROD. O roteador não aceita nenhum outro registro de um app, e apenas registros simples (single) (veja URIs que um App Pode Usar). O registro é restaurado após cada reconexão.

def add(a, b): return a + b await ironflock.register_device_function("com.myapp.add", add)

register() é um alias para register_device_function().

callDeviceFunction / call_device_function

Chama um procedimento registrado por outro dispositivo do mesmo app. O SDK monta o tópico WAMP completo automaticamente usando a chave do dispositivo de destino: <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic>, o mesmo URI que registerDeviceFunction / register_device_function registra no dispositivo de destino.

result = await ironflock.call_device_function( 42, # target device key "com.myapp.add", # procedure name args=[3, 5] # arguments ) print(result) # 8

call

Chama um procedimento remoto usando um URI WAMP completo. Um app não pode registrar um nome como some.full.wamp.topic, então um nome assim só alcança procedimentos que serviços da plataforma registram no realm de dados do app, como os do backend de dados e os do armazenamento de arquivos. Para chamar uma função que outro dispositivo do seu app registrou, use callDeviceFunction / call_device_function.

result = await ironflock.call("some.full.wamp.topic", args=[42])

URIs que um App Pode Usar

O contêiner do seu app entra no seu próprio realm de dados, realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>, com o papel de roteador app. O roteador confere cada publicação, inscrição, chamada e registro ali contra a lista de permissões desse papel. Os próprios métodos do SDK ficam dentro dela; as regras importam quando você passa os seus próprios URIs para publish, subscribe ou call, ou usa a connection diretamente (Connection() em Go). Uma ação recusada falha com wamp.error.not_authorized (em Go, ironflock.WampURI(err) o retorna).

URI no seu próprio realmPermitidoUsado por
Os seus próprios nomes, por exemplo com.myapp.statuspublicar, inscrever-se (também por prefixo), chamar — nunca registrarpublish, subscribe, call
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table>publicar — nunca inscrever-sepublishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table
transformed.<table>, transformed.bulk.<table>apenas inscrever-sesubscribeToTable / subscribe_to_table
databackend.errors.…, files.events…apenas inscrever-seOs eventos de erro e de arquivos do backend de dados
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>registrar (single), chamar, publicar — nunca inscrever-seregisterDeviceFunction / register_device_function, callDeviceFunction / call_device_function
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.…chamar (o SDK os chama)Métodos de tabela, histórico, segredos e arquivos
sys.appaccess.list, sys.appaccess.resolvechamarlistConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps
wamp.session.getchamarO heartbeat do SDK JavaScript

Nomes com verificação de identidade. Nomes que começam com um dígito — os tópicos de escrita das tabelas e os URIs de funções de dispositivo — também passam pela verificação de identidade da plataforma, que os vincula à SWARM_KEY do seu realm. Um registro só é aceito para um URI de função do seu próprio dispositivo com a SWARM_KEY, a APP_KEY e o stage do seu realm; um registro por prefixo ou curinga, apenas quando fixa esses quatro segmentos. Publicar e chamar exigem apenas a SWARM_KEY do seu realm: uma linha publicada em um tópico de tabela com outra APP_KEY é aceita, mas não chega a nenhum backend de dados e se perde. Deixe o SDK montar esses URIs.

Nomes reservados. O roteador recusa sys., exceto os dois procedimentos entre apps acima, e wamp., exceto wamp.session.get. Os outros nomes da tabela pertencem à plataforma: não comece os seus próprios nomes com um dígito, bulk., transformed., databackend.errors., files., append., appendBulk., history. ou secret.. Qualquer outro primeiro segmento é o espaço de tópicos do seu próprio app.

Ninguém se inscreve nos tópicos brutos das tabelas. Eles carregam a linha exatamente como foi enviada, incluindo o texto em claro das colunas secretas, que o backend de dados só criptografa quando armazena a linha. subscribeToTable / subscribe_to_table lê, em vez disso, as linhas armazenadas de transformed.<table>, tipadas e com as colunas secretas mascaradas.

Os dados de outros apps são somente leitura. No realm de um provedor, o seu app (papel de roteador app_reader) só pode se inscrever em transformed.… e files.events… e chamar history.transformed.… e files.read.…, além do heartbeat wamp.session.get — sem publicação, sem registro, sem escritas e sem os procedimentos de segredos.

Metadados do Dispositivo

setDeviceLocation / set_device_location

Pede à plataforma que atualize a localização GPS do dispositivo, que os mapas do IronFlock exibem em tempo real.

Indisponível no momento: nenhum serviço da plataforma responde a esta chamada no realm de dados do app ainda, então ela falha com wamp.error.no_such_procedure. Mantenha as localizações em uma tabela própria até que isso mude.

await ironflock.set_device_location(long=8.6821, lat=50.1109)
ParâmetroIntervalo
long-180 a 180
lat-90 a 90

O histórico de localização não é armazenado. Para rastrear a localização ao longo do tempo, crie uma tabela dedicada e use publish_to_table / publishToTable.

getRemoteAccessUrlForPort

Retorna a URL sob a qual uma porta declarada no port-template.yml do app fica acessível assim que o seu túnel estiver ativo.

url = ironflock.getRemoteAccessUrlForPort(8080) # "https://<device_key>-<app_name>-8080.app.ironflock.com" vnc = ironflock.getRemoteAccessUrlForPort(5900, protocol="tcp") # "tcp://app.ironflock.com:<the tunnel's remote port>"
ParâmetroTipoDescrição
portint / numberA porta, conforme declarada em port-template.yml
protocolstr / string, opcionalO protocolo da porta: "http" (padrão), "https", "tcp" ou "udp", com qualquer combinação de maiúsculas e minúsculas; os espaços ao redor são ignorados, e um protocolo vazio equivale a "http"
remote_port_environment / remotePortEnvironmentstr / string, opcionalApenas tcp/udp: a variável que contém a porta remota, em vez de REMOTE_PORT_FOR_<port> — o remote_port_environment da própria porta no template, de que agentes de dispositivo anteriores à 0.19.8 precisam

Portas http e https são servidas sob o rótulo de túnel <device_key>-<app name, lower case>-<port> (portas https com o prefixo secure- da plataforma) no domínio de túnel que o agente de dispositivo injeta (TUNNEL_DOMAIN), sempre como uma URL https://. Em um dispositivo que pertence a um appliance on-premises (o agente injeta INSTANCE_KEY), é retornada em vez disso a rota encaminhada pela nuvem https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN>.

Uma URL tcp ou udp carrega a porta pública que o túnel atribuiu, que o agente de dispositivo anuncia como REMOTE_PORT_FOR_<port> (agente 0.19.8 e posteriores): <protocol>://<TUNNEL_DOMAIN>:<remote port>. Em um dispositivo de um appliance, a porta voltada à internet, REMOTE_PORT_FOR_<port>_CLOUD, tem preferência enquanto o agente a anuncia; antes de ela aparecer, e novamente depois que o agente a remove (encaminhamento pela nuvem desligado), é retornada a URL local do appliance. Uma porta 0 ainda não foi atribuída.

Os valores são lidos ao vivo de /data/env, portanto chame o método novamente em vez de armazenar o resultado em cache. Alguns casos que a URL não consegue atender:

  • O agente de dispositivo cria túneis apenas para as portas de instalações de produção (PROD). Em um contêiner de desenvolvimento (DEV), a URL http(s) é a da instalação PROD do mesmo app neste dispositivo, e tcp/udp não fornecem nenhuma; acesse um app DEV pelo endereço LAN do dispositivo (DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>).
  • Em um dispositivo de borda de um appliance instalado sem domínio (modo simples), o agente define TUNNEL_DOMAIN como localhost, de modo que as URLs compostas ali apontam para localhost: um appliance assim serve as portas dos apps nas portas do seu host, o que uma URL de túnel não consegue expressar.
  • Domínios de túnel não definidos ou vazios recorrem a app.ironflock.com. A plataforma não define CLOUD_TUNNEL_DOMAIN hoje: as URLs de um appliance usam app.ironflock.com, a menos que o próprio app defina essa variável, nas suas configurações de ambiente.
  • Em um contêiner que roda como um usuário diferente de root, os valores são aqueles com que o contêiner iniciou (veja Variáveis de Ambiente).

O método retorna None / null (em Go, "" e false) quando a URL não pode ser composta: a chave do dispositivo ou o nome do app está ausente, uma porta tcp/udp ainda não tem uma porta remota anunciada (ou tem 0), o protocolo é desconhecido ou o rótulo não é um rótulo DNS válido (mais de 63 caracteres, ou um nome de app com um ponto). Em um navegador não há ambiente: a URL http(s) é composta em app.ironflock.com a partir das opções deviceKey e appName.

Propriedades de Conexão e Ciclo de Vida

PropriedadeTipoDescrição
is_connectedboolSe a conexão com a plataforma está ativa
connectionCrossbarConnectionA instância de conexão subjacente (uso avançado)
MétodoDescrição
run()Inicia a conexão e executa mainFunc (bloqueante)
await start()Inicia a conexão de forma assíncrona
await stop()Encerra a conexão e as conexões com apps consumidos, e cancela as tarefas em execução. As conexões saem dos seus realms e não se reconectam depois; start() conecta de novo. Um start() ou connect_to_app que ainda esteja aguardando falha (veja Confiabilidade da Conexão)
await run_async()Inicia e mantém a conexão em execução de forma assíncrona

Confiabilidade da Conexão

O SDK se reconecta sozinho. Quando a conexão cai, ele tenta novamente até o router voltar e então restaura todas as inscrições e todas as funções de dispositivo registradas — você não precisa se inscrever de novo após uma reconexão. Se uma delas não puder ser restaurada, as demais ainda são, e a que falhou é tentada novamente enquanto a sessão durar — após 1 segundo, dobrando até 30 segundos, com cada espera variando em até 15 % — e outra vez após cada reconexão: o router já a aceitou antes, então a recusa é passageira, como a verificação de identidade da plataforma para uma função de dispositivo que falha de forma fechada (fail closed) por um momento. A primeira falha é registrada como aviso, e a restauração é registrada quando é bem-sucedida.

As operações de tabela — escritas, leituras de histórico, de séries e de segredos, e as leituras de histórico de um app consumido — também atravessam uma reinicialização da plataforma, como uma atualização do router ou da plataforma. Elas aguardam até reconnect_window (Python, em segundos) / reconnectWindowMs (JavaScript) / WithReconnectWindow (Go, um time.Duration), 60 segundos por padrão, pela volta da conexão, e tentam novamente enquanto o backend de dados ainda não serve os seus procedimentos: o router responde wamp.error.no_such_procedure, ou o backend de dados recusa uma chamada que se adiantou ao seu registro com “client has no handler for registration”. Uma chamada assim nunca chegou a um handler, então uma nova tentativa nunca escreve uma linha duas vezes; qualquer outro erro é levantado como vier. As chamadas de arquivo não são repetidas (veja Erros de armazenamento de arquivos). Uma requisição já enviada quando a sua sessão é encerrada (o router a encerra, o enlace cai ou stop()) falha imediatamente em vez de esperar uma resposta, e não é repetida: ela pode ter chegado ao seu handler. Em Go, uma requisição assim — e qualquer operação que não encontre conexão dentro da sua espera — falha com um erro que envolve wamp.ErrNotConnected (wamp.ErrStopped após Stop). As outras operações do Go (Publish, Subscribe, Call, as funções de dispositivo, a descoberta entre apps) aguardam até 10 segundos por uma conexão e não são repetidas.

Um caso especial é um router acessível que ainda não tem um realm para o app. Na corrida de instalação concorrente, o container do app sobe antes de o backend de dados ter provisionado o seu realm, então as primeiras tentativas de conexão são recusadas com wamp.error.no_such_realm; o SDK continua tentando a cada poucos segundos e o app sobe assim que o realm existir. Um realm que ainda falta depois de um minuto muito provavelmente nunca vai aparecer — o app foi excluído ou não tem backend de dados para este stage —, então a partir daí o SDK reduz para uma tentativa a cada dois minutos em vez de bater no router a cada segundo para sempre. O ritmo rápido volta assim que o realm aparece, ou quando o próprio router cai e retorna.

stop() encerra toda espera por uma conexão. Um start() que ainda aguarda entrar no realm pela primeira vez — um realm que ainda não existe, por exemplo — falha com Connection to realm <realm> stopped before it opened. Um connect_to_app / connectToApp que ainda está abrindo a conexão com um app provedor falha com Connection to app '<app>' (<stage>) aborted by stop() ou, se a conexão entrar no realm mesmo assim, ela é fechada e a chamada falha com Connection to app '<app>' (<stage>) closed by stop(). Uma negação que chega ao mesmo tempo que stop() mantém o seu CrossAppAccessError. Quando stop() retorna, nada mais fica tentando se conectar; um router que não responde quando a conexão sai do realm é desconectado após 5 segundos.

Em Go, Stop(ctx) também encerra essas esperas, com erros que envolvem wamp.ErrStopped: um Start que ainda aguarda o retorna, e um ConnectToApp que ainda está abrindo falha com ironflock: connection to app '<app>' (<stage>) aborted by Stop (ou closed by Stop, quando a conexão entrou no realm mesmo assim). Stop aguarda a saída do realm dentro de ctx e então fecha o socket; Run lhe dá 10 segundos.

O caso mais difícil é uma conexão que morre silenciosamente: um NAT ou proxy a corta por inatividade sem avisar nenhuma das pontas, ou um router reinicia atrás de um balanceador de carga. Nada chega para sinalizar a perda. Um app que apenas publica descobre na próxima escrita que falha, mas um app que apenas se inscreve nunca escreve, então sem ajuda ele fica morto até o contêiner reiniciar.

Os três SDKs detectam isso. A detecção vem ativa por padrão e não exige configuração.

O SDK Python usa keepalive de WebSocket: envia um ping ao router a cada 30 segundos enquanto o enlace está ocioso e espera um pong em até 10 segundos. Se nenhum chegar, a conexão é descartada e restabelecida. O tráfego de entrada conta como prova de vida, portanto uma conexão movimentada nunca é sondada — o ping só sai quando o enlace realmente ficou em silêncio.

Conexões entre apps abertas com connectToApp / connect_to_app normalmente apenas se inscrevem, que é exatamente o caso protegido aqui. Elas são cobertas automaticamente e também se reconectam depois que o router as recusa, até que a recusa persista (veja connectToApp).

Tratamento de Erros

Todo método do SDK falha de forma explícita: diante de argumentos inválidos, de uma conexão perdida ou de uma rejeição da plataforma, ele levanta uma exceção (Python), rejeita (JavaScript) ou retorna um erro (Go) com uma mensagem que nomeia a operação, o tópico e o motivo. Nada é silenciosamente engolido, portanto envolva em um bloco try as chamadas que você deseja que sobrevivam — em Go, verifique o erro retornado.

try: rows = await ironflock.getHistory("sensordata", {"limit": 100}) except ValueError as e: # Invalid parameters — e.g. limit out of range, or a malformed filter print(f"Bad query: {e}") except RuntimeError as e: # Not connected, table not in the data-template, or the platform rejected the call print(f"Query failed: {e}")

Em JavaScript, as falhas provenientes da plataforma são instâncias de WampError — uma subclasse normal de Error que adicionalmente carrega o URI de erro WAMP em error e a carga do erro em args / kwargs. Todo o restante (parâmetros inválidos, ausência de conexão) é um Error comum.

Em Go, uma operação que falha retorna um *ironflock.OperationError cuja mensagem nomeia a operação e a causa, que ele envolve; errors.Is e errors.As enxergam através dele. Uma recusa do router ou de quem é chamado é um *ironflock.WampError (*wamp.Error) com URI, Args e Kwargs; ironflock.WampURI(err) retorna o URI, ou "". Outras sentinelas são ironflock.ErrMissingConfig (falta uma identidade de que a operação precisa), ironflock.ErrAlreadyStarted e wamp.ErrStopped (após Stop).

Migrando: versões mais antigas dos SDKs Python e JavaScript registravam uma mensagem e retornavam None / null quando uma chamada falhava. Agora elas levantam o erro, de modo que código no formato if result is None: não detecta mais as falhas — use try / except (ou try / catch).

Recusas do backend de dados

Uma operação que o backend de dados recusa falha com um dos URIs de erro WAMP abaixo, com o motivo como primeiro argumento. Os três SDKs os exportam como constantes; o Go os nomeia no estilo Go (ironflock.URIRateLimited, ironflock.URIStorageFull, …). Em Python, os métodos de IronFlock levantam um RuntimeError cuja causa é o ApplicationError do autobahn (as leituras de um app consumido levantam o próprio ApplicationError), e wamp_error_uri(err) lê o URI de qualquer um dos dois. Em JavaScript, o erro é um WampError cujo error é o URI. Em Go, é um *ironflock.OperationError em torno de um *ironflock.WampError, e ironflock.WampURI(err) lê o URI. Os SDKs não repetem nenhum deles. Uma linha publicada que o backend de dados recusa é descartada sem erro — apenas os métodos de append a reportam.

from datetime import datetime, timezone from ironflock import URI_STORAGE_FULL, wamp_error_uri row = {"tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5} try: await ironflock.append_to_table("sensordata", row) except RuntimeError as err: if wamp_error_uri(err) == URI_STORAGE_FULL: pending.append(row) # the appliance's disk is nearly full: append the row later else: raise
ConstanteRecusado
URI_RATE_LIMITEDrevealSecrets / reveal_secrets acima de 30, verifySecret / verify_secret acima de 120 chamadas por minuto por credencial de app em um dispositivo
URI_RESULT_TOO_LARGEUma leitura com uma única linha acima do orçamento de mensagem de 8 MiB, ou um resultado acima do limite de proteção de 1 GiB (outros resultados grandes chegam em partes, que o SDK remonta). Os seus argumentos nomeados indicam as maiores colunas: reduza o limit, estreite o timeRange, selecione menos columns ou pode com columnPaths
URI_INVALID_LIMITUma leitura de um transform com limit acima de 3000
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPSConsultas de séries que a tabela não consegue responder: sem início, ou com um fim que não é posterior a ele; uma métrica que a tabela não consegue agregar (coluna inexistente, coluna secreta, um método que o tipo da coluna não aceita, tsp com um método diferente de COUNT); uma coluna de groupBy pela qual ela não consegue agrupar; mais de 50.000 linhas
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMNUm filtro sobre uma coluna secreta; verifySecret / verify_secret sobre uma coluna que não é secreta, ou revealSecrets / reveal_secrets de uma tabela sem colunas secretas
URI_STORAGE_FULLUm append enquanto o disco do appliance está quase cheio (abaixo de cerca de 3 GiB livres; aceito de novo a partir de 4 GiB) — temporário: acrescente as linhas mais tarde
URI_STORAGE_OVERUSAGEUm append além da cota de armazenamento da conta; tentar de novo logo não ajuda
URI_ENTITY_KEY_CONFLICTUma linha cuja chave de entidade (maintainLatestFlagFor) e cujo tsp outra linha já tem, no lote ou armazenada: dê a cada linha o seu próprio tsp
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTEDUma escrita que mantém o valor anterior de um segredo (__secret__) sem uma chave de entidade para encontrá-lo; uma escrita de um valor já criptografado

Um append que o backend de dados recusa sem motivo — wamp.error.runtime_error, a resposta dele a uma linha sem tsp — não é uma recusa tipada: a mensagem de erro do SDK aponta para o tsp ausente e para a tabela error-logs do app, onde o backend de dados registra a causa.

Uso no Navegador (apenas JavaScript)

O SDK em JavaScript funciona em navegadores modernos. Como os navegadores não possuem variáveis de ambiente, passe toda a configuração pelo construtor:

import { IronFlock } from "ironflock"; const ironflock = new IronFlock({ serialNumber: "device-serial-from-server", deviceKey: "12", appName: "MyWebApp", swarmKey: 10, appKey: 20, env: "PROD", }); await ironflock.start(); await ironflock.publishToTable("sensordata", [{ tsp: new Date().toISOString(), temperature: 22 }]);

Use IronFlock.fromServer() para buscar a configuração do seu backend em vez de embutir as credenciais no código:

const ironflock = await IronFlock.fromServer("/api/ironflock-config"); await ironflock.start();

O endpoint do seu backend deve retornar um objeto JSON com as opções de conexão (serialNumber, deviceKey, appName, swarmKey, appKey, env).

Registrando Funções para Agentes de IA

O SDK pode registrar funções que podem ser chamadas por agentes de IA. Registre um procedimento e referencie o seu tópico no seu ai-template.yml:

def get_sensor_reading(sensor_id): """Returns the latest reading from a sensor.""" reading = read_from_hardware(sensor_id) return { "sensor_id": sensor_id, "temperature": reading.temp, "humidity": reading.hum, "timestamp": reading.ts } await ironflock.register_device_function("sensors.get_latest", get_sensor_reading)

O agente de IA pode então chamar esta função quando um usuário fizer uma pergunta que exija dados de sensor ao vivo.

Para conectar o tópico WAMP registrado a um agente de IA, referencie-o no .ironflock/ai-template.yml da sua aplicação:

sensor_agent: tool_description: | Delegate to this agent when the user asks about sensor readings, live device data, or current environmental conditions. system_prompt: | You are a sensor data specialist. Use get_current to retrieve the latest reading from any sensor. Always include the unit in your response. main: true max_context_tokens: 30000 max_iterations: 5 tools: get_current: description: Returns the latest reading from a sensor. topic: sensors.get_latest parameters: sensor_id: type: string description: The sensor identifier to query. required: true

O valor de topic (sensors.get_latest) deve corresponder ao nome passado para register_device_function / registerDeviceFunction / RegisterDeviceFunction no seu código de borda. O IronFlock roteia automaticamente a chamada para o dispositivo onde a função está registrada.

Para a referência completa do ai-template.yml, consulte Definindo Agentes e Ferramentas.

Variáveis de Ambiente

Estas variáveis são definidas automaticamente pelo runtime do IronFlock dentro dos contêineres de aplicação:

VariávelDescrição
DEVICE_NAMENome de exibição do dispositivo
DEVICE_SERIAL_NUMBERIdentificador único e imutável do dispositivo
DEVICE_KEYChave do dispositivo para autenticação
SWARM_KEYIdentificador do projeto
APP_KEYIdentificador da aplicação
APP_NAMENome da aplicação
ENVAmbiente: DEV ou PROD
APP_AUTH_ID, APP_AUTH_SECRETA credencial própria do app, que o SDK apresenta ao roteador. O acesso entre apps a exige; sem ela (um agente de dispositivo mais antigo), o SDK recorre ao número de série do dispositivo
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")

As variáveis de acesso remoto (TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …) estão listadas em Configurando Portas.

O agente de dispositivo também espelha esses valores em /data/env, um arquivo <NAME>.txt para cada um, e mantém o espelho atualizado enquanto o app roda; os SDKs leem primeiro dele, de modo que valores que mudam em tempo de execução, como uma porta de acesso remoto, chegam a um app em execução. A credencial por app é lida de novo a cada tentativa de conexão, de modo que uma credencial que o agente rotaciona enquanto o app roda é apresentada na próxima reconexão. O agente torna /data/env legível apenas pelo root: em um contêiner que roda como outro usuário, o SDK vê os valores com que o contêiner iniciou e registra um aviso uma única vez.

Last updated on