于 2026 年 4 月 13 日定案。供实现使用的工作文档。
所有敏感数据只保存 在手机上。主人可以选择让其中一部分经过服务器——并会收到明确警告。
| 数据 | 默认 | 可选:经服务器 |
|---|---|---|
| 坐标(lat, lon) | 仅在手机上 | 可以分享给访客(带警告) |
| Wi-Fi 指纹(BSSID[]) | 仅在手机上 | 从不传输 |
| Webhook 地址与密钥 | 仅在手机上 | 可以存放在服务器上(带警告) |
| 半径、时间、确认层级 | 经服务器 | — |
添加对象时,主人先选连接类型。随后打开该类型的设置窗口。
| Icon | 名称 | 工作原理 | Feedback | Example |
|---|---|---|---|---|
| 📞 | Call | 从主人手机发起的电话呼叫 | 没有(电话要么拨出,要么没有) | 道闸、对讲门禁 |
| 🌐 | URL(Webhook) | 向设备地址发出的 HTTP 请求,可来自手机或 Entrixy 服务器 | 有——带状态的 JSON 响应 | 带 API 的智能锁、Tasmota、Shelly Cloud |
| 📟 | 联网控制器(WS) | 通过 WebSocket 发给已连接控制器的命令 | 有——通过 WebSocket 返回的应答 | 自制 ESP32 / ESP8266、Shelly Plus 1、NodeMCU |
| 📶 | BLE 控制器 | 手机与控制器之间的蓝牙直连通道,无需互联网 | 有——通过 BLE 返回的应答(notify) | 装在没有 Wi-Fi 的门口、用电池供电的控制器 |
后两种类型的固件配置器: /controller/ (BLE 与“联网”,在浏览器中)。
在用户看来,各种类型长得都一样:一张卡片、一个“呼叫”按钮、一个状态。不同的只是投递机制。
Webhook 设置窗口提供一个切换项,利弊直接写在屏幕上:
| Mode | 地址与密钥在哪里 | 服务器知道地址吗? |
|---|---|---|
| 从手机 | 主人手机上的 Prefs | No |
| 从服务器 | 表 numbers 数据库中的 | Yes |
安全性:HMAC 签名。设备与 Entrixy 共享一个 webhook_secret.
POST https://device-url.com/open
Content-Type: application/json
{
"action": "open",
"object_id": 42,
"timestamp": 1713020000,
"nonce": "a1b2c3",
"signature": "hmac-sha256(secret, timestamp + nonce + action)"
}
设备会核验签名有效、时间戳不超过 30 秒、nonce 不是重复值。一切正常就执行并回复:
{"status": "ok", "message": "Door opened"}
or
{"status": "error", "message": "Lock jammed"}
状态会在应用中显示给用户。
The guest presses Call
→ the server receives the request
→ sends it to the owner over the WebSocket: {type: "do_webhook", ...}
→ the owner's phone makes the HTTP call to the device
→ receives the JSON response
→ sends it to the server: {type: "webhook_result", status, message}
→ the server passes it to the guest
→ both see the status in the log
两种模式下,主人和访客手机上的日志都会记下:谁、何时、哪个对象、返回了什么状态。
控制器——支持 ESP32、ESP32-S3、ESP32-C3 与 ESP8266——没有公网 IP。它与 Entrixy 服务器保持一条出站 WebSocket 连接,跟主人的手机一样。
固件与配置器位于 /esp/socket/。源码: ESP32, ESP8266.
// Device → server: connect
{"type": "device_hello", "device_key": "xyz789", "device_secret": "..."}
// Server → device: OK
{"type": "device_ok"}
// Server → device: command
{"type": "device_command", "action": "open", "command_id": "abc123"}
// Device → server: result
{"type": "device_response", "command_id": "abc123", "status": "ok", "message": "Opened"}
服务器通过各自的 WebSocket 连接把状态转发给客户端——主人与访客。
device_key + device_secretwss://entrixy.com/ws对象卡片会显示设备的连接状态,在线或离线,就像访客看到主人的状态一样。
基于 ESP32、S3、C3、C6 或 H2 的控制器,触发时既不需要 Wi-Fi 也不需要服务器。它处于深度睡眠,每秒醒来约 200 毫秒,广播一条带签名计数器的 BLE 通告。当持有有效密钥的手机靠近,Android——或前台运行的应用——捕获该通告,通过 GATT 连接,发送带一次性 nonce 的 fire 命令,控制器便以短脉冲闭合继电器。对于双稳态驱动和门锁,另有一种“开”“关”分离并跟踪位置的模式,其中“开/关”以加密字节随通告一同发送。
Entrixy 服务器不参与这条通道。 只有在创建访客密钥时才需要它:主人签署一个 GuestToken,经端到端加密通道交给访客。此后访客即可离线工作。
完整的协议规范、位布局和测试向量见 entrixy.com/ble。威胁模型见 单独的文档.
同一台驱动装置可以同时装 BLE 与 WebSocket——在应用里显示为两个独立对象。BLE 用于近处无网开启,WebSocket 用于你人在远方的时候。
entrixy-pair)固件与配置器位于 /esp/ble/。源码: /esp32-example/.
在对象卡片上:灰色表示空口不可见,太远或已关闭;橙色表示可见但自动化尚未触发;绿色表示已触发或正在触发;灰色带绿边表示刚刚触发过,点一下可以再开一次。
CREATE TABLE devices ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, host_id INT UNSIGNED NOT NULL, device_key VARCHAR(64) NOT NULL UNIQUE, secret_hash VARCHAR(64) NOT NULL, label VARCHAR(128) DEFAULT '', last_seen DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
In server.php 新增第三种 WebSocket 角色: device (与 host 与 guest).
GPS 与 Wi-Fi 各自独立工作。两者都配置时,谁先满足谁触发。两次触发之间的公共冷却为 20 秒。
模式 1:Wi-Fi 画面(指纹)
模式 2:已连接到网络
WifiManager.getScanResults() ——缓存结果,不做新的扫描;我们在每次 GPS 更新时免费调用它NetworkCallback,完全不做扫描
wifi_trigger_<actionKey> = {
"mode": "fingerprint" | "connect",
"bssids": ["AA:BB:CC:DD:EE:FF", ...],
"min_match": 5,
"connect_bssid": "AA:BB:CC:DD:EE:FF",
"connect_ssid": "TP-Link_5G",
"enabled": true
}
由主人决定是否需要确认。 具体如何确认身份由客户端决定,取决于设备提供了什么。
| Level | 用户看到什么 | Example |
|---|---|---|
| Automatic | 什么都不用做——对象自行开启 | 道闸、大门 |
| Confirm | 通知栏里的提醒:“打开[对象]?”→ 点一下 | 车库、小门,以及防误触发 |
| 确认身份 | 生物识别、手机 PIN 或应用 PIN | 入户门 |
“确认”并不是多余的一步。 通知会在恰当的时刻自己出现在通知栏。不用找应用、不用解锁、不用翻出对象。点一下,门就开了。
BiometricPromptKeyguardManager主人设定“允许时段:07:00–23:00”。窗口之外,条件会被静默忽略。跨夜区间同样有效(22:00–06:00)。
| Setting | 主人(自己的) | 主人 → 访客 | 访客 |
|---|---|---|---|
| 对象类型 | Chooses | 传递 | Receives |
| Coordinates | 由其设定 | 可选(带警告) | 由其设定或接收 |
| GPS 半径 | 由其设定 | 传递 | Receives |
| Wi-Fi 指纹 | Scans | Never | 自行扫描 |
| Wi-Fi 最少匹配数 | 由其设定 | 传递 | Receives |
| 时间窗口 | 由其设定 | 传递 | Receives |
| 确认 | 由其设定 | 传递 | Receives |
| GPS 开/关 | yes | — | yes |
| Wi-Fi 开/关 | yes | — | yes |
第一步是选择连接类型:
┌────────────────────────────┐ │ Connection type │ │ │ │ 📞 Call │ │ Opening with a phone │ │ call │ │ │ │ 🌐 URL │ │ Sending a command to │ │ a device address │ │ │ │ 📟 Device │ │ Connected through Entrixy │ │ (ESP32, Arduino) │ └────────────────────────────┘
选定之后,打开该类型的设置窗口。
┌──────────────────────────────────────────┐ │ Corner barrier 📶 3/5 312m │ │ Mine • ● │ ├──────────────────────────────────────────┤ │ [Edit] [Icon] │ │ ───────────────────────── │ │ [📍 Geo] [📶 Wi-Fi] │ └──────────────────────────────────────────┘
◉ 按 Wi-Fi 画面
○ 按连接到网络
下方:时间窗口与确认,与“定位”共用
| Trigger | Indicator |
|---|---|
| GPS | "312 m" |
| Wi-Fi 指纹 | 📶 信号格(按匹配数 0–4) |
| Wi-Fi 连接 | 📶 绿色或灰色 |
| Webhook/Device | ● 绿色(在线)或灰色(离线) |
内部套餐。不向用户展示,但限制照样生效。
| 默认套餐 | Limit |
|---|---|
| 对象(numbers) | 10 |
| 访客(user_keys) | 10 |
超出时给出温和的提示:“对象数量有限。如需提高,请联系支持。”
CREATE TABLE plans (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
max_numbers INT NOT NULL DEFAULT 10,
max_keys INT NOT NULL DEFAULT 10
);
INSERT INTO plans (name) VALUES ('default');
ALTER TABLE hosts ADD COLUMN plan_id INT UNSIGNED DEFAULT 1;
-- Plans
CREATE TABLE plans (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
max_numbers INT NOT NULL DEFAULT 10,
max_keys INT NOT NULL DEFAULT 10
);
INSERT INTO plans (name) VALUES ('default');
ALTER TABLE hosts ADD COLUMN plan_id INT UNSIGNED DEFAULT 1;
-- Object type and settings
ALTER TABLE numbers
ADD COLUMN type ENUM('call','webhook','device') DEFAULT 'call',
ADD COLUMN radius INT DEFAULT 80,
ADD COLUMN time_from TIME NULL,
ADD COLUMN time_to TIME NULL,
ADD COLUMN security_level ENUM('auto','confirm','identity') DEFAULT 'auto',
ADD COLUMN geo_available TINYINT(1) DEFAULT 1,
ADD COLUMN wifi_available TINYINT(1) DEFAULT 1,
ADD COLUMN wifi_min_match INT DEFAULT 5,
ADD COLUMN share_lat DOUBLE NULL,
ADD COLUMN share_lon DOUBLE NULL,
ADD COLUMN webhook_url VARCHAR(512) NULL,
ADD COLUMN webhook_secret VARCHAR(64) NULL,
ADD COLUMN webhook_mode ENUM('phone','server') DEFAULT 'phone',
ADD COLUMN device_id INT UNSIGNED NULL;
-- Devices (ESP32)
CREATE TABLE devices (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
host_id INT UNSIGNED NOT NULL,
device_key VARCHAR(64) NOT NULL UNIQUE,
secret_hash VARCHAR(64) NOT NULL,
label VARCHAR(128) DEFAULT '',
last_seen DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
share_lat / share_lon ——只有当主人启用了“与访客分享坐标”时才填写。
webhook_url / webhook_secret ——仅在服务器模式下填写。手机模式下它们存放在 Prefs 里。
主人给对象设置了自定义图标,访客应当能看到。但是 图标并不存放在服务器上 ——服务器只当邮差:送到即忘。
设置时图标会立刻缩小为 64×64 PNG (约 3–5 KB),并本地存于 Prefs 或文件中。
1. The guest connects
→ guest_ok carries has_avatar: true for objects that have an icon
2. The guest client checks: has_avatar=true but no icon locally?
→ Yes → it asks the server
3. The server has no icon
→ it asks the owner over the WebSocket: {type: "avatar_request", number_id: 42}
4. The owner's phone
→ reads the local file
→ sends: {type: "avatar_data", number_id: 42, data: "base64..."}
5. The server passes it to the guest
→ {type: "avatar_data", number_id: 42, data: "base64..."}
6. The guest stores it locally and never asks again
7. The server stored nothing — the data merely passed through
| Situation | Behaviour |
|---|---|
| 主人离线 | 在主人上线之前,访客看到的是默认图标。 |
| 50 位访客同时索取 | 50 × 5 KB = 250 KB 走 WebSocket——微不足道。 |
| 主人更换图标 | has_avatar_hash 变化,访客会重新索取 |
| 主人删除图标 | has_avatar: false,访客显示默认图标 |
| Subscribers | 同时在线(约 15%) | WebSocket 连接数 | Server |
|---|---|---|---|
| 1 000 | ~150 | ~150 | Easy |
| 10 000 | ~1 500 | ~1 500 | Easy |
| 50 000 | ~7 500 | ~7 500 | 单个工作进程已到极限 |
| 100 000 | ~15 000 | ~15 000 | 2–3 个工作进程 |
单个工作进程上的 Workerman:5000–10000 条并发连接,每条约 20 KB。
在 MySQL,而不在 WebSocket。可每天 25 万次插入——5 万订户 × 5 次呼叫——对 SSD 上的 MySQL 来说不值一提。
Entrixy —— 最终架构 v2,2026 年 4 月 13 日定案