Skip to Content

IronFlock SDK

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

SDKPaqueteRequisitos
Pythonironflock en PyPIPython 3.11+
JavaScriptironflock en npmNode.js 22+ o navegador moderno
Gogithub.com/ironflock/sdk-goGo 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

pip install ironflock

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

Inicio rápido

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

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

Opciones del constructor

ironflock = IronFlock( mainFunc=main, # async function to run after connecting serial_number="abc123", # override device serial (optional) reconnect_window=60, # seconds an operation rides out a platform restart (optional) )
ParámetroDescripción
mainFuncUna 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)
ironFlockUrlLa 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_numberSobrescribe el número de serie del dispositivo que se envía con las publicaciones y las escrituras en tablas. Por defecto usa la variable de entorno DEVICE_SERIAL_NUMBER. No cambia la identidad con la que se autentica la conexión mientras el agente del dispositivo inyecte credenciales propias de la app
reconnect_windowSegundos que 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).

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

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

appendToTable / append_to_table

Añade datos a una tabla de flota usando una llamada a procedimiento remoto: 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.

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.

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.

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.

# 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ámetroTipoDescripción
errorstr / string o excepción / ErrorEl mensaje de error, o una excepción cuyo traceback/stack (o mensaje) se registra
levelstr / string, opcionalSeveridad: "error", "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"
appendbool / boolean, opcionalSe 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
tspstr / string, opcionalSobrescritura de marca de tiempo ISO-8601. Por defecto la hora actual
user_message / userMessagestr / string, opcionalTexto para el operador, almacenado como user_message. Por defecto, el mensaje registrado

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

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

Consulta de datos históricos

getHistory

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

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

Parámetros de consulta:

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

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":

# 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.

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

Parámetros de consulta:

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

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

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

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

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

Suscripción a datos

subscribeToTable / subscribe_to_table

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

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

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.

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

subscribe

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

def on_alert(*args, **kwargs): print("Alert received:", args, kwargs) 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.

ManejadorDevuelto porMiembros
Subscriptionsubscribeunsubscribe(), topic, active
TableSubscriptionsubscribeToTable / subscribe_to_table, de tu propia app y de una app consumidaunsubscribe(), table, active
RegistrationregisterDeviceFunction / register_device_function y sus aliasunregister(), 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.

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, un WampError) 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, null o undefined, nil). Después de stop() / Stop, una llamada solo quita la suscripción o el registro localmente: en Python y JavaScript, el siguiente start() 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 TableSubscription quita sus dos flujos, transformed.<table> y transformed.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 / subscribeToTable devolvía solo la suscripción del flujo de filas, así que al quitarla el flujo masivo seguía suscrito. El código que llama a unsubscribe() o unregister() 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, como id, 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 lecturaQué 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 datosel texto cifrado almacenado, ifsec:1:…
revealSecrets / reveal_secretsel texto en claro descifrado

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

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

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.

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

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

revealSecrets / reveal_secrets

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

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

Los parámetros de consulta son los mismos que los de getHistory, 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.

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

Parámetros:

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

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

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

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

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

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

Parámetros:

ParámetroTipoDescripción
app_name / appNamestr / stringNombre de la app proveedora, tal como se declara en tu sección consumes:
stagestr / string, opcionalStage del proveedor: "dev" o "prod". Por defecto, el stage de tu propia app
on_error / onErrorcallable, opcionalSe invoca con un CrossAppAccessError cuando a la conexión abierta se le deniega el acceso de forma definitiva (p. ej., si la concesión se revoca más tarde — más abajo se explica cuándo una denegación se considera definitiva)

Si el acceso se deniega o se usa incorrectamente, se lanza un CrossAppAccessError (Python / JavaScript) o se devuelve (Go, un *ironflock.CrossAppAccessError que se comprueba con errors.As) con un campo code (Code en Go, con constantes como ironflock.CodeNoGrant): NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, PRIVATE_TABLE, SECRET_COLUMN (una consulta que filtra — también dentro de un grupo — o selecciona una columna que el proveedor marca como secreta) o NOT_AUTHORIZED (la concesión se revocó o no pudo verificarse en ese momento — o tu app presentó la credencial heredada del dispositivo, en cuyo caso el mensaje lo indica). 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, stage y on_error son argumentos con nombre; en JavaScript se pasan a través de un objeto de opciones (connectToApp(appName, { stage, onError })); en Go, a través de un struct (ConnectToApp(ctx, appName, ironflock.ConnectToAppOptions{Stage, OnError})), donde OnError es una func(*ironflock.CrossAppAccessError). En Go, ctx solo acota la espera de quien llama: las llamadas simultáneas para la misma app comparten un intento, y Close(ctx) cierra un manejador. Go compara los nombres de app sin distinguir mayúsculas y minúsculas, y ConsumedApp.Stage es el stage en minúsculas ("dev" o "prod"), a diferencia de ifl.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.yml de tu app, y entrecomilla el * — un * sin comillas es un alias de YAML y no se parseará:

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

Cada entrada describe un proveedor:

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

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

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

connectToAllApps / connect_to_all_apps

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

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

Parámetros:

ParámetroTipoDescripción
stagestr / string, opcionalStage del proveedor: "dev" o "prod". Por defecto, el stage de tu propia app
continue_on_error / continueOnErrorbool / boolean, opcionalCuando es true (el valor por defecto), un proveedor que no se puede abrir se reporta a on_error / onError y se omite del resultado. Cuando es false, el primer fallo se lanza
on_error / onErrorcallable, opcionalSe invoca con 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_error y continue_on_error son argumentos con nombre; en JavaScript se pasan a través de un objeto de opciones (connectToAllApps({ stage, onError, continueOnError })); en Go, a través de un struct (ConnectToAllApps(ctx, ironflock.ConnectToAllAppsOptions{Stage, OnError, StopOnError})), donde OnError es una func(error) y StopOnError: true corresponde a continueOnError: false.

Almacenamiento de archivos gestionado

Cada backend de datos de app dispone de almacenamiento de objetos privado junto a sus tablas, accesible a través de la propiedad files (ifl.Files() en Go, un *filestore.FileStore). 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:

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

# Bytes in, bytes out info = await ironflock.files.put("reports/march.pdf", pdf_bytes, content_type="application/pdf") data = await ironflock.files.get("reports/march.pdf") # Or straight from/to a local file — these stream on the large-object path, # so a multi-gigabyte file never has to fit in memory await ironflock.files.put_file("firmware/v2.bin", "/data/build/v2.bin") await ironflock.files.get_to_file("firmware/v2.bin", "/tmp/v2.bin")
MétodoDescripción
put(key, data, …)Almacena un objeto (bytes en Python, Uint8Array en JavaScript, []byte en Go). Devuelve los metadatos del objeto, incluida su url
get(key, namespace?)Devuelve el contenido del objeto. Un objeto por encima del límite inline llega directamente del almacenamiento de objetos o, donde eso no es posible, por rangos de bytes a través del router (consulta Objetos grandes)
put_file(key, path, …) / get_to_file(key, path, …)Python y Go (PutFile / GetToFile; Go además hace streaming desde un io.Reader con PutReader y hacia un io.Writer con GetTo). Almacena desde un archivo local, o escribe en él. Usa streaming en la ruta de objetos grandes
delete(key, namespace?)Elimina un objeto
copy(key, to, …)Copia un objeto, opcionalmente a otro espacio de nombres
move(key, to, …)Copia y luego elimina. No es atómico — el servicio no tiene un verbo de movimiento, así que un borrado fallido deja ambas copias

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étodoDescripción
list(…)Una página del listado del servicio de archivos, con estilo de carpetas por defecto: objects contiene los objetos cuyas claves continúan el prefijo sin una /, prefixes las carpetas que hay debajo. Un delimiter de "" lista en plano — todos los objetos bajo el prefijo, también en subcarpetas. 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:

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

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

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

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

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

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

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

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

Compartir objetos

Hay dos tipos de enlace, y la diferencia importa:

MétodoVigenciaQuién puede leerlo
url(key, …)PermanenteSolo un solicitante autenticado con READ sobre este backend de datos — se 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 / ShareURLCaduca (15 min por defecto, acotado por el servidor)Cualquiera que tenga el enlace. Nada vuelve a comprobar la autorización cuando se usa

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

url devuelve None / undefined / "" (Go) cuando el despliegue no tiene un borde HTTP (por ejemplo, un appliance de HTTP simple) — esa es la señal para recurrir a get. 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 objetoCómo viaja
Hasta el límite inline (actualmente 6 MiB)Una única llamada a través del router de mensajes
MayorDirectamente al almacenamiento de objetos por HTTPS, sin pasar por el router
Mayor, en lecturas donde no hay ruta directaPor rangos de bytes a través del router de mensajes

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

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

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

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

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

Errores de almacenamiento de archivos

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

from ironflock.filestore import FileStoreError try: await ironflock.files.put("huge.bin", payload) except FileStoreError as e: if e.code == "QUOTA_EXCEEDED": print("Filestore is full:", e.reason) else: raise
CódigoSignificado
NOT_AUTHORIZEDEl llamador no puede realizar esta operación
NO_SUCH_NAMESPACEEl espacio de nombres no está declarado en la plantilla de datos
NO_SUCH_OBJECTLa clave no existe
TOO_LARGESupera el límite de transferencia en una sola llamada
OBJECT_TOO_LARGESupera el maxObjectBytes propio del espacio de nombres
QUOTA_EXCEEDEDEl filestore está lleno
CONTENT_TYPE_NOT_ALLOWEDEl espacio de nombres restringe los contentTypes
INVALID_KEYUna clave o un prefijo mal formados (ver abajo; FleetFiles v0.2 todavía lo reporta como INTERNAL)
INVALID_RANGEUn rango de bytes fuera del objeto; los SDKs nunca piden uno
NOT_SUPPORTEDEl backend no puede hacer esto
NOT_AVAILABLEEste despliegue no tiene servicio de archivos — o todavía no: tras un reinicio de la plataforma, o mientras el servicio de archivos registra sus procedimientos. La llamada no se ejecutó en ningún sitio y puede repetirse
PRESIGN_UNREACHABLEEl almacenamiento de objetos no es accesible directamente (¿un proxy?)
CLOCK_SKEWEl reloj del dispositivo está demasiado desfasado
INTERNALCualquier otra cosa — también un objeto que cambió durante una lectura por rangos

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

Las llamadas de archivos esperan 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, FileStoreError se importa desde ironflock.filestore; en JavaScript se exporta desde la raíz del paquete (import { FileStoreError } from "ironflock"); en Go es *filestore.Error (también ironflock.FileStoreError), con los códigos como constantes como filestore.CodeQuotaExceeded.

Comunicación entre dispositivos

registerDeviceFunction / register_device_function

Registra un procedimiento al que pueden llamar otros dispositivos que ejecutan la misma app (en el mismo stage), y también las acciones de los widgets del tablero. El SDK construye por ti la URI completa <SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<topic>, donde STAGE es 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).

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

register() es un alias de register_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 de ENV tal cual en el segmento del stage. Eso solo importa cuando una app establece ENV por sí misma con otra grafía, como prod: 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.

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

call

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

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

URIs que una app puede usar

El contenedor de tu app se une a su propio realm de datos, realm-<SWARM_KEY>-<APP_KEY>-<dev|prod>, con el rol de router app. El router comprueba allí cada publicación, suscripción, llamada y registro contra la lista de permitidos de ese rol. Los métodos propios del SDK se mantienen dentro de ella; las reglas importan cuando pasas tus propias URIs a publish, subscribe o call, o usas la connection directamente (Connection() en Go). Una acción rechazada falla con wamp.error.not_authorized. En 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 realmPermitidoUsado por
Tus propios nombres, p. ej. com.myapp.statuspublicar, suscribirse (también por prefijo), llamar — nunca registrarpublish, subscribe, call
<SWARM_KEY>.<APP_KEY>.<table>, bulk.<SWARM_KEY>.<APP_KEY>.<table>publicar — nunca 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 suscribirsesubscribeToTable / subscribe_to_table
databackend.errors.…, files.events…solo suscribirseLos eventos de error y de archivos del backend de datos
<SWARM_KEY>.<DEVICE_KEY>.<APP_KEY>.<STAGE>.<name>registrar (individual), llamar, publicar — nunca suscribirseregisterDeviceFunction / register_device_function, callDeviceFunction / call_device_function
append.…, appendBulk.…, history.transformed.…, secret.reveal.…, secret.verify.…, files.read.…, files.write.…llamar (el SDK los llama)Métodos de tablas, historial, secretos y archivos
sys.appaccess.list, sys.appaccess.resolvellamarlistConsumableApps / list_consumable_apps, connectToApp / connect_to_app, connectToAllApps / connect_to_all_apps
wamp.session.getllamarEl heartbeat del SDK de JavaScript

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

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

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

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

Metadatos del dispositivo

setDeviceLocation / set_device_location

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

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

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

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

getRemoteAccessUrlForPort

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

url = ironflock.getRemoteAccessUrlForPort(8080) # "https://<device_key>-<app_name>-8080.app.ironflock.com" vnc = ironflock.getRemoteAccessUrlForPort(5900, protocol="tcp") # "tcp://app.ironflock.com:<the tunnel's remote port>"
ParámetroTipoDescripción
portint / numberEl puerto, tal como se declara en port-template.yml
protocolstr / string, opcionalEl protocolo del puerto: "http" (predeterminado), "https", "tcp" o "udp", sin distinguir mayúsculas y minúsculas; los espacios alrededor se ignoran, y uno vacío equivale a "http"
remote_port_environment / remotePortEnvironmentstr / string, opcionalSolo tcp/udp: la variable que contiene el puerto remoto, en lugar de REMOTE_PORT_FOR_<port> — el remote_port_environment propio del puerto en la plantilla, que necesitan los agentes de dispositivo anteriores a 0.19.8

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

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

Los valores 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_DOMAIN en localhost, así que las URLs que se componen allí apuntan a localhost: un appliance así sirve los puertos de las apps en sus puertos del host, algo que una URL de túnel no puede expresar.
  • Los dominios de túnel no definidos o vacíos usan por defecto app.ironflock.com. La plataforma no establece CLOUD_TUNNEL_DOMAIN hoy en día: las URLs de un appliance usan app.ironflock.com salvo que la propia app establezca esa variable en su configuración de entorno.
  • En un contenedor que se ejecuta con un usuario distinto de root, los valores son aquellos con los que arrancó el contenedor (consulta Variables de entorno).

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

Propiedades de conexión y ciclo de vida

PropiedadTipoDescripción
is_connectedboolIndica si la conexión con la plataforma está activa
connectionCrossbarConnectionLa instancia de conexión subyacente (uso avanzado)
filesFileStoreEl almacenamiento de archivos gestionado de la app (consulta Almacenamiento de archivos gestionado)
MétodoDescripció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.

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

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

Manejo de errores

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).

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 / null cuando una llamada fallaba. Ahora lanzan un error en su lugar, por lo que el código con la forma if result is None: ya no detecta los fallos — usa try / except (o try / catch).

Rechazos del backend de datos

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

from datetime import datetime, timezone from ironflock import URI_STORAGE_FULL, wamp_error_uri row = {"tsp": datetime.now(timezone.utc).isoformat(), "temperature": 22.5} try: await ironflock.append_to_table("sensordata", row) except RuntimeError as err: if wamp_error_uri(err) == URI_STORAGE_FULL: pending.append(row) # the appliance's disk is nearly full: append the row later else: raise
ConstanteRechazado
URI_RATE_LIMITEDrevealSecrets / reveal_secrets por encima de 30, verifySecret / verify_secret por encima de 120 llamadas por minuto por credencial de app en un dispositivo
URI_RESULT_TOO_LARGEUna lectura con una sola fila por encima del presupuesto de mensaje de 8 MiB, o un resultado por encima de la protección de 1 GiB (los demás resultados grandes llegan en fragmentos, que el SDK vuelve a ensamblar). Sus argumentos con nombre indican las columnas más grandes: reduce limit, acota timeRange, selecciona menos columns o recorta con columnPaths
URI_INVALID_LIMITUna lectura de un transform con un limit superior a 3000
URI_INVALID_TIME_RANGE, URI_INVALID_METRIC, URI_INVALID_GROUP_BY, URI_SERIES_TOO_MANY_GROUPSConsultas de series que la tabla no puede responder: sin inicio, o un fin que no es posterior a él; una métrica que la tabla no puede agregar (no existe la columna, una columna secreta, un método que el tipo de la columna no admite, tsp con un método distinto de COUNT); una columna de groupBy por la que no puede agrupar; más de 50.000 filas
URI_SECRET_COLUMN, URI_NOT_A_SECRET_COLUMNUn filtro sobre una columna secreta; verifySecret / verify_secret sobre una columna que no es secreta. Una tabla sin columnas secretas no tiene ninguna función de revelado ni de verificación (consulta revealSecrets)
URI_STORAGE_FULLUn append mientras el disco del appliance está casi lleno (por debajo de unos 3 GiB libres; se vuelve a aceptar a partir de 4 GiB) — temporal: añade las filas más tarde
URI_STORAGE_OVERUSAGEUn append que supera la asignación de almacenamiento de la cuenta; reintentar pronto no ayuda
URI_ENTITY_KEY_CONFLICTUna fila cuya clave de entidad (maintainLatestFlagFor) y tsp ya tiene otra fila, en el lote o almacenada: da a cada fila su propio tsp
URI_SECRET_SENTINEL_UNRESOLVABLE, URI_SECRET_CIPHERTEXT_REJECTEDUna escritura que conserva el valor anterior de un secreto (__secret__) sin una clave de entidad con la que encontrarlo; una escritura de un valor ya cifrado

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

Uso en navegador (solo JavaScript)

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

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

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

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

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

Registro de funciones para agentes de IA

El SDK puede registrar funciones que son invocables por agentes de IA. Registra un procedimiento y referencia su topic en tu ai-template.yml:

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

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

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

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

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

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

Variables de entorno

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

VariableDescripción
DEVICE_NAMENombre visible del dispositivo
DEVICE_SERIAL_NUMBERIdentificador único e inmutable del dispositivo
DEVICE_KEYLa 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_KEYIdentificador del proyecto
APP_KEYIdentificador de la app
APP_NAMENombre de la app
ENVEntorno: DEV o PROD
APP_AUTH_ID, APP_AUTH_SECRETLa credencial propia de la app, que el SDK presenta al router. El acceso entre apps la requiere; sin ella (un agente del dispositivo más antiguo), el SDK recurre al número de serie del dispositivo
DEVICE_ENDPOINT_URLLa 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_URLLa 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
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.

Last updated on