Разработчикам
Отправка событий с устройства.
На устройстве работает агент subrose, небольшая программа. Любая другая программа на устройстве может сообщить ему о событии: об авторизации, аномалии, показании датчика. Агент подписывает сообщение ключом устройства и отправляет на ноду, независимый сервер на связи с устройством. События читаются через HTTP API ноды или в веб-консоли Subrose, а для периодического сбора есть cron-скрипты.
Подпись не защищает событие от чтения. Паролям и токенам в событиях не место, шифрование на подходе.
01 · Механизмы
Два способа отправить событие.
Локальное событие (local push)
Программа на устройстве пишет JSON в локальный канал агента (AF_UNIX-сокет на Linux, macOS и OpenWrt; Windows на подходе) или вызывает subrose emit из командной строки. Работает, только если владелец включил события в контракте устройства, то есть в его записи в TON.
Скрипты-сборщики (collector scripts)
Агент запускает *.sh-скрипты из директории collectors по cron-расписанию, которое владелец задаёт в контракте устройства. Stdout скрипта становится данными события. Работает, только если владелец включил флаг stat.
02 · Требования
Что владелец включает заранее.
Для локальных событий (механизм 1) владелец включает флаг
events = true в контракте устройства командой
SetEvents. Для скриптов-сборщиков (механизм 2) —
флаг stat = true через SetStat
и расписание через SetCronSchedule.
Пока флаг выключен, агент принимает сообщения в локальный канал, но не пересылает их на ноду. Новые флаги устройство получает, когда перечитывает состояние контракта: после регистрации, по сигналу ноды или по своему таймеру.
03 · Механизм 1
Локальное событие: программа пишет в канал агента.
Агент слушает локальный канал. Любая программа, запущенная от имени того же пользователя (или root), открывает соединение, пишет одну JSON-строку и закрывает его. Агент добавляет подпись ключом устройства и отправляет событие на ноду.
Путь сокета по умолчанию — <stateDir>/subrose.sock,
где <stateDir> — рабочая директория агента
(на Linux и роутерах обычно /var/run/subrose);
сменить путь можно ключом конфига events.socketPath.
Windows-канал \\.\pipe\subrose — на подходе.
Формат сообщения — одна строка JSON с переводом строки в конце:
{"name":"motion_detected","data":{"camera":"garage","confidence":0.93}}
04 · Рецепты
Как вызвать из разных окружений.
Shell — subrose emit
Самый простой способ. Работает везде, где установлен пакет subrose.
Флаг --data принимает JSON-объект; без него поле data
остаётся пустым.
# Простое событие без данных
subrose emit --name door_open
# Событие с данными
subrose emit --name wifi_connected --data '{"ssid":"HomeNet","rssi":-55}'
# В скрипте init.d / cron (например, при смене DNS)
/opt/subrose/subrose emit --name dns_changed --data "{\"server\":\"$DNS\"}"
Shell — прямая запись в сокет (socat / nc)
Если subrose emit недоступен, событие пишут в сокет
напрямую. Подходит для скриптов init.d на Keenetic/OpenWrt, где нет
пути к бинарю.
# socat (широко доступен в OpenWrt/opkg)
echo '{"name":"reboot","data":{}}' | socat - UNIX-CONNECT:/var/run/subrose/subrose.sock
# nc с поддержкой unix-сокетов (BusyBox nc не поддерживает -U, там нужен socat)
# На Linux с полноценным nc:
echo '{"name":"reboot","data":{}}' | nc -U /var/run/subrose/subrose.sock
Python 3
Встроенный модуль socket — дополнительных зависимостей нет.
Работает на Linux, macOS и Raspberry Pi.
import socket, json
SOCK_PATH = "/var/run/subrose/subrose.sock" # или из конфига
def emit(name: str, data: dict = None):
payload = json.dumps({"name": name, "data": data or {}}) + "\n"
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect(SOCK_PATH)
s.sendall(payload.encode())
# Пример использования
emit("temperature", {"celsius": 42.5, "sensor": "cpu"})
emit("disk_full", {"path": "/data", "used_pct": 97})
C (POSIX)
Минимальный фрагмент для встраивания в системный демон или init-скрипт на OpenWrt / Keenetic. Зависимостей нет, только POSIX-сокеты.
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define SOCK_PATH "/var/run/subrose/subrose.sock"
int subrose_emit(const char *name, const char *data_json) {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_un addr = {0};
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd); return -1;
}
char buf[4096];
int n = snprintf(buf, sizeof(buf),
"{\"name\":\"%s\",\"data\":%s}\n", name, data_json ? data_json : "{}");
write(fd, buf, n);
close(fd);
return 0;
}
/* Пример: */
int main(void) {
subrose_emit("sensor_alert", "{\"type\":\"overheat\",\"temp\":85}");
return 0;
}
Windows — named pipe (на подходе)
Рецепт заработает вместе со сборкой агента для Windows, она на подходе
(см. «Пакеты»). Агент будет слушать
именованный канал \\.\pipe\subrose. Удобнее всего будет
вызывать subrose emit, а в C/C++ подойдут
CreateFile и WriteFile.
# PowerShell
$pipe = [System.IO.File]::Open('\\.\pipe\subrose',
[System.IO.FileMode]::Open, [System.IO.FileAccess]::Write,
[System.IO.FileShare]::None)
$bytes = [System.Text.Encoding]::UTF8.GetBytes(
'{"name":"vpn_up","data":{"peer":"10.0.0.1"}}' + "`n")
$pipe.Write($bytes, 0, $bytes.Length)
$pipe.Close()
05 · Механизм 2
Сбор данных по расписанию: скрипты-сборщики.
Агент периодически сверяется с расписанием (cronSchedule)
и запускает все исполняемые *.sh-файлы из директории
collectors. Результат каждого скрипта уходит на ноду отдельным
событием.
Расписание — стандартные пять полей cron:
минута час день-месяца месяц день-недели.
Владелец задаёт его командой SetCronSchedule в контракте
устройства, а агент получает его вместе с остальным состоянием
контракта.
06 · Пример
Структура директории collectors и пример скрипта.
# Структура по умолчанию:
# <stateDir>/collectors/
# cpu_temp.sh
# disk_usage.sh
# wan_status.sh
# Пример: cpu_temp.sh (должен быть chmod +x)
#!/bin/sh
TEMP=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0)
# Агент ожидает валидный JSON-объект на stdout.
# Если stdout — не JSON, агент оборачивает: {"output":"..."}
printf '{"celsius":%d}' "$((TEMP / 1000))"
Имя события = имя файла скрипта (например, cpu_temp.sh).
Пример расписания для запуска каждые 5 минут: */5 * * * *.
Каждые 2 часа по будням: 0 */2 * * 1-5.
07 · Чтение событий
Чтение — через HTTP API ноды.
Накопленные события читаются через HTTP API ноды. Нода хранит до 50 последних событий на устройство в кольцевом буфере.
GET /device/events?address=0:<device-contract-address>
# Пример ответа:
{
"address": "0:abc...def",
"events": [
{ "name": "cpu_temp.sh", "data": {"celsius": 67}, "at": 1751000001 },
{ "name": "wifi_connected", "data": {"ssid": "HomeNet"}, "at": 1751000120 }
]
}
Поле at — Unix-время записи события на ноде в секундах.
Список идёт от старых к новым, свежее в конце. Индексатор TON
/events/{abi}/{address} устроен иначе: он показывает
события контракта устройства в TON.
08 · Протокол
Как устроена аутентификация события.
Агент подписывает каждое событие закрытым ключом устройства (device private key) перед отправкой на ноду. Нода проверяет подпись открытым ключом устройства (device public key), сохранённым при регистрации, в окне ±60 секунд. Событие без корректной подписи или с устаревшей меткой времени нода отклоняет.
Формула подписи:
sig = hex( ed25519_sign( devicePrivateKey,
sha256( address + ts + name ) ) )
Автору события достаточно записать JSON в локальный канал, подпись добавит агент.
Подпись покрывает адрес, время и имя события. Поле data в
неё не входит, и его содержимое нода видит открытым.
09 · Вопросы
Перед тем как отправлять события.
GET /device/events или в веб-консоли Subrose. Событие появляется там через несколько секунд после отправки.subrose emit и скриптов-сборщиков остаются в буфере ноды (до 50 последних на устройство) и читаются через API или консоль.SetCronSchedule, а stdout скрипта становится данными события.Начать
Первое событие за несколько минут.
Нужно поставить агента, включить события в контракте устройства из
веб-консоли Subrose и выполнить команду
subrose emit --name hello --data '{}'. Событие появится
через несколько секунд.