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.
| SDK | Paquete | Requisitos |
|---|---|---|
| Python | ironflock en PyPI | Python 3.11+ |
| JavaScript | ironflock en npm | Node.js 22+ o navegador moderno |
| Go | github.com/ironflock/sdk-go | Go 1.27.2+ |
Esta página es la guía de los tres SDK. El README de cada SDK en GitHub es su referencia completa: IronFlock/sdk-py, IronFlock/sdk-js e IronFlock/sdk-go.
Instalación
Python
pip install ironflockO añade ironflock al archivo requirements.txt de tu app.
Inicio rápido
Python
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
Python
ironflock = IronFlock(
mainFunc=main, # async function to run after connecting
serial_number="abc123", # override device serial (optional)
reconnect_window=60, # seconds an operation rides out a platform restart (optional)
)| Parámetro | Descripción |
|---|---|
mainFunc | Una función asíncrona (async def) que se ejecuta una vez establecida la conexión. Cuando retorna o lanza una excepción, run() y run_async() detienen la conexión (consulta Propiedades de conexión y ciclo de vida) |
ironFlockUrl | La URL WebSocket del router, p. ej. wss://cbw.ironflock.com/ws-ua-usr. Sin ella, el SDK toma la URL del entorno (consulta Variables de entorno) |
serial_number | Sobrescribe 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_window | Segundos que las operaciones de tabla, publish, subscribe y register_device_function esperan 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). IronFlock() lanza un ValueError ante un valor negativo, NaN o un infinito, y un TypeError ante un valor que no es un número |
Publicación de datos
publishToTable / publish_to_table
Escribe 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.
Es lo mismo que appendToTable / append_to_table: llama al procedimiento de append de la tabla, así que sobrevive a un reinicio de la plataforma — espera a la conexión como máximo durante la ventana de reconexión y reintenta mientras el backend de datos todavía no sirva la tabla — y lanza una excepción (rechaza la promesa, devuelve un error) cuando el backend de datos rechaza la fila. No se pierde ninguna fila mientras la plataforma se reinicia.
Hasta la versión 1.9.1, los tres SDK publicaban en cambio la fila en el topic de escritura de la tabla: una fila publicada mientras el backend de datos se reiniciaba se perdía, y una fila rechazada se descartaba sin error. Ahora una fila así hace fallar la llamada — por ejemplo, una fila sin
tsp, o una tabla que falta en la plantilla de datos (tras la ventana de reconexión).
Python
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. Rechaza una fila sin tsp, como se describe en appendToTable / append_to_table.
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
Nonese envía como null y borra la columna. Elimina la clave del dict para conservar el valor anterior. - JavaScript: una propiedad cuyo valor es
undefineddesaparece del payload, así que conserva el valor anterior. Asígnalenullpara borrar la columna. - Go: un campo de struct con la etiqueta
omitemptyuomitzerose omite cuando está vacío, así que conserva el valor anterior. En otro caso, un campo punteronil, igual que un valornilen unironflock.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: una fila rechazada es un error. publishToTable / publish_to_table hace lo mismo; la única diferencia es el nombre que el mensaje de error da a la operación.
Python
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
Escribe 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. Es lo mismo que appendRowsToTable / append_rows_to_table: llama al procedimiento de append masivo, sobrevive a un reinicio de la plataforma igual que publishToTable y falla cuando el backend de datos rechaza el lote.
Python
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 — hace fallar la llamada, y no se almacena ninguna de sus filas. (Hasta la versión 1.9.1, los SDK publicaban el lote en el topic de inserción masiva de la tabla, donde un lote rechazado se descartaba sin error.)
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. publishRowsToTable / publish_rows_to_table hace lo mismo. Cada fila necesita su propio tsp; los rechazos son los de appendToTable, incluida la pista para un lote rechazado sin motivo.
Python
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.
Python
# Writes a row to the error-logs table
await ironflock.report_error("Sensor read timed out", level="warning")
# Pass an exception to capture its traceback (falls back to the message)
try:
risky_operation()
except Exception as err:
await ironflock.report_error(err)
# Show the operator a friendlier line than the raw error
await ironflock.report_error(
"Calibration failed: offset 0.42 out of range",
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ámetro | Tipo | Descripción |
|---|---|---|
error | str / string o excepción / Error | El mensaje de error, o una excepción cuyo traceback/stack (o mensaje) se registra |
level | str / string, opcional | Severidad: "error", "warning", "info" o "debug", los tipos que muestra el widget de toast del tablero. Por defecto "error". "warn", como se escribía hasta el SDK 1.9.1, se envía como "warning" |
append | bool / boolean, opcional | Se mantiene por compatibilidad: la fila se añade de todos modos, ya que tanto publishToTable como appendToTable añaden. Solo elige a cuál de los dos nombra el mensaje de error |
tsp | str / string, opcional | Sobrescritura de marca de tiempo ISO-8601. Por defecto la hora actual |
user_message / userMessage | str / string, opcional | Texto 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})), dondeerrOrMsges unerroro una cadena,Leveluno deironflock.LevelError,LevelWarning,LevelInfo,LevelDebug, y el*Resultdevuelto es el resultado de la inserción.
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.
Al igual que las escrituras en tablas, publish añade los metadatos del dispositivo a los argumentos con nombre: DEVICE_SERIAL_NUMBER, DEVICE_KEY y DEVICE_NAME. Si pasas tú mismo un argumento con nombre que se llame igual que uno de ellos, prevalece el tuyo.
Sin conexión, publish espera a una como máximo durante la ventana de reconexión (60 segundos por defecto, consulta Fiabilidad de la conexión), así que una publicación sobrevive a un corte breve. Después se envía una sola vez, sin reintentos. El router confirma un mensaje al que nadie está suscrito, así que un suscriptor que esté caído entretanto se lo pierde.
Python
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.
Python
# 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:
| Campo | Tipo | Descripción |
|---|---|---|
limit | int / number | Máximo de filas a devolver (1–10.000, obligatorio; 1–3.000 para un transform) |
offset | int / number | Desplazamiento para paginación |
timeRange | list / 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 |
filterAnd | list / array | Entradas de filtro combinadas con AND: condiciones, grupos y/o el marcador latest (ver abajo) |
columns | list / array | Columnas a devolver (opcional). tsp, device_key y authid siempre se incluyen; omítelo para obtener todas las columnas |
columnPaths | list / array | Recorta 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 |
Si pasas parámetros de consulta, limit es obligatorio; sin ninguno (nil en Go), getHistory lee las 10 filas más recientes.
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. JavaScript exporta esta lista como SQL_OPERATORS; en Go, la devuelve ironflock.SQLOperators().
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":
Python
# 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. El SDK de Python también rechaza una cadena que datetime.fromisoformat no puede leer. 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.
Python
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:
| Campo | Tipo | Descripción |
|---|---|---|
metrics | list / array | Los 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 |
bucketMs | int / number | Ancho del intervalo en milisegundos enteros, al menos 1000 (opcional). Omítelo para dividir el rango de tiempo en limit intervalos |
limit | int / number | El presupuesto de intervalos (1–10.000) — no un límite de filas |
timeRange | list / 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 |
groupBy | list / array | Columnas que dividen cada intervalo en una fila por grupo (opcional) |
filterAnd | list / array | Condiciones 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
metricscomo nombres de columna más unmethodde 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 (unDeprecationWarningen 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 leyendorow["temperature"]— salvo que la fila ya tenga una columna con ese nombre, comotspparaCOUNTdetsp. Pasa a métricas{ref, method}y leerow["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.
Python
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)subscribeToTable / subscribe_to_table devuelve un TableSubscription, el manejador de sus dos flujos: su unsubscribe() los quita y pone fin a la puesta al día que se describe a continuación (consulta unsubscribe / unregister).
Las filas almacenadas mientras la conexión estaba caída llegan tras la reconexión. El router no vuelve a entregar lo que se publicó mientras la conexión estaba caída, así que tras cada reconexión el SDK lee las filas almacenadas entretanto y las entrega a tu handler, de la más antigua a la más reciente, antes que las filas que llegan en vivo: todas las filas cuyo tsp es posterior al de la fila más reciente que recibió tu handler. Antes de la primera fila, el punto de partida es la fila más reciente almacenada cuando te suscribiste: el SDK la lee justo después de suscribirse y, si esa lectura falla, parte del momento de la suscripción (según el reloj del dispositivo); una tabla que entonces estaba vacía se lee desde el principio. Una fila escrita entretanto con un tsp más antiguo (por ejemplo, un dispositivo que envía lecturas que tenía en su búfer) no entra en la puesta al día. Cada puesta al día entrega como máximo las 10.000 filas más recientes y registra una advertencia cuando ha leído esa cantidad. La lectura de la puesta al día espera al backend de datos como cualquier lectura de historial, como máximo durante la ventana de reconexión, y las filas en vivo esperan a que termine; si falla, las filas almacenadas entretanto no se entregan y se registra una advertencia. Las filas de la puesta al día proceden de una lectura, no de eventos, así que el handler recibe cada una sin los detalles del evento: en Python, solo la fila; en JavaScript, ([row], {}); en Go, un Event con Topic y Args, que se entrega por turnos en el DeliveryGroup de la suscripción. La puesta al día forma parte del SDK desde la versión 1.10.0.
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. Un transform no necesita puesta al día: su siguiente ejecución tras una reconexión vuelve a publicar la vista completa. 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.
Python
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).
Python
def on_alert(*args, **kwargs):
print("Alert received:", args, kwargs)
alerts = await ironflock.subscribe("com.myapp.alerts", on_alert)
# Later: the handler is called for no further event
await alerts.unsubscribe()subscribe devuelve un Subscription, el manejador de la suscripción (consulta unsubscribe / unregister). Cada llamada devuelve su propio manejador, también para un topic al que ya estás suscrito.
Sin conexión, subscribe espera a una como máximo durante la ventana de reconexión (60 segundos por defecto), igual que publish, y lo mismo hace subscribeToTable / subscribe_to_table (consulta Fiabilidad de la conexión).
El topic se compara de forma exacta. Las opciones de suscripción, un argumento opcional después del handler, pueden cambiarlo: un match de "prefix" suscribe a todos los topics que empiezan por el indicado. En Python, las opciones son las SubscribeOptions de autobahn (SubscribeOptions(match="prefix"), de autobahn.wamp.types); en JavaScript, un objeto ({ match: "prefix" }), y en Go, ironflock.SubscribeOptions{Match: "prefix"}. La suscripción se restaura con las mismas opciones tras cada reconexión. subscribeToTable / subscribe_to_table acepta las mismas opciones y suscribe con ellas sus dos flujos.
unsubscribe / unregister
subscribe, subscribeToTable / subscribe_to_table y registerDeviceFunction / register_device_function devuelven cada uno un manejador (handle). Un manejador sigue siendo válido a través de las reconexiones hasta que lo quitas: el SDK restaura lo que representa tras cada reconexión.
| Manejador | Devuelto por | Miembros |
|---|---|---|
Subscription | subscribe | unsubscribe(), topic, active |
TableSubscription | subscribeToTable / subscribe_to_table, de tu propia app y de una app consumida | unsubscribe(), table, active |
Registration | registerDeviceFunction / register_device_function y sus alias | unregister(), procedure, active |
topic, table y procedure nombran lo que representa el manejador; procedure es la URI WAMP completa. active indica si la suscripción o el registro están activos ahora en el router: es falso mientras la conexión está caída, tras una restauración fallida hasta que un reintento tiene éxito, y una vez que se ha quitado el manejador. Un TableSubscription está activo mientras lo estén sus dos flujos. ironflock.unsubscribe(handle) acepta un Subscription o un TableSubscription, e ironflock.unregister(registration) acepta un Registration; cada uno hace lo mismo que el método propio del manejador.
Python
status = await ironflock.subscribe("com.myapp.status", on_status)
readings = await ironflock.subscribe_to_table("sensordata", on_row)
add = await ironflock.register_device_function("com.myapp.add", add_numbers)
print(readings.table, readings.active) # sensordata True
await status.unsubscribe() # the same as: await ironflock.unsubscribe(status)
await ironflock.unsubscribe(readings) # both feeds of the table, and its catch-up
await ironflock.unregister(add) # the same as: await add.unregister()unsubscribe() y unregister() actúan de inmediato, en el momento en que las llamas, y devuelven un awaitable: espéralo con await para saber que el router ha respondido; un rechazo se lanza ahí. Un handler que es una función normal, no async, puede llamar a subscription.unsubscribe() sin esperar su resultado. Las tres clases de manejador se exportan desde ironflock.
En los tres SDK, quitar una suscripción o un registro sigue las mismas reglas:
- Surte efecto de inmediato. El handler ya no se llama para ningún evento más: ni para uno que siga en cola, ni para las filas restantes de una inserción masiva, ni para las filas que entregaría una puesta al día. Una función quitada ya no se llama; una llamada que el router todavía le enrute antes de haber descartado el registro se rechaza con
wamp.error.no_such_procedure. No se espera a una llamada que ya está en curso, así que un handler puede quitar su propia suscripción. Lo que quitas nunca se restaura tras una reconexión. - No espera a una conexión. Mientras la conexión está caída, la suscripción o el registro se quita localmente, porque el router lo descartó con la sesión, y la llamada vuelve de inmediato. Mientras la conexión está activa, se avisa al router, y la llamada termina en cuanto el router ha respondido. Un rechazo se lanza (Python, un
RuntimeError; JavaScript, unWampError) o se devuelve (Go), pero la suscripción o el registro quedan quitados de todos modos. - Es idempotente. Una segunda llamada no hace nada, y tampoco una llamada sin manejador (
None,nulloundefined,nil). Después destop()/Stop, una llamada solo quita la suscripción o el registro localmente: en Python y JavaScript, el siguientestart()ya no lo restaura. - Los manejadores son independientes. Quitar un manejador nunca quita el handler de otro, aunque sea del mismo topic o de la misma tabla. La suscripción en el router termina con el último manejador de su topic.
- Un
TableSubscriptionquita sus dos flujos,transformed.<table>ytransformed.bulk.<table>, y pone fin a su puesta al día.
Al actualizar: antes de la versión 1.10.0, los SDK de Python y JavaScript devolvían los objetos de suscripción y de registro de autobahn, y
subscribe_to_table/subscribeToTabledevolvía solo la suscripción del flujo de filas, así que al quitarla el flujo masivo seguía suscrito. El código que llama aunsubscribe()ounregister()sobre el valor devuelto sigue funcionando, y lo que quita ya no se restaura tras una reconexión. Los demás miembros de los objetos de autobahn, comoid, ya no existen.
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 lectura | Qué aspecto tiene una columna secreta |
|---|---|
getHistory, subscribeToTable / subscribe_to_table y las lecturas de una app consumida (las lecturas de historial y las filas en tiempo real que también usan los tableros) | el marcador de posición __secret__, exportado como SECRET_PLACEHOLDER (ironflock.SecretPlaceholder en Go) |
| SQL (la consola de consultas SQL, las exportaciones de datos y el inicio de sesión de Postgres de FleetDB Access) y las copias de seguridad de la base de datos | el texto cifrado almacenado, ifsec:1:… |
revealSecrets / reveal_secrets | el 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.
Solo una columna string puede ser secreta, y ni tsp ni una clave de entidad pueden serlo. El backend de datos cifra cada valor con AES-256-GCM y un IV aleatorio propio, así que el mismo secreto se almacena como un texto cifrado distinto en cada fila: una columna secreta no se puede filtrar, agrupar ni ordenar. Un filtro sobre una de ellas se rechaza con URI_SECRET_COLUMN; para comprobar un valor, usa verifySecret.
Python
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.
Python
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, salvo que 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. Sin parámetros de consulta, la llamada lee las 10 filas más recientes. A diferencia de getHistory, devuelve las filas de la más reciente a la más antigua.
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. Un filtro sobre una columna secreta se rechaza con URI_SECRET_COLUMN. El backend de datos solo sirve esta función para las tablas que declaran una columna secreta: para cualquier otra tabla, el router responde wamp.error.no_such_procedure, que el SDK reintenta durante la ventana de reconexión, y después la llamada falla con un error que indica que el procedimiento no está registrado. El error indica las causas, la más probable primero: la tabla no declara ninguna columna secreta (secret: true en la plantilla de datos), la tabla no está declarada, o el backend de datos es anterior a las columnas secretas.
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.
Python
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ámetro | Tipo | Descripción |
|---|---|---|
tablename | str / string | La tabla contra la que se comprueba |
column | str / string | La columna secreta a comparar |
candidate | str / string | El texto en claro que se está probando. Se compara en el servidor y nunca se escribe en la tabla |
query_params / queryParams | SecretQueryParams | Qué 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; en una tabla sin ninguna columna secreta, la llamada falla igual que revealSecrets / reveal_secrets en esa tabla.
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 por stage, 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. El subscribeToTable / subscribe_to_table del manejador devuelve el mismo manejador TableSubscription que para tus propias tablas, que ironflock.unsubscribe también acepta (consulta unsubscribe / unregister), y esas suscripciones a tablas se ponen al día de la misma manera tras una reconexión (consulta subscribeToTable).
El manejador también lleva el nombre y el stage del proveedor (app y stage; en Go, App y Stage) y su conexión al realm del proveedor: is_connected / isConnected / IsConnected() indica si está abierta, y connection / Connection() es la propia conexión. En JavaScript, la promesa de close() se resuelve en cuanto la conexión se ha cerrado, en un plazo de 5 segundos.
Python
# 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ámetro | Tipo | Descripción |
|---|---|---|
app_name / appName | str / string | Nombre de la app proveedora, tal como se declara en tu sección consumes: |
stage | str / string, opcional | Stage del proveedor: "dev" o "prod". Por defecto, el stage de tu propia app |
on_error / onError | callable, opcional | Se 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). Un subscribeToTable / subscribe_to_table de una app consumida que el router del proveedor rechaza (wamp.error.not_authorized, wamp.error.authorization_failed o wamp.error.authentication_failed) también falla con un CrossAppAccessError con NOT_AUTHORIZED, igual que una lectura rechazada.
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,
stageyon_errorson 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})), dondeOnErrores unafunc(*ironflock.CrossAppAccessError). En Go,ctxsolo acota la espera de quien llama: las llamadas simultáneas para la misma app comparten un intento, yClose(ctx)cierra un manejador. Go compara los nombres de app sin distinguir mayúsculas y minúsculas, yConsumedApp.Stagees el stage en minúsculas ("dev"o"prod"), a diferencia deifl.Stage().
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.ymlde tu app, y entrecomilla el*— un*sin comillas es un alias de YAML y no se parseará:
consumes:
- app: "*"Python
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:
| Campo | Tipo | Descripción |
|---|---|---|
app | str / string | Nombre de la app proveedora |
provider_app_key | int / number | La clave de app del proveedor |
stages | dict / object | Catá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.
Python
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ámetro | Tipo | Descripción |
|---|---|---|
stage | str / string, opcional | Stage del proveedor: "dev" o "prod". Por defecto, el stage de tu propia app |
continue_on_error / continueOnError | bool / boolean, opcional | Cuando 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 / onError | callable, opcional | Se invoca con el error de 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_errorycontinue_on_errorson 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})), dondeOnErrores unafunc(error)yStopOnError: truecorresponde acontinueOnError: 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). La propiedad no realiza ninguna operación de E/S, así que en los tres SDK puede leerse antes de iniciar la conexión. Ú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:
Python
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 comprueba en cada petición y guarda en caché su decisión durante un máximo de 5 minutos. 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
Python
# 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étodo | Descripció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 |
put y get mantienen el objeto entero en memoria. Para un objeto grande, usa las variantes de archivo, que hacen streaming (en Go, también PutReader y GetTo). 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étodo | Descripció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. Una página contiene hasta 200 objetos, salvo que pases otro limit. 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:
Python
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.
| Campo | Significado |
|---|---|
size_bytes / sizeBytes | Bytes almacenados actualmente |
object_count / objectCount | Número de objetos almacenados |
quota_bytes / quotaBytes | El presupuesto aplicado. 0 significa ilimitado |
free_bytes / freeBytes | Bytes restantes. -1 significa ilimitado — indicar 0 ahí se leería como “lleno” |
per_namespace / perNamespace | Bytes 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:
| Campo | Significado |
|---|---|
quota_bytes / quotaBytes | Lo que realmente se aplica, leído del almacén de objetos — el ajuste del usuario del proyecto |
suggested_quota_bytes / suggestedQuotaBytes | Lo 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étodo | Vigencia | Quién puede leerlo |
|---|---|---|
url(key, …) | Permanente | Solo un solicitante autenticado con READ sobre este backend de datos — se comprueba en cada petición (una decisión se guarda en caché durante un máximo de 5 minutos). Seguro para almacenar en una columna de tabla |
share_url / shareUrl / ShareURL | Caduca (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. En un espacio de nombres declarado con share: none, la URL permanente tampoco funciona, pero url no puede saberlo, porque el catálogo no informa de cómo se comparte un espacio de nombres: devuelve la URL de todos modos, y el borde HTTP de archivos le responde con un 404. Lee esos objetos con get; los enlaces de share_url / shareUrl / ShareURL no dependen de share. 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 (válida durante 1 hora por defecto) 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 objeto | Cómo viaja |
|---|---|
| Hasta el límite inline (actualmente 6 MiB) | Una única llamada a través del router de mensajes |
| Mayor | Directamente al almacenamiento de objetos por HTTPS, sin pasar por el router |
| Mayor, en lecturas donde no hay ruta directa | Por 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.
Python
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ódigo | Significado |
|---|---|
NOT_AUTHORIZED | El llamador no puede realizar esta operación |
NO_SUCH_NAMESPACE | El espacio de nombres no está declarado en la plantilla de datos |
NO_SUCH_OBJECT | La clave no existe |
TOO_LARGE | Supera el límite de transferencia en una sola llamada |
OBJECT_TOO_LARGE | Supera el maxObjectBytes propio del espacio de nombres |
QUOTA_EXCEEDED | El filestore está lleno |
CONTENT_TYPE_NOT_ALLOWED | El espacio de nombres restringe los contentTypes |
INVALID_KEY | Una clave o un prefijo mal formados (ver abajo; FleetFiles v0.2 todavía lo reporta como INTERNAL) |
INVALID_RANGE | Un rango de bytes fuera del objeto; los SDKs nunca piden uno |
NOT_SUPPORTED | El backend no puede hacer esto |
NOT_AVAILABLE | Este 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_UNREACHABLE | El almacenamiento de objetos no es accesible directamente (¿un proxy?) |
CLOCK_SKEW | El reloj del dispositivo está demasiado desfasado |
INTERNAL | Cualquier 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 hasta 10 segundos a la conexión (si se emiten antes de start() / Start, también esperan a que se inicie) 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,
FileStoreErrorse importa desdeironflock.filestore; en JavaScript se exporta desde la raíz del paquete (import { FileStoreError } from "ironflock"); en Go es*filestore.Error(tambiénironflock.FileStoreError), con los códigos como constantes comofilestore.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 PROD cuando ENV es PROD (en mayúsculas o minúsculas) y DEV en cualquier otro caso. Antes de enviar nada, el SDK rechaza un topic vacío y una clave de dispositivo, de proyecto o de app que falte o sea 0; lo mismo hace callDeviceFunction / call_device_function. El router no acepta ningún otro registro de una app, y solo registros individuales (consulta URIs que una app puede usar). Sin conexión, el método espera a una como máximo durante la ventana de reconexión (60 segundos por defecto, consulta Fiabilidad de la conexión). El registro se restaura tras cada reconexión. El método devuelve un Registration, el manejador del registro: su unregister() lo quita (consulta unsubscribe / unregister).
Python
def add(a, b):
return a + b
await ironflock.register_device_function("com.myapp.add", add)
register()es un alias deregister_device_function().
Los nombres anteriores register_function / registerFunction / RegisterFunction y call_function / callFunction / CallFunction siguen funcionando, pero están obsoletos.
Apps que sobrescriben
ENV: las versiones del SDK hasta la 1.8.6 (Python) y la 1.8.4 (JavaScript) ponían el valor deENVtal cual en el segmento del stage. Eso solo importa cuando una app estableceENVpor sí misma con otra grafía, comoprod: entonces los dispositivos con esas versiones y los que tienen una versión más reciente no pueden llamar a las funciones de los otros.
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.
Python
result = await ironflock.call_device_function(
42, # target device key
"com.myapp.add", # procedure name
args=[3, 5] # arguments
)
print(result) # 8call
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.
Python
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 Python, el método lanza un RuntimeError y wamp_error_uri(err) devuelve la URI; en JavaScript, el error es un WampError cuyo error es la URI; en Go, ironflock.WampURI(err) la devuelve.
| URI en tu propio realm | Permitido | Usado por |
|---|---|---|
Tus propios nombres, p. ej. com.myapp.status | publicar, suscribirse (también por prefijo), llamar — nunca registrar | publish, subscribe, call |
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table> | publicar — nunca suscribirse | – (los topics de escritura de las tablas; las escrituras en tablas del SDK usan append en su lugar) |
transformed.<table>, transformed.bulk.<table> | solo suscribirse | subscribeToTable / subscribe_to_table |
databackend.errors.…, files.events… | solo suscribirse | Los eventos de error y de archivos del backend de datos |
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name> | registrar (individual), llamar, publicar — nunca suscribirse | registerDeviceFunction / 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.resolve | llamar | listConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps |
wamp.session.get | llamar | El 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.
Python
await ironflock.set_device_location(long=8.6821, lat=50.1109)| Parámetro | Rango |
|---|---|
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.
Python
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ámetro | Tipo | Descripción |
|---|---|---|
port | int / number | El puerto, tal como se declara en port-template.yml |
protocol | str / string, opcional | El 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 / remotePortEnvironment | str / string, opcional | Solo 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 de los puertos 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_DOMAINenlocalhost, 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 estableceCLOUD_TUNNEL_DOMAINhoy en día: las URLs de un appliance usanapp.ironflock.comsalvo 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
Python
| Propiedad | Tipo | Descripción |
|---|---|---|
is_connected | bool | Indica si la conexión con la plataforma está activa |
connection | CrossbarConnection | La instancia de conexión subyacente (uso avanzado) |
files | FileStore | El almacenamiento de archivos gestionado de la app (consulta Almacenamiento de archivos gestionado) |
| Método | Descripción |
|---|---|
run() | Inicia la conexión, ejecuta mainFunc (bloqueante) y después detiene la conexión |
await start() | Inicia la conexión de forma asíncrona. Las operaciones emitidas antes esperan a que se inicie, dentro de su propia espera (consulta Fiabilidad de la conexión) |
await stop() | Detiene la conexión y las conexiones a apps consumidas, y cancela la tarea principal (mainFunc). Las conexiones abandonan sus realms y no se reconectan después; start() vuelve a conectar. Toda operación que siga esperando una conexión, o a start(), falla de inmediato, igual que un start() o connect_to_app que siga esperando; una operación emitida después de stop() espera al siguiente start() (consulta Fiabilidad de la conexión) |
await run_async() | Inicia y mantiene la conexión de forma asíncrona |
run() y run_async() detienen la conexión en cuanto mainFunc retorna o lanza una excepción; una excepción que lance se vuelve a lanzar después de la parada. Por eso, una mainFunc que solo se suscribe o registra funciones de dispositivo debe seguir ejecutándose, por ejemplo con await asyncio.Event().wait(). mainFunc debe ser una función asíncrona (async def). Sin una mainFunc, run() y run_async() mantienen la conexión abierta hasta que termina el proceso.
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, y los manejadores que recibiste siguen siendo válidos; su active indica si cada uno está activo ahora en el router (consulta unsubscribe / unregister). Las suscripciones a tablas también entregan las filas almacenadas mientras la conexión estaba caída (consulta subscribeToTable); los mensajes de tus propios topics no se almacenan, así que los que se publicaron entretanto se pierden. 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 en tablas, lecturas de historial, de series y de secretos, y las lecturas de historial y de series 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; una emitida antes de start() / Start espera a que se inicie dentro de la misma ventana. También 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. La lectura de puesta al día de una suscripción a tabla es una de estas lecturas.
Las escrituras en tablas incluyen publishToTable / publish_to_table, publishRowsToTable / publish_rows_to_table y reportError / report_error: llaman a los mismos procedimientos de append que appendToTable y appendRowsToTable, así que una fila escrita mientras el backend de datos se reinicia no se pierde.
publish, subscribe, subscribeToTable / subscribe_to_table (también el de una app consumida) y registerDeviceFunction / register_device_function también esperan a una conexión dentro de la ventana de reconexión, así que un corte breve no las hace fallar. No se reintentan; una suscripción o un registro se restaura tras cada reconexión posterior.
Todas las demás operaciones — call, callDeviceFunction / call_device_function, setDeviceLocation / set_device_location, las llamadas de archivos, y el descubrimiento y la apertura de conexiones entre apps — esperan hasta 10 segundos a una conexión y no se reintentan. Una emitida antes de start() / Start espera a que se inicie dentro de esos 10 segundos. Una operación que no obtiene conexión dentro de su espera falla con un error que indica la espera en segundos: en Python, los métodos de IronFlock lanzan un RuntimeError cuya causa es un TimeoutError; en JavaScript, es un Error normal; en Go, un error que envuelve wamp.ErrNotConnected. unsubscribe y unregister no esperan en absoluto: surten efecto de inmediato, también mientras la conexión está caída (consulta unsubscribe / unregister).
0 desactiva la ventana: estas operaciones se comportan entonces como cualquier otra operación. Esperan 10 segundos a una conexión y no se reintentan, así que una puesta al día justo después de un reinicio de la plataforma tampoco se reintenta. Una ventana negativa, y en Python y JavaScript también NaN o un infinito, se rechaza al crear el objeto IronFlock (consulta Opciones del constructor).
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 Python, la causa del error es el ApplicationError de autobahn con el motivo del cierre (p. ej., wamp.close.system_shutdown), o TransportLost cuando se cayó el enlace. En JavaScript, es un WampError cuyo error es el motivo del cierre: el del router, wamp.close.goodbye_and_out después de stop(), o wamp.close.transport_lost cuando se cayó el enlace. En Go, una petición así falla con un error que envuelve wamp.ErrNotConnected (wamp.ErrStopped después de Stop).
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() / Stop pone fin de inmediato a toda espera. Una operación que siga esperando una conexión — o, si se emitió antes de start() / Start, a que se inicie — falla con el error de parada del SDK, igual que una llamada de tabla que está en pausa entre dos reintentos: en Python, un ConnectionError que indica que la conexión de IronFlock se detuvo (los métodos de IronFlock lo lanzan como causa de su RuntimeError); en JavaScript, un ConnectionStoppedError, y en Go, un error que envuelve wamp.ErrStopped. 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; en Go, Start devuelve un error que envuelve wamp.ErrStopped. 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(). En Go, ConnectToApp falla con ironflock: connection to app '<app>' (<stage>) aborted by Stop (o closed by Stop). Una denegación que llega a la vez que stop() conserva su CrossAppAccessError. En Python y JavaScript, cuando stop() termina ya no queda nada intentando conectarse, y un router que no responde cuando la conexión abandona el realm queda desconectado tras 5 segundos; el Stop de Go espera la salida del realm dentro de ctx y luego cierra el socket, y Run le da 10 segundos.
En Python y JavaScript, la instancia puede volver a iniciarse después de stop(): start() vuelve a conectar, y una operación emitida después de stop() espera al siguiente start(), igual que una emitida antes del primer start(). El Stop de Go es definitivo: una operación emitida después falla de inmediato con wamp.ErrStopped.
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.
Python
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_appnormalmente 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 (consultaconnectToApp).
Manejo de errores
Un método del SDK que falla — ante argumentos inválidos, una conexión perdida o un rechazo de la plataforma — lanza una excepción (Python), rechaza la promesa (JavaScript) o devuelve un error (Go) con un mensaje que indica la operación, el topic y el motivo, así que envuelve en un bloque try las llamadas que quieras que sobrevivan — en Go, comprueba el error devuelto. Algunos fallos no llegan a tu código. Un mensaje publicado en un topic propio no llega a ningún suscriptor que esté caído entretanto. Una lectura de puesta al día que falla tras una reconexión solo se registra en el log (consulta subscribeToTable).
Python
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, un rechazo del router o de un destinatario de la llamada suele ser un 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. Tres tipos de llamada fallan de otra forma: una llamada de historial, de series, de revelado o de verificación cuyo procedimiento no está registrado falla con un Error normal que lo indica y nombra las causas probables, y solo una vez agotada toda la ventana de reconexión, porque wamp.error.no_such_procedure se reintenta; una llamada de archivos falla con un FileStoreError (consulta Errores de almacenamiento de archivos); y una llamada entre apps denegada, incluido el subscribeToTable de una app consumida, falla con un CrossAppAccessError. Una operación a la que stop() pone fin mientras espera la conexión falla con un ConnectionStoppedError, una subclase exportada de Error. 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 (una operación después de Stop, o una que Stop interrumpió).
Migración: las versiones anteriores de los SDK de Python y JavaScript registraban un mensaje y devolvían
None/nullcuando una llamada fallaba. Ahora lanzan un error en su lugar, por lo que el código con la formaif result is None:ya no detecta los fallos — usatry/except(otry/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. Todas las escrituras en tablas los informan, incluidos los métodos de publicación, que usan append.
Python
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| Constante | Rechazado |
|---|---|
URI_RATE_LIMITED | revealSecrets / 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_LARGE | Una 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_LIMIT | Una 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_GROUPS | Consultas 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_COLUMN | Un filtro sobre una columna secreta; verifySecret / verify_secret sobre una columna que no es secreta. Una tabla sin columnas secretas no tiene ninguna función de revelado ni de verificación (consulta revealSecrets) |
URI_STORAGE_FULL | Un 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_OVERUSAGE | Un append que supera la asignación de almacenamiento de la cuenta; reintentar pronto no ayuda |
URI_ENTITY_KEY_CONFLICT | Una 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_REJECTED | Una 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:
Python
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: trueEl 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:
| Variable | Descripción |
|---|---|
DEVICE_NAME | Nombre visible del dispositivo |
DEVICE_SERIAL_NUMBER | Identificador único e inmutable del dispositivo |
DEVICE_KEY | La clave numérica del dispositivo. El SDK construye con ella las URIs de las funciones de dispositivo y las etiquetas de túnel de acceso remoto, y la envía como metadato con las publicaciones y las escrituras en tablas; no se autentica con ella |
SWARM_KEY | Identificador del proyecto |
APP_KEY | Identificador de la app |
APP_NAME | Nombre de la app |
ENV | Entorno: DEV o PROD |
APP_AUTH_ID, APP_AUTH_SECRET | La 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 |
DEVICE_ENDPOINT_URL | La URL del router a la que se conecta el SDK, con su ruta sustituida por /ws-ua-usr. Tiene prioridad sobre RESWARM_URL; una URL del router pasada como opción (ironFlockUrl, WithURL en Go) prevalece sobre ambas |
RESWARM_URL | La URL del studio, de la que el SDK deriva la URL del router cuando DEVICE_ENDPOINT_URL no está definida. Una URL del studio que el SDK no conoce hace fallar start() / Start. Sin ninguna de las dos variables, el SDK se conecta a wss://cbw.ironflock.com/ws-ua-usr |
Python
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 SDK leen de él dos tipos de valores, dando prioridad al archivo, para que un cambio llegue a una app en ejecución: la credencial propia de la app (APP_AUTH_ID, APP_AUTH_SECRET), que 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; y los puertos remotos (REMOTE_PORT_FOR_<port> y REMOTE_PORT_FOR_<port>_CLOUD), que se leen en cada llamada a getRemoteAccessUrlForPort. Todos los demás valores — la identidad (DEVICE_KEY, DEVICE_NAME, …), ENV y los dominios de túnel — proceden del entorno con el que arrancó el contenedor (Go lee la identidad y ENV una sola vez, en New). Los agentes del dispositivo más antiguos hacen que /data/env solo sea legible por root: en un contenedor que se ejecuta con otro usuario, el SDK ve entonces los valores con los que arrancó el contenedor y registra una advertencia una sola vez. En JavaScript, leer el reflejo requiere Node.js 22.3 o posterior; un Node.js más antiguo solo ve el entorno.