Своё устройство
Сигнал можно принимать не только на Mac. Подойдёт лампа, мягкая игрушка, браслет или любая плата с Wi‑Fi. Устройство подключается к серверу от твоего имени и умеет две вещи:
- принимать: когда тебе присылают сердечко, сервер сообщает об этом устройству, а оно светится, мурчит или машет ушами;
- отправлять: нажатие кнопки на устройстве отправляет сердечко человеку из твоего списка.
Ниже пример: игрушка на плате ESP32 с кольцом светодиодов и одной кнопкой.
Как это устроено
- В приложении на Mac ты создаёшь устройство и получаешь его токен.
- Устройство держит открытым WebSocket к серверу и получает по нему сигналы. Он работает только в одну сторону, от сервера к устройству: всё, что устройство пишет в него, сервер пропускает.
- Чтобы отправить сигнал, устройство делает один 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" } }
- Незнакомые сообщения пропускай. Если
typeилиvнезнакомые, просто ничего не делай. Так устройство переживёт новые типы событий. - Сообщения
{"type":"sync"}можно игнорировать. Это подсказка приложениям перечитать список контактов. - Переподключайся сам. Соединение иногда рвётся: сервер обновляется, Wi‑Fi пропадает. При обновлении сервер сам закрывает старые соединения, а пока новая версия запускается, на подключение может ответить
503. Переподключайся с паузой: хватит и постоянных 5 секунд, как в примере ниже, а лучше начинать с секунды и удваивать до минуты. - Шли ping раз в 30 секунд, чтобы соединение не считалось простаивающим. Если pong не пришёл за несколько секунд, соединение мертво, даже если сокет об этом ещё не знает: переподключайся.
Сигналы от незнакомцев (кто-то нажал «Кому-нибудь на свете») устройство по умолчанию не получает. Чтобы получать, подключайся к /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
Что понадобится:
- Плата. Любая ESP32, например ESP32‑DevKitC или ESP32‑C3 Super Mini. ESP32 видит только сети Wi‑Fi на 2,4 ГГц, а имя сети пиши с точностью до регистра: «GGWP» и «ggwp» — разные сети.
- Светодиоды. Кольцо WS2812B на 12–16 штук: вход данных на GPIO 5, питание на 5V и GND.
- Кнопка между GPIO 4 и GND, подтяжка включается внутри платы.
- Корпус. Мягкая игрушка: светодиоды в животике, кнопка в лапе.
Библиотеки для 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.
Куда развить
- Несколько кнопок. Каждой свой контакт и свой цвет: соответствие хранится на устройстве, сервер о цветах ничего не знает.
- Вибромотор или пищалка вместо светодиодов.
- Пауза ночью. ESP32 знает время через NTP и может не светиться с 23:00 до 8:00.
- Wi‑Fi и токен без перепрошивки. Через точку доступа для настройки, например библиотекой WiFiManager.
- Экран и выбор человека. Плата с экраном, например LilyGO T‑Display‑S3 AMOLED: двумя кнопками листаешь список, через 3 секунды сердечко улетает выбранному, последний пункт — «Кому-нибудь». Список берётся из
GET /v1/contactsи обновляется по{"type":"sync"}.
Безопасность
- Токен даёт право отправлять сердечки от твоего имени. Не публикуй скетч с настоящим токеном.
- Отключённое устройство теряет доступ сразу, включая открытое соединение.
- В готовом устройстве не оставляй
setInsecure(). Проверяй сертификат сервера черезsetCACert.