Своё устройство

Сигнал можно принимать не только на Mac. Подойдёт лампа, мягкая игрушка, браслет или любая плата с Wi‑Fi. Устройство подключается к серверу от твоего имени и умеет две вещи:

Ниже пример: игрушка на плате ESP32 с кольцом светодиодов и одной кнопкой.

Как это устроено

  1. В приложении на Mac ты создаёшь устройство и получаешь его токен.
  2. Устройство держит открытым WebSocket к серверу и получает по нему сигналы. Он работает только в одну сторону, от сервера к устройству: всё, что устройство пишет в него, сервер пропускает.
  3. Чтобы отправить сигнал, устройство делает один HTTP-запрос.

Устройство видит только твой список контактов и может только отправлять и принимать сигналы. Оно не может менять список, удалить аккаунт или узнать, кто прислал сигнал: этого не знает никто.

1. Получи токен устройства

Нажми на сердечко в строке меню → Настройки → Устройства. Дай устройству имя, например «Котик на полке», отметь, что оно умеет: принимать, отправлять или и то и другое, и нажми Подключить устройство.

Приложение покажет токен один раз:

dev_4kq9XbT2mWc7RfLp8sHn3vYzAe6uJd5G

Скопируй его и никому не показывай: с ним можно отправлять сердечки от твоего имени. Если токен утёк или устройство потерялось, отключи устройство в том же разделе, и токен сразу перестанет работать.

2. Адрес сервера

https://needed-server-a5a7qooeca-uc.a.run.app

Каждый запрос несёт токен в заголовке:

Authorization: Bearer dev_…

3. Узнай, кому отправлять

Проще всего в меню: правый клик по человеку → Скопировать id для устройства. Или спроси список у сервера:

GET /v1/contacts
{
  "contacts": [
    { "id": "DXEjjB91rJYQvgaGGdsx", "label": "Мама", "position": 0, "close": true, … },
    { "id": "Qm3LkT0pZ8aW2vRcN5yE", "label": "Аня",  "position": 1, "close": false, … }
  ]
}

id — это идентификатор человека в твоём списке. Сопоставь кнопкам на устройстве нужные id, например «левая лапа — мама». Имена (label) видишь только ты. position — порядок в списке, close — человек в близких (кружок наверху меню на Mac): их удобно ставить первыми.

4. Принимай сигналы

Открой WebSocket:

wss://needed-server-a5a7qooeca-uc.a.run.app/v1/stream
Authorization: Bearer dev_…

Сначала придёт:

{ "type": "ready" }

Потом на каждое сердечко, которое тебе прислали:

{ "type": "event", "event": { "id": "ev_8f2c", "type": "signal", "v": 1, "at": "2026-10-10T14:00:00Z" } }

Сигналы от незнакомцев (кто-то нажал «Кому-нибудь на свете») устройство по умолчанию не получает. Чтобы получать, подключайся к /v1/stream?anyone=true. Такой сигнал выглядит как обычный и приходит не чаще раза в час.

5. Отправляй сигналы

POST /v1/events
Content-Type: application/json

{ "type": "signal", "to": { "contact": "DXEjjB91rJYQvgaGGdsx" } }

Ответ — 202 Accepted. Если нажать много раз подряд, человеку придёт не больше одного сигнала в секунду.

Сердечко живёт только в моменте: его увидит тот, кто сейчас онлайн. Если человек не в сети, оно никуда не копится и не придёт позже, так и задумано. Поэтому 202 значит «принято», а не «доставлено».

Кнопка «Кому-нибудь на свете» есть и у устройства: сердечко уйдёт случайному человеку онлайн, который принимает сигналы наугад.

{ "type": "signal", "to": { "anyone": true } }

Так можно раз в час, считается каждая попытка. Раньше часа сервер ответит 429. Если онлайн никого нет, ответ всё равно 202: сервер не говорит, сколько людей в сети.

Коды ошибок: 401 — токен неверный или устройство отключено, 403 — устройству не разрешено это действие (например, оно только принимает), 404 — такого человека нет в твоём списке, 429 — наугад уже отправляли в этот час.

Пример: игрушка на ESP32

Что понадобится:

Библиотеки для Arduino IDE, через Library Manager: WebSockets (Markus Sattler), ArduinoJson версии 7 (Benoit Blanchon; с версией 6 скетч не соберётся), Adafruit NeoPixel.

#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <WebSocketsClient.h>
#include <ArduinoJson.h>
#include <Adafruit_NeoPixel.h>

// ---- настрой под себя -------------------------------------------------
const char* WIFI_SSID  = "моя-сеть";
const char* WIFI_PASS  = "пароль";
const char* TOKEN      = "dev_…";                  // из «Настройки → Устройства»
const char* CONTACT_ID = "…";                      // кому шлёт кнопка
const uint32_t COLOR   = 0xE86A9A;                 // цвет свечения

const char* HOST = "needed-server-a5a7qooeca-uc.a.run.app";
const int LED_PIN = 5, LED_COUNT = 12, BUTTON_PIN = 4;
// -----------------------------------------------------------------------

Adafruit_NeoPixel ring(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
WebSocketsClient ws;

void fill(int level) {
  const uint8_t r = COLOR >> 16, g = COLOR >> 8, b = COLOR;
  ring.fill(ring.Color(r * level / 255, g * level / 255, b * level / 255));
  ring.show();
}

// Три мягкие вспышки, как на Mac.
void glow() {
  for (int flash = 0; flash < 3; flash++) {
    for (int i = 0; i <= 255; i += 5) { fill(i); delay(5); }
    for (int i = 255; i >= 40; i -= 5) { fill(i); delay(5); }
  }
  ring.clear(); ring.show();
}

void onMessage(WStype_t type, uint8_t* payload, size_t length) {
  if (type != WStype_TEXT) return;
  JsonDocument msg;
  if (deserializeJson(msg, payload, length)) return;
  // Незнакомые типы и версии пропускаем.
  if (msg["type"] == "event" && msg["event"]["type"] == "signal" && msg["event"]["v"] == 1) {
    glow();
  }
}

void sendSignal() {
  WiFiClientSecure tls;
  tls.setInsecure();  // для прототипа; в готовом устройстве задай корневой сертификат через setCACert
  HTTPClient http;
  http.begin(tls, String("https://") + HOST + "/v1/events");
  http.addHeader("Authorization", String("Bearer ") + TOKEN);
  http.addHeader("Content-Type", "application/json");
  int code = http.POST(String("{\"type\":\"signal\",\"to\":{\"contact\":\"") + CONTACT_ID + "\"}}");
  http.end();
  // Короткая белая вспышка: улетело.
  if (code == 202) { ring.fill(ring.Color(40, 40, 40)); ring.show(); delay(150); ring.clear(); ring.show(); }
}

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  ring.begin(); ring.setBrightness(120); ring.clear(); ring.show();

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(300);

  ws.beginSSL(HOST, 443, "/v1/stream");
  ws.setExtraHeaders((String("Authorization: Bearer ") + TOKEN).c_str());
  ws.onEvent(onMessage);
  ws.setReconnectInterval(5000);      // переподключение после обрыва
  ws.enableHeartbeat(30000, 5000, 2); // ping раз в 30 с
}

void loop() {
  ws.loop();
  static bool wasDown = false;
  bool down = digitalRead(BUTTON_PIN) == LOW;
  if (down && !wasDown) sendSignal();
  wasDown = down;
  delay(10);
}

Залей скетч, и игрушка будет светиться, когда о тебе думают. А нажатие на лапу отправит сердечко маме.

Скетч занимает почти всю память, отведённую под программу по умолчанию. Если будешь его дописывать, выбери в Arduino IDE Tools → Partition Scheme → Huge APP.

Куда развить

Безопасность