Skip to Content

IronFlock SDK

El SDK de IronFlock permite que tus aplicaciones de borde interactúen con la plataforma IronFlock. Está disponible en tres lenguajes — Python, JavaScript y Go — con la misma funcionalidad, documentados uno junto a otro a continuación. Gestiona la autenticación automáticamente cuando se ejecuta en un dispositivo registrado y proporciona funciones para publicar datos, consultar historial, llamar procedimientos remotos entre dispositivos y actualizar metadatos de dispositivos.

SDKPaqueteRequisitos
Pythonironflock en PyPIPython 3.8+
JavaScriptironflock en npmNode.js 18+ o navegador moderno
Gogithub.com/RecordEvolution/ironflock-goGo 1.27.2+

Instalación

pip install ironflock

O añade ironflock al archivo requirements.txt de tu app.

Inicio 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()

Cuando se usa dentro de un contenedor de app de IronFlock, el SDK lee las credenciales de conexión del entorno automáticamente — no se necesita configuración manual. Cada fila que escribes en una tabla lleva un tsp, su marca de tiempo (consulta publishToTable).

Opciones del constructor

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ámetroDescripción
mainFuncUna función asíncrona que se ejecuta una vez establecida la conexión
serial_numberSobrescribe el número de serie del dispositivo que se envía con las publicaciones y las escrituras en tablas. Por defecto usa la variable de entorno DEVICE_SERIAL_NUMBER. No cambia la identidad con la que se autentica la conexión mientras el agente del dispositivo inyecte credenciales propias de la app
reconnect_windowSegundos que una operación de tabla espera a que la plataforma vuelva tras un reinicio antes de lanzar un error. Por defecto 60; 0 lo desactiva (consulta Fiabilidad de la conexión)

Publicación de datos

publishToTable / publish_to_table

Publica un registro de datos en una tabla de flota. El nombre de la tabla debe coincidir con una tabla definida en el archivo data-template.yml de tu app. El SDK enruta los datos automáticamente a la base de datos del proyecto correspondiente.

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" })

El backend de datos lee la fila del primer argumento: un dict en Python, el primer elemento del array en JavaScript, el primer argumento posicional en Go (un map o un struct). Los argumentos con nombre adicionales — en Go, un valor ironflock.Kwargs entre los argumentos — solo se leen para las columnas que la plantilla de datos les asigna (kwargs.<name>).

Cada fila necesita un tsp. Toda tabla tiene una columna tsp obligatoria, la marca de tiempo de la fila — p. ej., una cadena ISO 8601, o en Go un time.Time, que se envía como RFC 3339 en UTC —, que el backend de datos lee de la fila salvo que la plantilla de datos la asigne a otra fuente. Descarta una fila publicada sin tsp y solo deja constancia de ello con una entrada en la tabla error-logs de la app — la publicación confirmada parece exitosa. Una fila que rechaza por falta de almacenamiento se descarta sin siquiera eso. Usa appendToTable / append_to_table cuando necesites saber si una fila se almacenó.

Filas parciales en tablas de entidades. En una tabla con clave de entidad (maintainLatestFlagFor), una fila puede llevar solo la clave de entidad, tsp y las columnas que cambian. Una columna que la fila omite conserva el valor anterior de la entidad; una columna enviada como None / null / nil se borra. Que un campo cuente como omitido depende de cómo construyas la fila:

  • Python: una clave cuyo valor es None se envía como null y borra la columna. Elimina la clave del dict para conservar el valor anterior.
  • JavaScript: una propiedad cuyo valor es undefined desaparece del payload, así que conserva el valor anterior. Asígnale null para borrar la columna.
  • Go: un campo de struct con la etiqueta omitempty u omitzero se omite cuando está vacío, así que conserva el valor anterior. En otro caso, un campo puntero nil, igual que un valor nil en un ironflock.Row, se envía como null y borra la columna.

Lo mismo vale para los cuatro métodos de escritura. En una tabla sin clave de entidad, una columna que la fila omite se almacena como NULL.

appendToTable / append_to_table

Añade datos a una tabla de flota usando una llamada a procedimiento remoto en lugar de pub/sub. Usa esto cuando necesites confirmación de que los datos fueron persistidos: una fila rechazada es un error.

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

La fila necesita un tsp, igual que con publishToTable. El backend de datos rechaza una fila sin tsp con wamp.error.runtime_error y sin motivo; el error del SDK añade entonces “the data backend refused the row without a reason — a missing tsp is the usual cause; see the error-logs table”. Los rechazos tipados, como un disco lleno en un appliance, se enumeran en Rechazos del backend de datos.

publishRowsToTable / publish_rows_to_table

Publica muchas filas en un solo mensaje (inserción masiva) en una tabla de flota. La plataforma inserta todo el lote de forma atómica (todo o nada) en una sola operación. Usa esto para datos de alta frecuencia donde una ida y vuelta por fila sería demasiado costosa. Al igual que publishToTable, esto es fire-and-forget — la confirmación valida la entrega al router, no la inserción en la base de datos.

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}, ])

El segundo argumento es una lista no vacía de objetos de fila a insertar, cada uno con su propio tsp — en Go, un slice de filas ([]ironflock.Row, []map[string]any o un slice de structs). Un lote que el backend de datos rechaza — porque a una fila le falta su tsp, por ejemplo — se descarta sin avisar a quien publica, igual que con publishToTable.

appendRowsToTable / append_rows_to_table

Añade muchas filas en una sola llamada a procedimiento remoto (inserción masiva) en una tabla de flota. La plataforma inserta todo el lote de forma atómica (todo o nada): si cualquier fila es inválida, se rechaza el lote completo y no se persiste nada. Prefiere esto sobre publishRowsToTable / publish_rows_to_table cuando necesites conocer el resultado de la inserción. Cada fila necesita su propio tsp; los rechazos son los de appendToTable, incluida la pista para un lote rechazado sin 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 un error de aplicación en la tabla error-logs de tu flota. Esto es un envoltorio de conveniencia sobre publishToTable / appendToTable: marca la fila con source: "app", un level de severidad y una marca de tiempo, y luego la escribe como cualquier fila normal de tabla. El error llega a la misma tabla error-logs que usan los errores de sistema de fleetdb (etiquetados con source: "system"), por lo que se puede consultar con getHistory, transmitir con subscribeToTable / subscribe_to_table, usar en plantillas de tablero y entregar en tiempo real en transformed.error-logs — sin disparar el toast de error de sistema de la 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", )

La fila es { tsp, msg, user_message, source: "app", level }: msg contiene el texto técnico (un traceback o un stack), user_message la línea que muestra el toast de error del tablero — el mensaje de usuario que pasas o, si no pasas ninguno, el propio mensaje.

Parámetros:

ParámetroTipoDescripción
errorstr / string o excepción / ErrorEl mensaje de error, o una excepción cuyo traceback/stack (o mensaje) se registra
levelstr / string, opcionalSeveridad: "error", "warn", "info" o "debug". Por defecto "error"
appendbool / boolean, opcionalCuando es true, usa la RPC de append (devuelve el resultado de la inserción). Por defecto false (publicación fire-and-forget)
tspstr / string, opcionalSobrescritura de marca de tiempo ISO-8601. Por defecto la hora actual
user_message / userMessagestr / string, opcionalTexto para el operador, almacenado como user_message. Por defecto, el mensaje registrado

En Python las opciones son argumentos con nombre (report_error(error, level=..., append=..., tsp=..., user_message=...)); en JavaScript se pasan a través de un objeto de opciones (reportError(error, { level, append, tsp, userMessage })); en Go, a través de un struct (ReportError(ctx, errOrMsg, ironflock.ReportErrorOptions{Level, Append, Tsp, UserMessage})), donde errOrMsg es un error o una cadena, Level uno de ironflock.LevelError, LevelWarn, LevelInfo, LevelDebug, y el *Result devuelto es nil salvo que se indique Append.

publish

Publica un mensaje en un topic WAMP propio. Usa esto para mensajería personalizada o eventos que no se corresponden con una tabla de base de datos. El router reserva algunos nombres para la plataforma — consulta URIs que una app puede usar.

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

Consulta de datos históricos

getHistory

Recupera datos históricos de una tabla de flota o de un transform: las filas más recientes que coinciden, en orden ascendente de tsp. Soporta filtrado, rangos de tiempo y paginación.

# 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 de consulta:

CampoTipoDescripción
limitint / numberMáximo de filas a devolver (1–10.000, obligatorio; 1–3.000 para un transform)
offsetint / numberDesplazamiento para paginación
timeRangelist / array[start, end] — cadenas de fecha y hora ISO o números de milisegundos epoch, nunca mezclados; None / null para un extremo abierto. Un objeto heredado {"start": ..., "end": ...} se convierte en el par
filterAndlist / arrayEntradas de filtro combinadas con AND: condiciones, grupos y/o el marcador latest (ver abajo)
columnslist / arrayColumnas a devolver (opcional). tsp, device_key y authid siempre se incluyen; omítelo para obtener todas las columnas
columnPathslist / arrayRecorta columnas json (opcional): rutas JSON en la notación que usan los filtros ("json_data.a.b", "json_data['key']"). Una columna json incluida en columns vuelve conteniendo solo la unión de sus rutas; una sin ruta aquí vuelve completa. Lo ignoran los transforms y los backends de datos anteriores a fleetdb v1.0.52

En Go, estos son los campos Limit, Offset, TimeRange, FilterAnd, Columns y ColumnPaths de ironflock.TableQueryParams. TimeRange es un *ironflock.TimeRange{Start, End}: cada límite es un time.Time, una cadena ISO 8601, un número en milisegundos epoch o nil para un extremo abierto (ironflock.Between, Since y Until construyen uno a partir de valores time.Time). FilterAnd es un []ironflock.Filter. No existe la forma de objeto heredada.

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

Cada condición de filtro es un objeto con las claves column, operator y value — en Go, construido con ironflock.Where(column, operator, value), ironflock.IsNull(column) e ironflock.IsNotNull(column). Los operadores se reconocen sin distinguir mayúsculas y minúsculas, ignorando los espacios alrededor, y se envían en la forma indicada arriba — la única que acepta el backend de datos —, de modo que "like" sale como "LIKE", también dentro de grupos. Un operador que no está en la lista se envía tal cual, con una advertencia. IS NULL e IS NOT NULL no llevan value y son la única forma de preguntar por NULL: cualquier otro operador excluye una fila cuya columna sea NULL, exactamente como en SQL. IN y NOT IN reciben una lista (un slice en Go). LIKE distingue mayúsculas y minúsculas; usa ILIKE para comparar sin distinguirlas.

Combinar con OR. Las entradas de filterAnd se combinan con AND. Una entrada de grupo combina sus propias entradas — condiciones, el marcador latest u otros grupos — con un único operador, "AND" u "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}, ], } ]

Un backend de datos anterior a los grupos (fleetdb antes de v1.2.0) rechaza esta forma en lugar de aplicar solo una parte, así que falla de forma explícita en vez de devolver en silencio filas que el filtro excluye.

Leer los valores actuales. Una entrada {"latest": true} en filterAnd (ironflock.Latest() en Go) no es una condición de filtro sino un cambio de modo: el backend de datos devuelve solo la fila más reciente de cada entidad, derivada en SQL a partir de la clave de entidad que la tabla declara con maintainLatestFlagFor. Una tabla sin clave de entidad devuelve su única fila más reciente.

Las demás condiciones se combinan con el marcador como cabría esperar: las condiciones sobre columnas de la clave de entidad acotan qué entidades se devuelven, mientras que todas las demás condiciones y timeRange se aplican a las filas más recientes resultantes. Así, combinar {"latest": true} con un filtro sobre deleted oculta las entidades eliminadas en lugar de hacer reaparecer su fila anterior.

Las versiones anteriores de IronFlock almacenaban una columna física latest_flag. Ya no existe — un filtro heredado latest_flag = true se sigue aceptando y se trata como el marcador, pero el código nuevo debería usar {"latest": true}. El marcador latest no está disponible en getSeriesHistory.

Rangos de tiempo. El backend de datos lee un límite en forma de cadena con el Date de JavaScript, así que escribe una fecha o una fecha y hora ISO 8601 (2026-07-01, 2026-07-01T12:30:15.250Z) y separa la fecha y la hora con T. Los SDKs rechazan un espacio entre fecha y hora en los años 0–99, que Date interpreta mal: "0001-01-01 00:00:00" — lo que devuelve str(datetime.min) en Python — se leería como 2001. Una hora sin zona horaria se lee como UTC. Un límite numérico se expresa en milisegundos epoch y debe ser finito: NaN y ±Infinity llegarían al backend de datos como un extremo abierto y eliminarían en silencio el filtro de tiempo, así que los SDKs los rechazan antes de enviar. En Go, un límite time.Time se envía como RFC 3339 en UTC, y un límite de cadena debe ser una de las formas ISO 8601 que Date lee correctamente — una fecha, o una fecha y una hora del día con un Z o un desfase UTC opcional (+02:00 o +0200); otras cadenas, como el formato básico 20260701T123000Z, un desfase solo de horas o un día que el mes no tiene (2026-02-30), fallan con ironflock.ErrInvalidArgument antes de que se envíe nada.

Transforms. Un transform (una vista SQL de una plantilla de datos, consulta Tablas de transformación) se lee de forma distinta a una tabla. El backend de datos solo aplica limit — como máximo 3000; uno mayor se rechaza con URI_INVALID_LIMIT —, offset y filterAnd. Ignora timeRange, columns, columnPaths y el marcador latest, y descarta un filtro sobre una columna que el transform no declara (un transform no tiene columnas implícitas tsp, device_key ni authid), lo que amplía el resultado. Sus filas llegan en el orden propio de la vista, invertido: obtienes las filas más recientes, de la más antigua a la más reciente, solo cuando la vista ordena primero las más recientes. Acota el rango de tiempo de un transform en su SQL.

Resultados grandes. Un resultado demasiado grande para un único mensaje del backend de datos (8 MiB de JSON) llega en fragmentos, que el SDK vuelve a ensamblar en memoria: obtienes las filas de una única lectura, en el mismo orden. Los fragmentos que no cuadran — uno que falta o está duplicado, o un recuento de filas que difiere del resumen del backend de datos — hacen fallar la lectura en lugar de devolver parte de las filas. Una sola fila de más de 8 MiB, o un resultado que supera la protección de 1 GiB del backend de datos, se rechaza con URI_RESULT_TOO_LARGE: reduce limit, acota timeRange, selecciona menos columns o recorta con columnPaths.

getSeriesHistory / get_series_history

Recupera datos de series temporales submuestreados de una tabla de flota: columnas agregadas por intervalo de tiempo (p. ej., promedios por hora). Ideal para gráficos que abarcan rangos de tiempo largos. Disponible para tablas (no para transforms), en backends de datos a partir de 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 de consulta:

CampoTipoDescripción
metricslist / arrayLos agregados que se calculan por intervalo, al menos uno: {"ref": ..., "method": ...}. ref es una columna o una ruta JSON dentro de una columna json ("json_data.temp"); method es "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" o "LAST". Cada métrica vuelve bajo "<METHOD>:<ref>", que debe caber en 63 bytes
bucketMsint / numberAncho del intervalo en milisegundos enteros, al menos 1000 (opcional). Omítelo para dividir el rango de tiempo en limit intervalos
limitint / numberEl presupuesto de intervalos (1–10.000) — no un límite de filas
timeRangelist / array[start, end] — cadenas de fecha y hora ISO o números epoch-ms. Obligatorio, con un inicio; None / null como fin significa ahora. El fin debe ser posterior al inicio
groupBylist / arrayColumnas que dividen cada intervalo en una fila por grupo (opcional)
filterAndlist / arrayCondiciones y grupos combinados con AND, como en getHistory (opcional). Aquí se rechaza el marcador latest, también dentro de un grupo; usa getHistory para leer los valores actuales

Filas. Cada fila es un intervalo — de un grupo, con groupBy. tsp es el inicio del intervalo en milisegundos epoch, cada columna de groupBy vuelve con su propio nombre y cada métrica bajo "<METHOD>:<ref>" (SeriesMetric.column en Python, seriesColumn(metric) en JavaScript, SeriesMetric.Column() en Go), p. ej. row["AVG:temperature"]. Las filas van en orden ascendente de tsp; un intervalo sin filas no aparece (no se rellenan huecos). AVG y SUM admiten columnas numéricas, MIN y MAX también cadenas y marcas de tiempo; FIRST y LAST dan el valor de la fila más antigua y de la más reciente del intervalo; COUNT cuenta los valores que no son nulos (en el caso de tsp: las filas). Un resultado grande se vuelve a ensamblar a partir de fragmentos, como en getHistory.

Intervalos y un extremo abierto. limit cuenta intervalos, no filas. Sin bucketMs, el rango de tiempo se divide en limit intervalos. Con él, el backend de datos ensancha los intervalos a un múltiplo de bucketMs cuando el rango contiene más de limit — y un fin abierto llega hasta el ahora del propio backend de datos, un poco posterior al tuyo. Las últimas 24 horas con un fin abierto contienen 25 intervalos de una hora, así que limit: 24 obtiene intervalos de dos horas. Acota el fin, como en el ejemplo, o admite un intervalo más: con bucketMs y un fin abierto pueden volver limit + 1 intervalos.

Los SDKs comprueban antes de enviar lo que el backend de datos rechazaría: al menos una métrica, refs no vacías, métodos conocidos, nombres de columna de hasta 63 bytes, un inicio, un fin posterior al inicio y bucketMs como entero de al menos 1000 (en Go, Bucket de al menos 1 segundo, en milisegundos enteros). El backend de datos rechaza lo que no puede responder con URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY o URI_SERIES_TOO_MANY_GROUPS (más de 50.000 filas) — consulta Rechazos del backend de datos.

Forma obsoleta. Las versiones del SDK hasta la 1.9.0 enviaban metrics como nombres de columna más un method de nivel superior ({"metrics": ["temperature"], "method": "AVG", ...}), que los backends de datos rechazan desde fleetdb v1.0.58. Esa forma sigue funcionando: el SDK envía una métrica {ref, method} por columna, emite una advertencia (un DeprecationWarning en Python, una advertencia en consola una vez por proceso en JavaScript) y vuelve a renombrar las columnas del resultado con los nombres de columna, de modo que el código antiguo sigue leyendo row["temperature"] — salvo que la fila ya tenga una columna con ese nombre, como tsp para COUNT de tsp. Pasa a métricas {ref, method} y lee row["AVG:temperature"]. El SDK de Go solo tiene la forma actual: un método por métrica (SeriesMetric).

Suscripción a datos

subscribeToTable / subscribe_to_table

Se suscribe a actualizaciones en tiempo real de una tabla de flota. El handler se llama cada vez que el backend de datos almacena una fila nueva, con la fila tal como se almacenó — valores tipados según las columnas de la plantilla de datos, columnas secretas enmascaradas — como primer argumento. Las filas escritas a través de la ruta de inserción masiva (publishRowsToTable / appendRowsToTable) se entregan a tu handler una por una, por lo que el código del handler permanece igual sin importar cómo se escribieron los datos.

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)

Un transform no tiene filas propias: en cada ejecución de su programación, el backend de datos publica la vista completa — como máximo 3000 filas, en el orden inverso de la vista — como un único evento cuyo primer argumento es la lista de filas. event_rows(args[0]) en Python, eventRows(args) en JavaScript y ev.Rows() en Go leen ambas formas: la fila de una tabla da [row], la lista de un transform la propia lista ([] para una vista vacía), y cualquier otra cosa None / undefined / nil. En Go, ev.Row() es nil para el evento de un 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

Se suscribe a un topic WAMP propio para mensajería personalizada en tiempo real. Para filas de tablas usa subscribeToTable / subscribe_to_table: el router rechaza las suscripciones a los topics de escritura en bruto de las tablas (consulta URIs que una app puede usar).

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

Columnas secretas

Una columna declarada secret: true en la plantilla de datos de la app se almacena cifrada. Ninguna ruta de lectura ordinaria devuelve el texto en claro, ni siquiera a la app que lo escribió:

Ruta de lecturaQué aspecto tiene una columna secreta
getHistory, subscribeToTable / subscribe_to_table y todo lo demás que pasa por el router de mensajesel marcador de posición __secret__, exportado como SECRET_PLACEHOLDER (ironflock.SecretPlaceholder en Go)
El acceso SQL (el inicio de sesión de Postgres de FleetDB Access)el texto cifrado almacenado, ifsec:1:…
revealSecrets / reveal_secretsel texto en claro descifrado

NULL se transmite tal cual como NULL en todas las rutas, así que establecido pero oculto se sigue distinguiendo de nunca escrito.

En una tabla de entidades, el patrón leer-modificar-escribir es seguro: volver a publicar una fila que todavía lleva el marcador de posición __secret__ — u omitir por completo la columna secreta — conserva el secreto almacenado en lugar de sobrescribirlo, y un null explícito lo borra. Consulta Columnas secretas en la guía del backend de datos para conocer las reglas exactas.

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__"}]

Ambas funciones que siguen solo se pueden llamar desde los propios contenedores de la app. El router de mensajes se las deniega a todos los roles de navegador y a las apps consumidoras, y por eso un manejador ConsumedApp no tiene un equivalente: las columnas secretas de otra app llegan enmascaradas y así se quedan.

revealSecrets / reveal_secrets

Lee filas de una de tus propias tablas con sus columnas secretas descifradas.

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

Los parámetros de consulta son los mismos que los de getHistory, con una diferencia: limit no puede superar 100. Ese techo existe para impedir que una sola llamada descifre una tabla entera, y el backend de datos rechaza un valor mayor en lugar de recortarlo en silencio.

El backend de datos responde como máximo a 30 revelaciones por minuto por credencial de app en un dispositivo; por encima de eso, la llamada falla con URI_RATE_LIMITED. Revela un secreto una vez y consérvalo, en lugar de hacerlo en cada uso. Una tabla sin columnas secretas se rechaza con URI_NOT_A_SECRET_COLUMN, y un filtro sobre una columna secreta con URI_SECRET_COLUMN.

verifySecret / verify_secret

Comprueba un valor candidato contra una columna secreta sin revelarla — la contraparte de la función de revelado para comprobar contraseñas. La comparación se ejecuta dentro del backend de datos, en tiempo constante, y solo vuelve la respuesta.

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ámetroTipoDescripción
tablenamestr / stringLa tabla contra la que se comprueba
columnstr / stringLa columna secreta a comparar
candidatestr / stringEl texto en claro que se está probando. Se compara en el servidor y nunca se escribe en la tabla
query_params / queryParamsSecretQueryParamsQué filas participan. Por defecto, la fila más reciente; limit no puede superar 100

En Go, los parámetros son cadenas posicionales después del contexto, y la consulta es un *ironflock.TableQueryParams (nil para la fila más reciente).

JavaScript devuelve { match, checked } y Go un *ironflock.SecretVerifyResult con Match y Checked; Python devuelve un bool simple. Cuando dispones de checked, merece la pena usarlo: distingue un candidato incorrecto (checked > 0) de un selector que no ha encontrado ninguna fila (checked === 0), dos fallos muy distintos a la hora de informar al usuario.

Como cada fila se cifra con su propio valor aleatorio, dos filas que contienen el mismo secreto almacenan textos cifrados distintos. Por eso un filtro = sobre una columna secreta no puede funcionar — el backend de datos rechaza cualquier filtro sobre una de ellas con URI_SECRET_COLUMN — y existe en su lugar esta función.

El backend de datos responde como máximo a 120 comprobaciones por minuto por credencial de app en un dispositivo; por encima de eso, la llamada falla con URI_RATE_LIMITED. Para una comprobación en cada petición (una clave de API, por ejemplo), guarda en caché brevemente un resultado positivo. Una columna que no es secreta se rechaza con URI_NOT_A_SECRET_COLUMN.

Acceso a datos entre apps

Lee los datos de flota de otra app desde dentro de tu propia app, en el mismo proyecto. Tu app debe declarar la app proveedora en la sección consumes: de su propio data-template.yml (o tener la concesión comodín, ver más abajo), y el usuario del proyecto debe conceder el acceso. El acceso es de solo lectura: puedes consultar el historial y suscribirte en tiempo real a las filas de las tablas y transforms que comparte el proveedor, pero no puedes escribir en ellas. Las conexiones a apps consumidas se almacenan en caché por app y se cierran automáticamente cuando tu instancia se detiene.

La conexión necesita las credenciales propias de la app que inyecta el agente del dispositivo (APP_AUTH_ID, APP_AUTH_SECRET). La plataforma rechaza la credencial heredada del dispositivo — el recurso al número de serie de una app que no recibió credenciales propias, p. ej. con un agente del dispositivo más antiguo — en el realm de otra app (NOT_AUTHORIZED, con un mensaje que lo indica), salvo que la app proveedora se ejecute en el mismo dispositivo: entonces la conexión se une a su realm con todos los derechos de esa app en lugar de solo lectura.

Si tu app posee la concesión comodín (consumes: [{ app: "*" }]), puedes descubrir y abrir proveedores de forma dinámica con listConsumableApps / list_consumable_apps y connectToAllApps / connect_to_all_apps (más abajo).

connectToApp / connect_to_app

Abre una conexión de solo lectura al backend de datos de otra app y devuelve un manejador (handle). El manejador expone getHistory / get_history, subscribeToTable / subscribe_to_table y getSeriesHistory / get_series_history (solo tablas) — las mismas consultas y suscripciones que usas en tus propias tablas, con los mismos parámetros y resultados — además de close y los catálogos compartidos 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ámetroTipoDescripción
app_name / appNamestr / stringNombre de la app proveedora, tal como se declara en tu sección consumes:
stagestr / string, opcionalStage del proveedor: "dev" o "prod". Por defecto, el stage de tu propia app
on_error / onErrorcallable, opcionalSe invoca con un CrossAppAccessError cuando a la conexión abierta se le deniega el acceso de forma definitiva (p. ej., si la concesión se revoca más tarde — más abajo se explica cuándo una denegación se considera definitiva)

Si el acceso se deniega o se usa incorrectamente, se lanza un CrossAppAccessError (Python / JavaScript) o se devuelve (Go, un *ironflock.CrossAppAccessError que se comprueba con errors.As) con un campo code (Code en Go, con constantes como ironflock.CodeNoGrant): NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (una consulta que filtra — también dentro de un grupo — o selecciona una columna que el proveedor marca como secreta) o NOT_AUTHORIZED (la concesión se revocó o no pudo verificarse en ese momento — o tu app presentó la credencial heredada del dispositivo, en cuyo caso el mensaje lo indica).

Una denegación mientras connectToApp / connect_to_app espera hace fallar la llamada de inmediato. Una vez abierto un manejador, una denegación solo se considera definitiva cuando persiste — 3 rechazos o más desde la última vez que la conexión estuvo activa (que el router cierre la sesión por un motivo de autenticación cuenta como uno, igual que cada reconexión rechazada), el primero hace al menos 60 segundos —, porque la plataforma rechaza de la misma forma mientras brevemente no puede verificar el acceso. Hasta entonces, el manejador sigue reconectándose y se recupera si una reconexión tiene éxito. Una vez que la denegación es definitiva, se llama a on_error / onError / OnError y el manejador se descarta: el siguiente connectToApp / connect_to_app / ConnectToApp abre una conexión nueva. A partir de entonces, las lecturas y suscripciones del manejador fallan de inmediato, con el error que recibió on_error / onError — en Go, con un error que envuelve wamp.ErrStopped.

En Python, stage y on_error son argumentos con nombre; en JavaScript se pasan a través de un objeto de opciones (connectToApp(appName, { stage, onError })); en Go, a través de un struct (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), donde OnError es una func(*ironflock.CrossAppAccessError). En Go, ctx solo acota la espera de quien llama: las llamadas simultáneas para la misma app comparten un intento, y Close(ctx) cierra un manejador.

listConsumableApps / list_consumable_apps

Lista todos los proveedores no privados del proyecto — la primitiva de descubrimiento para apps que poseen la concesión de consumo comodín (consumes: [{ app: "*" }] en tu data-template.yml, concedida por el usuario del proyecto). Realiza una única llamada y no abre ninguna conexión: renderiza los catálogos devueltos en un selector, luego llama a connectToApp / connect_to_app para los que quieras — o a connectToAllApps / connect_to_all_apps para abrirlos todos a la vez.

Nota: Declara la concesión en el data-template.yml de tu app, y entrecomilla el * — un * sin comillas es un alias de YAML y no se parseará:

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 describe un proveedor:

CampoTipoDescripción
appstr / stringNombre de la app proveedora
provider_app_keyint / numberLa clave de app del proveedor
stagesdict / objectCatálogo por stage { dev?, prod? }; un stage está presente solo si el proveedor tiene un backend de datos para él. Cada catálogo contiene las tables y transforms no privadas que comparte

En Go, cada entrada es un ironflock.ConsumedAppInfo con App, ProviderAppKey y Stages.Dev / Stages.Prod (un *StageCatalog con Tables y Transforms, nil para un stage que no existe); Catalog(stage) devuelve uno por su nombre.

Genera (Python) / lanza (JavaScript) / devuelve (Go) un CrossAppAccessError con code: NO_GRANT si tu app no posee ninguna concesión comodín.

connectToAllApps / connect_to_all_apps

Abre manejadores de solo lectura a todos los proveedores no privados del proyecto en una sola llamada (solo para consumidores comodín). Enumera los proveedores mediante listConsumableApps / list_consumable_apps y abre cada uno, omitiendo los que no tengan un backend de datos para el stage solicitado. Cada manejador se almacena en caché bajo la misma clave que connectToApp / connect_to_app, de modo que una llamada posterior a connectToApp(name) devuelve el manejador ya preparado. Los manejadores devueltos se cierran juntos cuando tu instancia se detiene.

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ámetroTipoDescripción
stagestr / string, opcionalStage del proveedor: "dev" o "prod". Por defecto, el stage de tu propia app
continue_on_error / continueOnErrorbool / boolean, opcionalCuando es true (el valor por defecto), un proveedor que no se puede abrir se reporta a on_error / onError y se omite del resultado. Cuando es false, el primer fallo se lanza
on_error / onErrorcallable, opcionalSe invoca con cada proveedor que no se pudo abrir (mientras continue_on_error / continueOnError sea true), y con un CrossAppAccessError si a una conexión ya abierta se le deniega después el acceso de forma definitiva (p. ej., si la concesión se revoca)

Devuelve los manejadores de proveedor abiertos correctamente (el mismo tipo de manejador que connectToApp / connect_to_app). Genera (Python) / lanza (JavaScript) / devuelve (Go) un CrossAppAccessError con code: NO_GRANT si tu app no posee ninguna concesión comodín.

En Python, stage, on_error y continue_on_error son argumentos con nombre; en JavaScript se pasan a través de un objeto de opciones (connectToAllApps({ stage, onError, continueOnError })); en Go, a través de un struct (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), donde OnError es una func(error) y StopOnError: true corresponde a continueOnError: false.

Almacenamiento de archivos gestionado

Cada backend de datos de app dispone de almacenamiento de objetos privado junto a sus tablas, accesible a través de la propiedad files (ifl.Files() en Go, un *filestore.FileStore que puede obtenerse antes de Start). Úsalo para imágenes, PDF, fotogramas de cámara, blobs de firmware — cualquier cosa que no pertenezca a una fila de una tabla. No requiere configuración alguna: una app sin sección files: en su plantilla de datos también obtiene un espacio de nombres llamado default (una plantilla que declara sus propios espacios de nombres lo reemplaza — consulta Espacios de nombres).

Esta sección es la referencia del lado del cliente. Para el lado de la plataforma — declarar espacios de nombres, cuotas y retención, compartir archivos entre apps, credenciales S3 por app y el comportamiento en appliances on-premises — consulta Almacenamiento de archivos.

La idea clave es que almacenar un objeto te devuelve una URL permanente que puedes escribir directamente en una columna de tabla, de modo que un widget de tablero pueda renderizarla sin ningún trabajo 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)

Esa URL nunca caduca, pero no es un enlace público: solo permanece legible para un solicitante autenticado que tenga acceso READ sobre este backend de datos, y un proxy de autenticación lo vuelve a comprobar en cada petición. Por eso es seguro almacenarla en la base de datos.

Espacios de nombres

Un espacio de nombres es un prefijo de clave que lleva asociada una política — retención, reglas de compartición, tipos de contenido permitidos. No es un bucket aparte; todos los espacios de nombres de una app viven dentro de la única área de almacenamiento de esa app. Declara uno solo cuando un conjunto de objetos necesite reglas distintas; en caso contrario quédate en default y organiza tus objetos con rutas de clave como 2026/03/part-1.jpg.

Declara los espacios de nombres en data-template.yml. Declarar cualquiera reemplaza el default implícito: incluye también uno llamado default, o toda llamada sin espacio de nombres fallará con 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 }

Ten en cuenta que el presupuesto se declara una sola vez para toda la app, no por espacio de nombres. Un espacio de nombres es solo un prefijo de clave dentro de la única área de almacenamiento de la app, así que no hay nada contra lo que aplicar un presupuesto por prefijo. maxObjectBytes sí es por espacio de nombres — limita un objeto individual, no un total: 100 MiB si no se establece, como máximo 5 GiB (OBJECT_TOO_LARGE por encima).

Todos los métodos siguientes aceptan el espacio de nombres como argumento opcional y, por defecto, usan default; un espacio de nombres vacío ("") también significa default.

Almacenar y leer 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étodoDescripción
put(key, data, …)Almacena un objeto (bytes en Python, Uint8Array en JavaScript, []byte en Go). Devuelve los metadatos del objeto, incluida su url
get(key, namespace?)Devuelve el contenido del objeto. Un objeto por encima del límite inline llega directamente del almacenamiento de objetos o, donde eso no es posible, por rangos de bytes a través del router (consulta Objetos grandes)
put_file(key, path, …) / get_to_file(key, path, …)Python y Go (PutFile / GetToFile; Go además hace streaming desde un io.Reader con PutReader y hacia un io.Writer con GetTo). Almacena desde un archivo local, o escribe en él. Usa streaming en la ruta de objetos grandes
delete(key, namespace?)Elimina un objeto
copy(key, to, …)Copia un objeto, opcionalmente a otro espacio de nombres
move(key, to, …)Copia y luego elimina. No es atómico — el servicio no tiene un verbo de movimiento, así que un borrado fallido deja ambas copias

JavaScript no tiene utilidades para rutas de archivo porque el paquete se distribuye con una única compilación para Node y el navegador — lee y escribe los archivos locales tú mismo con fs.

En Python, el trabajo con archivos de put_file y get_to_file se ejecuta fuera del event loop. get_to_file crea los directorios padre que falten y escribe el archivo in situ en cuanto tiene los primeros bytes del objeto: una lectura rechazada (no existe el objeto, no autorizado) deja la ruta intacta, mientras que una lectura que falla a mitad deja lo escrito hasta entonces. Un FIFO se escribe a medida que llega el objeto, y abrirlo espera a un lector, así que un FIFO que nadie lee bloquea la llamada (no el event loop); asyncio.wait_for puede abandonarla. put_file lee un FIFO hasta el final antes de almacenarlo.

En Go, GetToFile también crea los directorios padre que falten, pero descarga en un archivo temporal junto al destino y lo renombra en su lugar una vez completo: una descarga fallida o cancelada nunca deja un archivo parcial y conserva el anterior. Los enlaces simbólicos se siguen (se escribe el archivo al que apuntan, y los enlaces se mantienen); el reemplazo recibe, en la medida en que el proceso pueda, el propietario, el grupo y los bits de permiso del archivo anterior. Un FIFO o un dispositivo se escribe en su sitio a medida que llega el objeto. Para un destino junto al que no se puede crear un archivo nuevo, como uno en un sistema de archivos de solo lectura o en un pseudosistema de archivos, usa GetTo con un archivo que abras tú mismo.

put acepta: content_type / contentType (el tipo MIME; el espacio de nombres puede restringir cuáles se permiten) y namespace. En Python son argumentos con nombre; en JavaScript van en un objeto de opciones; en Go son opciones (filestore.ContentType(…), filestore.Namespace(…)).

Listar e inspeccionar

MétodoDescripción
list(…)Una página del listado del servicio de archivos, con estilo de carpetas por defecto: objects contiene los objetos cuyas claves continúan el prefijo sin una /, prefixes las carpetas que hay debajo. Un delimiter de "" lista en plano — todos los objetos bajo el prefijo, también en subcarpetas. Devuelve objects, prefixes, is_truncated / isTruncated / IsTruncated y un cursor que puedes volver a pasar para la página siguiente
iter(…) / iterate(…)Iterador asíncrono sobre todos los objetos bajo un prefijo, también en subcarpetas, paginando automáticamente. Se llama iter en Python, iterate en JavaScript e Iter en Go (un iter.Seq2[*ObjectInfo, error] para un bucle range)
stat(key, namespace?)Metadatos de un objeto sin transferir su contenido
exists(key, namespace?)Si un objeto existe o no
namespaces()Los espacios de nombres que esta app puede usar
usage(…)Cuánto almacenamiento está usando la app — véase más abajo
catalog()Espacios de nombres más los límites y cuotas emitidos por el servidor. Se obtiene una vez y se almacena en caché: catalog(refresh=True) / catalog(true) / RefreshCatalog(ctx) lo vuelve a obtener

Carpetas. Un prefijo que termina en / designa una carpeta: iter(prefix="2026/") / iterate({ prefix: "2026/" }) / Iter(ctx, filestore.Prefix("2026/")) devuelve 2026/a.txt y 2026/sub/c.txt, pero no 2026-notes.txt. list no puede abrir una carpeta por su nombre, porque el servicio de archivos rechaza un prefijo de listado que termine en / (FleetFiles v0.2 responde INTERNAL): list con el prefijo 2026 devuelve 2026-notes.txt entre los objetos y la carpeta 2026/ entre los prefijos. El servicio almacena las claves en la forma de normalización Unicode C, así que indica también el prefijo de carpeta en NFC; una iteración que encuentra una clave con otra forma del prefijo falla en lugar de omitir todas las claves.

Los objetos se describen con los mismos campos en los tres SDK, siguiendo el estilo de nomenclatura de cada lenguaje: namespace, key, size, etag, content_type / contentType, last_modified / lastModified, checksum_sha256 / checksumSha256 y url — en Go, los campos Namespace, Key, Size, ETag, ContentType, LastModified, ChecksumSHA256 y URL de filestore.ObjectInfo.

Uso de almacenamiento y cuota

usage responde desde el almacén de objetos en una sola llamada, así que los totales son exactos en lugar de sumados por el 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}

En Go, los campos son SizeBytes, ObjectCount, QuotaBytes, FreeBytes y PerNamespace de filestore.StorageUsage.

CampoSignificado
size_bytes / sizeBytesBytes almacenados actualmente
object_count / objectCountNúmero de objetos almacenados
quota_bytes / quotaBytesEl presupuesto aplicado. 0 significa ilimitado
free_bytes / freeBytesBytes restantes. -1 significa ilimitado — indicar 0 ahí se leería como “lleno”
per_namespace / perNamespaceBytes por espacio de nombres. Presente solo cuando pides el desglose detallado

El desglose por espacio de nombres está desactivado por defecto porque el almacén no puede responderlo directamente: lleva la contabilidad por área de almacenamiento, y un espacio de nombres es solo un prefijo, así que el SDK tiene que listar cada espacio de nombres para sumar los tamaños. Pídelo cuando lo necesites, no en una ruta crítica.

Aparecen dos cuotas distintas, y conviene no confundirlas. catalog() informa de ambas:

CampoSignificado
quota_bytes / quotaBytesLo que realmente se aplica, leído del almacén de objetos — el ajuste del usuario del proyecto
suggested_quota_bytes / suggestedQuotaBytesLo que pidió la plantilla de datos de la app. 0 si no pidió nada

En Go, Catalog(ctx) devuelve un *filestore.Catalog con QuotaBytes y SuggestedQuotaBytes.

Difieren siempre que un usuario ha subido o bajado el presupuesto de la app, y por eso el valor aplicado se lee del almacén y no de la plantilla — volver a desplegar la app no debe restablecer en silencio la elección de un usuario. Una interfaz puede mostrar ambos (“la app sugiere X, tú has establecido Y”). La aplicación efectiva siempre usa el primero. El usuario puede cambiar el presupuesto en cualquier momento, y catalog() lo informa tal como estaba en su última obtención: lee el valor actual con usage().

Compartir objetos

Hay dos tipos de enlace, y la diferencia importa:

MétodoVigenciaQuién puede leerlo
url(key, …)PermanenteSolo un solicitante autenticado con READ sobre este backend de datos — se vuelve a comprobar en cada petición. Seguro para almacenar en una columna de tabla
share_url / shareUrl / ShareURLCaduca (15 min por defecto, acotado por el servidor)Cualquiera que tenga el enlace. Nada vuelve a comprobar la autorización cuando se usa

share_url / shareUrl es una capacidad al portador: entrégalo a una persona que necesite acceso temporal, y no lo almacenes en la base de datos. Usa url para todo aquello que renderice un tablero.

url devuelve None / undefined / "" (Go) cuando el despliegue no tiene un borde HTTP (por ejemplo, un appliance de HTTP simple) — esa es la señal para recurrir a get. Pasar el etag de un objeto como argumento version (filestore.Version(etag) en Go) permite que los navegadores cacheen la respuesta de forma inmutable.

En Python y Go, cloud_url(key, …) / CloudURL(ctx, key, …) devuelve la URL permanente a través del túnel de archivos en la nube de un appliance (https://i<instanceKey>-files.<cloud-edge>): accesible desde internet mientras el appliance esté en línea, con la misma autorización que url. Devuelve None ("" en Go) en despliegues alojados en la nube (donde url ya es accesible desde internet), donde no hay borde HTTP, y mientras el uso compartido en la nube del backend de datos esté desactivado. El usuario puede cambiarlo en cualquier momento, así que cloud_url / CloudURL vuelve a leer el catálogo cuando el que tiene en caché tiene más de 30 segundos.

upload_url / uploadUrl / UploadURL emite una URL con caducidad que acepta una subida directa, y devuelve url, method, headers y expires_in / expiresIn (en Go, un *filestore.UploadTarget con URL, Method, Headers y ExpiresIn). Envía las cabeceras tal como se devuelven, para que el objeto se almacene con el tipo de contenido fijado al emitirla. Los tipos de contenido y el tamaño máximo del espacio de nombres, un size declarado y la cuota se comprueban cuando se emite la URL, no cuando se usa: la URL está firmada solo para el host, así que cualquiera que la tenga puede escribir cualquier contenido en su clave hasta que caduque. Al igual que share_url / shareUrl, es una capacidad al portador — una que escribe.

Objetos grandes

El SDK elige el transporte según el tamaño, de forma automática — no hay nada que configurar:

Tamaño del objetoCómo viaja
Hasta el límite inline (actualmente 6 MiB)Una única llamada a través del router de mensajes
MayorDirectamente al almacenamiento de objetos por HTTPS, sin pasar por el router
Mayor, en lecturas donde no hay ruta directaPor rangos de bytes a través del router de mensajes

El servidor informa del límite exacto en tiempo de ejecución como inline_max_bytes / inlineMaxBytes / InlineMaxBytes en catalog(), de modo que se puede subir sin publicar una nueva versión del SDK.

La ruta directa necesita un despliegue capaz de prefirmar URLs y un dispositivo que alcance el host del almacenamiento de objetos, no solo el router. Donde falta cualquiera de los dos — el catálogo indica que el despliegue no puede prefirmar (un appliance on-premises, por defecto), la emisión de la URL de lectura responde NOT_SUPPORTED, o el almacenamiento de objetos no es accesible antes de que llegue ningún byte —, get (y get_to_file en Python, GetTo y GetToFile en Go) leen en su lugar el objeto por rangos de bytes a través del router (FleetFiles v0.1.33 y posteriores), cada uno de como máximo el límite inline. Cada rango es una lectura propia: un objeto reemplazado durante una lectura así la hace fallar con INTERNAL (“object changed while it was read”). Las escrituras no tienen esa alternativa.

Quedan dos topes para las escrituras, y ambos reportan TOO_LARGE con un motivo que indica cuál de los dos has alcanzado:

  • 5 GiB — el límite de subida en una sola operación del almacén de objetos. La subida multiparte aún no está implementada.
  • El límite inline, allí donde no hay endpoint directo — un despliegue que no puede prefirmar (un appliance, por defecto) no puede almacenar en absoluto un objeto más grande. Ningún reintento ni fragmento más pequeño ayudará, y el mensaje lo dice.

Dos fallos de la ruta directa habituales en campo tienen sus propios códigos en lugar de parecer problemas de autorización: PRESIGN_UNREACHABLE (un proxy que solo permite el router — una lectura recurre a los rangos de bytes, una escritura falla) y CLOCK_SKEW (el almacén de objetos rechaza las peticiones desfasadas más de 15 minutos — revisa el NTP del dispositivo).

Errores de almacenamiento de archivos

Un fallo del servicio de archivos o de una transferencia directa genera (Python) / lanza (JavaScript) / devuelve (Go) un FileStoreError que lleva un code estable y un reason legible para humanos. Ramifica según code, nunca según reason. Los errores de archivos locales, una llamada cancelada y los errores WAMP del router distintos de los asignados abajo se lanzan tal cual.

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_AUTHORIZEDEl llamador no puede realizar esta operación
NO_SUCH_NAMESPACEEl espacio de nombres no está declarado en la plantilla de datos
NO_SUCH_OBJECTLa clave no existe
TOO_LARGESupera el límite de transferencia en una sola llamada
OBJECT_TOO_LARGESupera el maxObjectBytes propio del espacio de nombres
QUOTA_EXCEEDEDEl filestore está lleno
CONTENT_TYPE_NOT_ALLOWEDEl espacio de nombres restringe los contentTypes
INVALID_KEYUna clave o un prefijo mal formados (ver abajo; FleetFiles v0.2 todavía lo reporta como INTERNAL)
INVALID_RANGEUn rango de bytes fuera del objeto; los SDKs nunca piden uno
NOT_SUPPORTEDEl backend no puede hacer esto
NOT_AVAILABLEEste despliegue no tiene servicio de archivos — o todavía no: tras un reinicio de la plataforma, o mientras el servicio de archivos registra sus procedimientos. La llamada no se ejecutó en ningún sitio y puede repetirse
PRESIGN_UNREACHABLEEl almacenamiento de objetos no es accesible directamente (¿un proxy?)
CLOCK_SKEWEl reloj del dispositivo está demasiado desfasado
INTERNALCualquier otra cosa — también un objeto que cambió durante una lectura por rangos

Un servidor más nuevo puede introducir códigos que esta versión del SDK no conoce. Se propagan tal cual en code en lugar de colapsarse, así que trata un valor no reconocido como un fallo genérico.

Las llamadas de archivos esperan a la conexión (hasta 10 segundos en Python y Go, 6 en JavaScript) y no se reintentan: a diferencia de las operaciones de tabla, no sobreviven a un reinicio de la plataforma, así que durante uno una llamada puede fallar con NOT_AVAILABLE hasta que el servicio de archivos se haya vuelto a registrar. Una llamada así no se ejecutó en ningún sitio, por lo que tu app puede repetirla.

Claves. El servicio de archivos es la autoridad sobre las claves; los SDKs envían las claves y los prefijos tal cual. Una clave o un prefijo mal formados — vacíos; con una / inicial o final (salvo el prefijo de carpeta de iter / iterate); con un segmento vacío, . o .., o un segmento que termina en . o que empieza o termina con un espacio; con uno de \ : * ? " < > | ^ ~, un carácter de control o un carácter de formato Unicode; de más de 900 bytes, 20 segmentos o 255 bytes por segmento; o bajo el prefijo reservado _sys/ — se rechazan con INVALID_KEY en un servicio de archivos que clasifica los errores de clave, y con INTERNAL en FleetFiles v0.2. Cuidado con las marcas de tiempo: 2026-10-09T12:00:00Z no es un segmento de clave válido, por el :.

En Python, FileStoreError se importa desde ironflock.filestore; en JavaScript se exporta desde la raíz del paquete (import { FileStoreError } from "ironflock"); en Go es *filestore.Error (también ironflock.FileStoreError), con los códigos como constantes como filestore.CodeQuotaExceeded.

Comunicación entre dispositivos

registerDeviceFunction / register_device_function

Registra un procedimiento al que pueden llamar otros dispositivos que ejecutan la misma app (en el mismo stage), y también las acciones de los widgets del tablero. El SDK construye por ti la URI completa <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>, donde STAGE es DEV o PROD. El router no acepta ningún otro registro de una app, y solo registros individuales (consulta URIs que una app puede usar). El registro se restaura tras cada reconexión.

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

register() es un alias de register_device_function().

callDeviceFunction / call_device_function

Llama a un procedimiento registrado por otro dispositivo de la misma app. El SDK construye el topic WAMP completo automáticamente usando la clave del dispositivo de destino: <SWARM_KEY>.<device_key>.<APP_KEY>.<STAGE>.<topic>, la misma URI que registerDeviceFunction / register_device_function registra en el 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

Llama a un procedimiento remoto usando una URI WAMP completa. Una app no puede registrar un nombre como some.full.wamp.topic, así que un nombre así solo llega a procedimientos que los servicios de la plataforma registran en el realm de datos de la app, como los del backend de datos y los del almacenamiento de archivos. Para llamar a una función que registró otro dispositivo de tu app, usa callDeviceFunction / call_device_function.

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

URIs que una app puede usar

El contenedor de tu app se une a su propio realm de datos, realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>, con el rol de router app. El router comprueba allí cada publicación, suscripción, llamada y registro contra la lista de permitidos de ese rol. Los métodos propios del SDK se mantienen dentro de ella; las reglas importan cuando pasas tus propias URIs a publish, subscribe o call, o usas la connection directamente (Connection() en Go). Una acción rechazada falla con wamp.error.not_authorized (en Go, ironflock.WampURI(err) la devuelve).

URI en tu propio realmPermitidoUsado por
Tus propios nombres, p. ej. com.myapp.statuspublicar, suscribirse (también por prefijo), llamar — nunca registrarpublish, subscribe, call
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table>publicar — nunca suscribirsepublishToTable / publish_to_table, reportError / report_error, publishRowsToTable / publish_rows_to_table
transformed.<table>, transformed.bulk.<table>solo suscribirsesubscribeToTable / subscribe_to_table
databackend.errors.…, files.events…solo suscribirseLos eventos de error y de archivos del backend de datos
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>registrar (individual), llamar, publicar — nunca suscribirseregisterDeviceFunction / register_device_function, callDeviceFunction / call_device_function
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.…llamar (el SDK los llama)Métodos de tablas, historial, secretos y archivos
sys.appaccess.list, sys.appaccess.resolvellamarlistConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps
wamp.session.getllamarEl heartbeat del SDK de JavaScript

Nombres con comprobación de identidad. Los nombres que empiezan por un dígito — los topics de escritura de las tablas y las URIs de las funciones de dispositivo — pasan además la comprobación de identidad de la plataforma, que los vincula a la clave de swarm de tu realm. Un registro solo se acepta para una URI de función de tu propio dispositivo con la clave de swarm, la clave de app y el stage de tu realm; un registro por prefijo o comodín, solo cuando fija esos cuatro segmentos. Publicar y llamar solo requieren la clave de swarm de tu realm: una fila publicada en un topic de tabla con otra clave de app se acepta, pero no llega a ningún backend de datos y se pierde. Deja que el SDK construya estas URIs.

Nombres reservados. El router rechaza sys., salvo los dos procedimientos entre apps de arriba, y wamp., salvo wamp.session.get. Los demás nombres de la tabla pertenecen a la plataforma: no empieces tus propios nombres por un dígito, bulk., transformed., databackend.errors., files., append., appendBulk., history. ni secret.. Cualquier otro primer segmento es el espacio de topics propio de tu app.

Nadie se suscribe a los topics en bruto de las tablas. Transportan la fila exactamente como se envió, incluido el texto plano de las columnas secretas, que el backend de datos solo cifra al almacenar la fila. subscribeToTable / subscribe_to_table lee en su lugar las filas almacenadas desde transformed.<table>, tipadas y con las columnas secretas enmascaradas.

Los datos de otras apps son de solo lectura. En el realm de un proveedor, tu app (rol de router app_reader) solo puede suscribirse a transformed.… y files.events… y llamar a history.transformed.… y files.read.…, además del heartbeat wamp.session.get — sin publicar, sin registrar, sin escrituras y sin procedimientos de secretos.

Metadatos del dispositivo

setDeviceLocation / set_device_location

Pide a la plataforma que actualice la ubicación GPS del dispositivo, que los mapas de IronFlock muestran en tiempo real.

No disponible por el momento: todavía ningún servicio de la plataforma responde a esta llamada en el realm de datos de la app, así que falla con wamp.error.no_such_procedure. Mientras tanto, guarda las ubicaciones en una tabla propia.

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

El historial de ubicación no se almacena. Para rastrear la ubicación a lo largo del tiempo, crea una tabla dedicada y usa publish_to_table / publishToTable.

getRemoteAccessUrlForPort

Devuelve la URL bajo la que es accesible un puerto declarado en el port-template.yml de la app una vez que su túnel está activo.

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ámetroTipoDescripción
portint / numberEl puerto, tal como se declara en port-template.yml
protocolstr / string, opcionalEl protocolo del puerto: "http" (predeterminado), "https", "tcp" o "udp", sin distinguir mayúsculas y minúsculas; los espacios alrededor se ignoran, y uno vacío equivale a "http"
remote_port_environment / remotePortEnvironmentstr / string, opcionalSolo tcp/udp: la variable que contiene el puerto remoto, en lugar de REMOTE_PORT_FOR_<port> — el remote_port_environment propio del puerto en la plantilla, que necesitan los agentes de dispositivo anteriores a 0.19.8

Los puertos http y https se sirven bajo la etiqueta de túnel <device_key>-<app name, lower case>-<port> (los puertos https con el prefijo secure- de la plataforma) en el dominio de túnel que inyecta el agente del dispositivo (TUNNEL_DOMAIN), siempre como URL https://. En un dispositivo que pertenece a un appliance on-premises (el agente inyecta INSTANCE_KEY), se devuelve en su lugar la ruta reenviada a través de la nube https://i<INSTANCE_KEY>-<label>.<CLOUD_TUNNEL_DOMAIN>.

Una URL tcp o udp lleva el puerto público que asignó el túnel, que el agente del dispositivo anuncia como REMOTE_PORT_FOR_<port> (agente 0.19.8 y posteriores): <protocol>://<TUNNEL_DOMAIN>:<remote port>. En el dispositivo de un appliance se prefiere el puerto expuesto a internet, REMOTE_PORT_FOR_<port>_CLOUD, mientras el agente lo anuncie; antes de que aparezca, y de nuevo una vez que el agente lo ha eliminado (reenvío a través de la nube desactivado), se devuelve la URL local del appliance. Un puerto 0 todavía no se ha asignado.

Los valores se leen en vivo de /data/env, así que vuelve a llamar al método en lugar de guardar el resultado en caché. Hay casos que la URL no puede cubrir:

  • El agente del dispositivo solo crea túneles para los puertos de instalaciones de producción (PROD). En un contenedor de desarrollo (DEV), la URL http(s) es la de la instalación PROD de la misma app en este dispositivo, y tcp/udp no dan ninguna; accede a una app DEV a través de la dirección LAN del dispositivo (DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>).
  • En un dispositivo edge de un appliance instalado sin dominio (modo HTTP simple), el agente establece TUNNEL_DOMAIN en localhost, así que las URLs que se componen allí apuntan a localhost: un appliance así sirve los puertos de las apps en sus puertos del host, algo que una URL de túnel no puede expresar.
  • Los dominios de túnel no definidos o vacíos usan por defecto app.ironflock.com. La plataforma no establece CLOUD_TUNNEL_DOMAIN hoy en día: las URLs de un appliance usan app.ironflock.com salvo que la propia app establezca esa variable en su configuración de entorno.
  • En un contenedor que se ejecuta con un usuario distinto de root, los valores son aquellos con los que arrancó el contenedor (consulta Variables de entorno).

El método devuelve None / null (en Go, "" y false) cuando no se puede componer la URL: falta la clave del dispositivo o el nombre de la app, un puerto tcp/udp todavía no tiene puerto remoto anunciado (o es 0), el protocolo es desconocido, o la etiqueta no es una etiqueta DNS válida (más de 63 caracteres, o un nombre de app con un punto). En un navegador no hay entorno: la URL http(s) se compone sobre app.ironflock.com a partir de las opciones deviceKey y appName.

Propiedades de conexión y ciclo de vida

PropiedadTipoDescripción
is_connectedboolIndica si la conexión con la plataforma está activa
connectionCrossbarConnectionLa instancia de conexión subyacente (uso avanzado)
MétodoDescripción
run()Inicia la conexión y ejecuta mainFunc (bloqueante)
await start()Inicia la conexión de forma asíncrona
await stop()Detiene la conexión y las conexiones a apps consumidas, y cancela tareas en ejecución. Las conexiones abandonan sus realms y no se reconectan después; start() vuelve a conectar. Un start() o connect_to_app que siga esperando falla (consulta Fiabilidad de la conexión)
await run_async()Inicia y mantiene la conexión de forma asíncrona

Fiabilidad de la conexión

El SDK se reconecta por sí solo. Cuando la conexión se cae, reintenta hasta que el router vuelve a estar disponible y entonces restaura todas las suscripciones y todas las funciones de dispositivo registradas: no necesitas volver a suscribirte tras una reconexión. Si una no se puede restaurar, las demás sí, y la que falló se reintenta mientras dure la sesión — tras 1 segundo, duplicando hasta 30 segundos, con cada espera variada hasta un 15 % — y de nuevo tras cada reconexión: el router la aceptó antes, así que su rechazo es pasajero, como cuando la comprobación de identidad de una función de dispositivo por parte de la plataforma falla cerrada por un momento. El primer fallo se registra como advertencia, y la restauración se registra en cuanto tiene éxito.

Las operaciones de tabla — escrituras, lecturas de historial, de series y de secretos, y las lecturas de historial de una app consumida — también sobreviven a un reinicio de la plataforma, como una actualización del router o de la plataforma. Esperan hasta reconnect_window (Python, en segundos) / reconnectWindowMs (JavaScript) / WithReconnectWindow (Go, un time.Duration), 60 segundos por defecto, a que vuelva la conexión, y reintentan mientras el backend de datos todavía no sirva sus procedimientos: el router responde wamp.error.no_such_procedure, o el backend de datos rechaza una llamada que se adelantó a su registro con “client has no handler for registration”. Una llamada así nunca llegó a un handler, así que un reintento nunca escribe una fila dos veces; cualquier otro error se lanza tal cual. Las llamadas de archivos no se reintentan (consulta Errores de almacenamiento de archivos). Una petición ya enviada cuando se cierra su sesión (el router la cierra, se cae el enlace o stop()) falla de inmediato en lugar de esperar una respuesta, y no se reintenta: puede que haya llegado a su handler. En Go, una petición así — y cualquier operación que no encuentre conexión dentro de su espera — falla con un error que envuelve wamp.ErrNotConnected (wamp.ErrStopped después de Stop). Las demás operaciones de Go (Publish, Subscribe, Call, las funciones de dispositivo, el descubrimiento entre apps) esperan hasta 10 segundos a una conexión y no se reintentan.

Un caso especial es un router accesible que todavía no tiene un realm para la app. En la carrera de instalación concurrente, el contenedor de la app arranca antes de que el backend de datos haya aprovisionado su realm, por lo que los primeros intentos de unión se rechazan con wamp.error.no_such_realm; el SDK sigue reintentando cada pocos segundos y la app arranca en cuanto existe el realm. Un realm que sigue faltando después de un minuto muy probablemente no aparecerá nunca — la app fue eliminada o no tiene backend de datos para este stage —, así que a partir de entonces el SDK reduce el ritmo a un intento cada dos minutos en lugar de golpear el router cada segundo para siempre. El ritmo rápido vuelve en cuanto aparece el realm, o cuando el propio router se cae y regresa.

stop() pone fin a toda espera de una conexión. Un start() que todavía espera unirse por primera vez a su realm — uno que aún no existe, por ejemplo — falla con Connection to realm <realm> stopped before it opened. Un connect_to_app / connectToApp que todavía está abriendo la conexión a una app proveedora falla con Connection to app '<app>' (<stage>) aborted by stop() o, si la conexión se une a su realm de todos modos, esta se cierra y la llamada falla con Connection to app '<app>' (<stage>) closed by stop(). Una denegación que llega a la vez que stop() conserva su CrossAppAccessError. Cuando stop() termina, ya no queda nada intentando conectarse; un router que no responde cuando la conexión abandona el realm queda desconectado tras 5 segundos.

En Go, Stop(ctx) también termina estas esperas, con errores que envuelven wamp.ErrStopped: un Start que siga esperando lo devuelve, y un ConnectToApp que siga abriendo la conexión falla con ironflock: connection to app '<app>' (<stage>) aborted by Stop (o closed by Stop, cuando la conexión se unió de todos modos). Stop espera la salida del realm dentro de ctx y luego cierra el socket; Run le da 10 segundos.

El caso más difícil es una conexión que muere en silencio: un NAT o un proxy la corta por inactividad sin avisar a ninguno de los dos extremos, o un router se reinicia detrás de un balanceador de carga. No llega nada que señale la pérdida. Una app que solo publica se entera en su siguiente escritura fallida, pero una app que solo se suscribe nunca escribe, así que sin ayuda se queda muerta hasta que el contenedor se reinicia.

Los tres SDK lo detectan. La detección está activa por defecto y no requiere configuración.

El SDK de Python usa keepalive de WebSocket: envía un ping al router cada 30 segundos mientras el enlace está inactivo y espera un pong en 10 segundos. Si no llega, la conexión se descarta y se restablece. El tráfico entrante cuenta como prueba de vida, por lo que una conexión con actividad nunca se sondea: el ping solo sale cuando el enlace realmente se ha quedado en silencio.

Las conexiones entre apps abiertas con connectToApp / connect_to_app normalmente solo se suscriben, que es justo el caso que esto protege. Están cubiertas automáticamente, y además se reconectan después de que el router las rechace, hasta que el rechazo persiste (consulta connectToApp).

Manejo de errores

Todos los métodos del SDK fallan de forma explícita: ante argumentos inválidos, una conexión perdida o un rechazo de la plataforma, lanzan una excepción (Python), rechazan la promesa (JavaScript) o devuelven un error (Go) con un mensaje que indica la operación, el topic y el motivo. Nada se descarta en silencio, así que envuelve en un bloque try las llamadas que quieras que sobrevivan — en Go, comprueba el error devuelto.

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}")

En JavaScript, los fallos que provienen de la plataforma son instancias de WampError — una subclase normal de Error que además lleva la URI de error WAMP en error y la carga útil del error en args / kwargs. Todo lo demás (parámetros inválidos, sin conexión) es un Error normal.

En Go, una operación fallida devuelve un *ironflock.OperationError cuyo mensaje indica la operación y la causa, que envuelve; errors.Is y errors.As ven a través de él. Un rechazo del router o de un destinatario de la llamada es un *ironflock.WampError (*wamp.Error) con URI, Args y Kwargs; ironflock.WampURI(err) devuelve la URI, o "". Otros errores centinela son ironflock.ErrMissingConfig (falta un dato de identidad que la operación necesita), ironflock.ErrAlreadyStarted y wamp.ErrStopped (después de Stop).

Migración: las versiones anteriores de los SDK de Python y JavaScript registraban un mensaje y devolvían None / null cuando una llamada fallaba. Ahora lanzan un error en su lugar, por lo que el código con la forma if result is None: ya no detecta los fallos — usa try / except (o try / catch).

Rechazos del backend de datos

Una operación que el backend de datos rechaza falla con una de las URIs de error WAMP de abajo, con el motivo como primer argumento. Los tres SDK las exportan como constantes; Go las nombra al estilo de Go (ironflock.URIRateLimited, ironflock.URIStorageFull, …). En Python, los métodos de IronFlock lanzan un RuntimeError cuya causa es el ApplicationError de autobahn (las lecturas de una app consumida lanzan directamente el ApplicationError), y wamp_error_uri(err) lee la URI de cualquiera de los dos. En JavaScript, el error es un WampError cuyo error es la URI. En Go, es un *ironflock.OperationError que envuelve un *ironflock.WampError, e ironflock.WampURI(err) lee la URI. Los SDKs no reintentan ninguno de ellos. Una fila publicada que el backend de datos rechaza se descarta sin error — solo los métodos de append lo informan.

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
ConstanteRechazado
URI_RATE_LIMITEDrevealSecrets / reveal_secrets por encima de 30, verifySecret / verify_secret por encima de 120 llamadas por minuto por credencial de app en un dispositivo
URI_RESULT_TOO_LARGEUna lectura con una sola fila por encima del presupuesto de mensaje de 8 MiB, o un resultado por encima de la protección de 1 GiB (los demás resultados grandes llegan en fragmentos, que el SDK vuelve a ensamblar). Sus argumentos con nombre indican las columnas más grandes: reduce limit, acota timeRange, selecciona menos columns o recorta con columnPaths
URI_INVALID_LIMITUna lectura de un transform con un limit superior a 3000
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPSConsultas de series que la tabla no puede responder: sin inicio, o un fin que no es posterior a él; una métrica que la tabla no puede agregar (no existe la columna, una columna secreta, un método que el tipo de la columna no admite, tsp con un método distinto de COUNT); una columna de groupBy por la que no puede agrupar; más de 50.000 filas
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMNUn filtro sobre una columna secreta; verifySecret / verify_secret sobre una columna que no es secreta, o revealSecrets / reveal_secrets de una tabla sin columnas secretas
URI_STORAGE_FULLUn append mientras el disco del appliance está casi lleno (por debajo de unos 3 GiB libres; se vuelve a aceptar a partir de 4 GiB) — temporal: añade las filas más tarde
URI_STORAGE_OVERUSAGEUn append que supera la asignación de almacenamiento de la cuenta; reintentar pronto no ayuda
URI_ENTITY_KEY_CONFLICTUna fila cuya clave de entidad (maintainLatestFlagFor) y tsp ya tiene otra fila, en el lote o almacenada: da a cada fila su propio tsp
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTEDUna escritura que conserva el valor anterior de un secreto (__secret__) sin una clave de entidad con la que encontrarlo; una escritura de un valor ya cifrado

Un append que el backend de datos rechaza sin motivo — wamp.error.runtime_error, su respuesta a una fila sin tsp — no es un rechazo tipado: el mensaje de error del SDK apunta al tsp que falta y a la tabla error-logs de la app, donde el backend de datos registra la causa.

Uso en navegador (solo JavaScript)

El SDK de JavaScript funciona en navegadores modernos. Como los navegadores no tienen variables de entorno, pasa toda la configuración a través del constructor:

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 }]);

Usa IronFlock.fromServer() para obtener la configuración desde tu backend en lugar de codificar las credenciales:

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

Tu endpoint del backend debe devolver un objeto JSON con las opciones de conexión (serialNumber, deviceKey, appName, swarmKey, appKey, env).

Registro de funciones para agentes de IA

El SDK puede registrar funciones que son invocables por agentes de IA. Registra un procedimiento y referencia su topic en tu 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)

El agente de IA puede entonces llamar a esta función cuando un usuario haga una pregunta que requiera datos de sensores en tiempo real.

Para conectar el topic WAMP registrado con un agente de IA, referencíalo en el archivo .ironflock/ai-template.yml de tu app:

sensor_agent: tool_description: | Delega a este agente cuando el usuario pregunte sobre lecturas de sensores, datos en vivo del dispositivo o condiciones ambientales actuales. system_prompt: | Eres un especialista en datos de sensores. Usa get_current para obtener la lectura más reciente de cualquier sensor. Incluye siempre la unidad en tu respuesta. main: true max_context_tokens: 30000 max_iterations: 5 tools: get_current: description: Devuelve la lectura más reciente de un sensor. topic: sensors.get_latest parameters: sensor_id: type: string description: El identificador del sensor a consultar. required: true

El valor de topic (sensors.get_latest) debe coincidir con el nombre indicado en register_device_function / registerDeviceFunction / RegisterDeviceFunction en el código edge. IronFlock enruta automáticamente la llamada al dispositivo donde está registrada la función.

Para la referencia completa de ai-template.yml, consulta Definición de agentes y herramientas.

Variables de entorno

Estas variables son establecidas automáticamente por el runtime de IronFlock dentro de los contenedores de apps:

VariableDescripción
DEVICE_NAMENombre visible del dispositivo
DEVICE_SERIAL_NUMBERIdentificador único e inmutable del dispositivo
DEVICE_KEYClave del dispositivo para autenticación
SWARM_KEYIdentificador del proyecto
APP_KEYIdentificador de la app
APP_NAMENombre de la app
ENVEntorno: DEV o PROD
APP_AUTH_ID, APP_AUTH_SECRETLa credencial propia de la app, que el SDK presenta al router. El acceso entre apps la requiere; sin ella (un agente del dispositivo más antiguo), el SDK recurre al número de serie del dispositivo
import os device_name = os.environ.get("DEVICE_NAME") serial = os.environ.get("DEVICE_SERIAL_NUMBER") project_key = os.environ.get("SWARM_KEY")

Las variables de acceso remoto (TUNNEL_DOMAIN, DEVICE_LAN_IP, DEVICE_PORT_FOR_<port>, REMOTE_PORT_FOR_<port>, …) se enumeran en Configuración de puertos.

El agente del dispositivo también refleja estos valores en /data/env, un archivo <NAME>.txt por cada uno, y mantiene ese reflejo actualizado mientras la app se ejecuta; los SDKs lo leen primero, de modo que los valores que cambian en tiempo de ejecución, como un puerto de acceso remoto, llegan a una app en ejecución. La credencial propia de la app se vuelve a leer en cada intento de conexión, así que una credencial que el agente rote mientras la app se ejecuta se presenta en la siguiente reconexión. El agente hace que /data/env solo sea legible por root: en un contenedor que se ejecuta con otro usuario, el SDK ve los valores con los que arrancó el contenedor y registra una advertencia una sola vez.

Last updated on