Skip to content

Commit 809394d

Browse files
committed
feat: local ctrl
1 parent e335ea7 commit 809394d

8 files changed

Lines changed: 287 additions & 36 deletions

File tree

README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,77 @@ mySwitch.sendPushNotification("Hello SinricPro!");
150150
* [Settings](https://github.com/sinricpro/esp8266-esp32-sdk/tree/master/examples/Settings)
151151
---
152152

153+
## Local Control (LAN/UDP)
154+
155+
Starting with SDK v4.2.0, SinricPro devices can be controlled directly on the
156+
local network without a cloud round-trip. When the mobile app cannot reach the
157+
cloud within its timeout window, or when the user has selected "Local-first"
158+
mode, the app sends a signed UDP command directly to the device IP address and
159+
port. The same HMAC-SHA256 credentials used for cloud communication are reused
160+
for authentication — no separate provisioning step is needed.
161+
162+
### What it is
163+
164+
A UDP listener bound to the multicast group `224.9.9.9:3333` (also responds to
165+
unicast to the device IP on the same port). Incoming commands use exactly the
166+
same JSON payload shape as cloud WebSocket commands and are dispatched through
167+
the same capability callbacks (`onPowerState`, `onBrightness`, etc.).
168+
169+
### What changes for sketch authors
170+
171+
Nothing. Existing callback registrations work for both cloud and LAN commands
172+
automatically. Local control is on by default.
173+
174+
### What changes for app developers
175+
176+
LAN availability is signaled implicitly: the app gates local-control on the
177+
device reporting a non-empty `deviceIp` in the cloud model combined with an SDK
178+
version >= 4.2.0. No additional headers are required. After a successful LAN
179+
command the app posts the new state to `POST /api/v1/devices/:id/state` so the
180+
cloud and other clients stay in sync — the firmware does not echo UDP responses
181+
to the WebSocket.
182+
183+
### Compile-time flags
184+
185+
| Flag | Effect |
186+
|---|---|
187+
| `SINRICPRO_NOMDNS` | Disable the mDNS service announcement while keeping UDP active. |
188+
189+
Define this flag before including `SinricPro.h`, or pass it as a compiler
190+
flag (`-DSINRICPRO_NOMDNS`) in `platformio.ini` / Arduino IDE build flags.
191+
192+
### Network requirements
193+
194+
- The phone and device must be on the same LAN segment (or a routed LAN that
195+
passes multicast and allows unicast to device IP:3333).
196+
- UDP multicast is required only for mDNS-based discovery; unicast commands use
197+
the device IP reported by the cloud (`Device.lan.ip`).
198+
- Port 3333 UDP must not be firewalled between the phone and the device.
199+
- Enterprise or guest WiFi networks that block multicast/mDNS will fall back to
200+
the cloud-reported IP automatically.
201+
202+
### Security model
203+
204+
All UDP commands are signed with HMAC-SHA256 using the same `APP_SECRET` as
205+
cloud commands. The firmware verifies the signature before dispatching.
206+
207+
### mDNS service record
208+
209+
When `SINRICPRO_NOMDNS` is not defined the SDK announces:
210+
211+
```
212+
Service type : _sinricpro._udp.local.
213+
Port : 3333
214+
TXT records : deviceIds=<comma-separated device IDs>
215+
sdk=<SDK version, e.g. "4.2.0">
216+
udp=1
217+
```
218+
219+
Browse with `dns-sd -B _sinricpro._udp` (macOS) or
220+
`avahi-browse -r _sinricpro._udp` (Linux) to discover devices.
221+
222+
---
223+
153224
## Licensing and Credits
154225
* The Arduino IDE is developed and maintained by the Arduino team. The IDE is licensed under GPL.
155226
* [ArduinoJson](https://github.com/bblanchon/ArduinoJson) is licensed under the MIT.

changelog.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
# Changelog
22

3+
## Version 4.2.0
4+
5+
### New — Local Control (LAN/UDP)
6+
7+
1. **Completed UDP local-control path** — Commands sent directly to the device
8+
over the LAN are now fully dispatched through the same capability handlers
9+
as cloud commands. No sketch changes are required; existing `onPowerState`,
10+
`onBrightness`, etc. callbacks work for both cloud and LAN commands.
11+
12+
2. **Fixed multicast re-bind bug** (`SinricProUDP.h`).
13+
- ESP32: removed the unnecessary post-`endPacket()` `beginMulticast()` call.
14+
The underlying IDF socket correctly retains multicast group membership
15+
after a unicast send; the re-bind was harmless but wasteful.
16+
- ESP8266: introduced a dedicated TX socket (`WiFiUDP _udpTx`) so the
17+
multicast RX socket is never closed during a reply. The previous code
18+
called `endPacket()` on the multicast socket and then `beginMulticast()`
19+
again; some SDK versions unbind the socket from the multicast group on
20+
`endPacket()`, silently dropping all subsequent incoming packets.
21+
22+
3. **Compile-time gate**`SINRICPRO_NOMDNS` — define before including
23+
`SinricPro.h` to disable the mDNS service announcement while keeping UDP
24+
control active.
25+
26+
4. **mDNS service announcement** (`SinricProMDNS.h`, new file) — the SDK
27+
registers `_sinricpro._udp.local.` on `UDP_MULTICAST_PORT` (3333) with TXT
28+
records `deviceIds=<csv>`, `sdk=<version>`, `udp=1`. The record is updated
29+
each time the WebSocket reconnects so DHCP renewals are automatically
30+
reflected. Gate with `SINRICPRO_NOMDNS`.
31+
32+
### Behavior change
33+
34+
- **Cloud echo suppressed for `IF_UDP` requests** — when a command arrives over
35+
UDP the response is sent back over UDP only. The response is no longer also
36+
forwarded to the cloud via WebSocket. The mobile app is responsible for
37+
posting the new state that the cloud and other clients stay in sync. Users on the
38+
cloud path are unaffected.
39+
340
## Version 4.1.0
441
New:
542
1. The `sendSettingEvent` method has been added to SettingController.

src/SinricPro.h

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
#include "SinricProQueue.h"
1616
#include "SinricProSignature.h"
1717
#include "SinricProStrings.h"
18-
#include "SinricProUDP.h"
1918
#include "SinricProWebsocket.h"
2019
#include "Timestamp.h"
2120
#include "EventLimiter.h"
21+
#include "SinricProUDP.h"
22+
#include "SinricProMDNS.h"
2223
namespace SINRICPRO_NAMESPACE {
2324

2425
/**
@@ -144,6 +145,8 @@ class SinricProClass : public SinricProInterface {
144145

145146
SinricProDeviceInterface* getDevice(String deviceId);
146147

148+
String joinDeviceIds(char separator);
149+
147150
template <typename DeviceType>
148151
DeviceType& getDeviceInstance(String deviceId);
149152

@@ -155,6 +158,7 @@ class SinricProClass : public SinricProInterface {
155158

156159
WebsocketListener _websocketListener;
157160
UdpListener _udpListener;
161+
SinricProMDNS _mdnsListener;
158162
SinricProQueue_t receiveQueue;
159163
SinricProQueue_t sendQueue;
160164

@@ -262,6 +266,14 @@ void SinricProClass::begin(String appKey, String appSecret, String serverURL) {
262266
this->serverURL = serverURL;
263267
_begin = true;
264268
_udpListener.begin(&receiveQueue);
269+
#ifndef SINRICPRO_NOMDNS
270+
{
271+
String hostName = "sinricpro-" + WiFi.macAddress();
272+
hostName.replace(":", "");
273+
hostName.toLowerCase();
274+
_mdnsListener.begin(hostName, joinDeviceIds(',')); //mDNS TXT uses ',' (CSV).
275+
}
276+
#endif
265277
}
266278

267279
template <typename DeviceType>
@@ -322,6 +334,9 @@ void SinricProClass::handle() {
322334
if (!isConnected()) connect();
323335
_websocketListener.handle();
324336
_udpListener.handle();
337+
#ifndef SINRICPRO_NOMDNS
338+
_mdnsListener.handle();
339+
#endif
325340

326341
handleReceiveQueue();
327342
handleSendQueue();
@@ -484,7 +499,6 @@ void SinricProClass::handleInvalidSignatureRequest(JsonDocument& requestMessage,
484499
}
485500

486501
void SinricProClass::handleSendQueue() {
487-
if (!isConnected()) return;
488502
if (!timestamp.getTimestamp()) return;
489503
while (sendQueue.size() > 0) {
490504
DEBUG_SINRIC("[SinricPro:handleSendQueue()]: %i message(s) in sendQueue\r\n", sendQueue.size());
@@ -508,8 +522,12 @@ void SinricProClass::handleSendQueue() {
508522

509523
switch (rawMessage->getInterface()) {
510524
case IF_WEBSOCKET:
511-
DEBUG_SINRIC("[SinricPro:handleSendQueue]: Sending to websocket\r\n");
512-
_websocketListener.sendMessage(messageStr);
525+
if (isConnected()) {
526+
DEBUG_SINRIC("[SinricPro:handleSendQueue]: Sending to websocket\r\n");
527+
_websocketListener.sendMessage(messageStr);
528+
} else {
529+
DEBUG_SINRIC("[SinricPro:handleSendQueue]: Dropping WS message — not connected\r\n");
530+
}
513531
break;
514532
case IF_UDP:
515533
DEBUG_SINRIC("[SinricPro:handleSendQueue]: Sending to UDP\r\n");
@@ -523,17 +541,24 @@ void SinricProClass::handleSendQueue() {
523541
}
524542
}
525543

526-
void SinricProClass::connect() {
527-
String deviceList;
528-
int i = 0;
544+
String SinricProClass::joinDeviceIds(char separator) {
545+
String out;
546+
int i = 0;
529547
for (auto& device : devices) {
530-
String deviceId = device->getDeviceId();
531-
if (i > 0) deviceList += ';';
532-
deviceList += device->getDeviceId();
533-
i++;
548+
if (i++ > 0) out += separator;
549+
out += device->getDeviceId();
534550
}
551+
return out;
552+
}
535553

536-
_websocketListener.begin(serverURL, appKey, deviceList, &receiveQueue);
554+
void SinricProClass::connect() {
555+
_websocketListener.begin(serverURL, appKey, joinDeviceIds(';'), &receiveQueue);
556+
557+
#ifndef SINRICPRO_NOMDNS
558+
// Refresh mDNS TXT record on each (re-)connect so additions between
559+
// begin() and connect() are reflected. mDNS TXT uses ',' (CSV).
560+
_mdnsListener.update(joinDeviceIds(','));
561+
#endif
537562
}
538563

539564
void SinricProClass::stop() {

src/SinricProConfig.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
#pragma once
9-
/*
9+
/*
1010
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1111
* !! !!
1212
* !! WARNING: DON'T TOUCH ! !!
@@ -18,8 +18,6 @@
1818
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1919
*/
2020

21-
#pragma once
22-
2321
// Server Configuration
2422
#ifndef SINRICPRO_NOSSL
2523
#define WEBSOCKET_SSL
@@ -58,6 +56,10 @@
5856
#define UDP_MULTICAST_PORT 3333
5957
#endif
6058

59+
#ifndef SINRICPRO_NOMDNS
60+
#define SINRICPRO_MDNS_ENABLED
61+
#endif
62+
6163
// WebSocket Configuration
6264
#ifdef DEBUG_WIFI_ISSUE
6365
#define WEBSOCKET_PING_INTERVAL 10000

src/SinricProMDNS.h

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright (c) 2019 Sinric. All rights reserved.
3+
* Licensed under Creative Commons Attribution-Share Alike (CC BY-SA)
4+
*
5+
* This file is part of the Sinric Pro (https://github.com/sinricpro/)
6+
*/
7+
8+
#pragma once
9+
10+
#include "SinricProConfig.h"
11+
12+
#ifdef SINRICPRO_MDNS_ENABLED
13+
14+
#if defined(ESP8266)
15+
#include <ESP8266WiFi.h>
16+
#include <ESP8266mDNS.h>
17+
#elif defined(ESP32)
18+
#include <WiFi.h>
19+
#include <ESPmDNS.h>
20+
#endif
21+
22+
#include "SinricProVersion.h"
23+
#include "SinricProNamespace.h"
24+
#include "SinricProDebug.h"
25+
26+
namespace SINRICPRO_NAMESPACE {
27+
28+
/**
29+
* @brief Announces this device on the LAN via mDNS.
30+
*
31+
* Service: _sinricpro._udp.local. on UDP_MULTICAST_PORT.
32+
* TXT records:
33+
* deviceIds — semicolon-separated list of registered device IDs
34+
* sdk — SDK version string (e.g. "4.2.0")
35+
* udp — always "1"
36+
*
37+
* Gate with SINRICPRO_NOMDNS to remove entirely.
38+
*
39+
* Call begin() once after WiFi is connected and MDNS.begin() has been called
40+
* by user code (or it will be called internally here).
41+
* Call update() whenever the device list changes (e.g. after adding a device).
42+
*/
43+
class SinricProMDNS {
44+
public:
45+
void begin(const String& hostName, const String& deviceIds);
46+
void update(const String& deviceIds);
47+
void handle();
48+
private:
49+
bool _announced = false;
50+
void announce(const String& deviceIds);
51+
};
52+
53+
void SinricProMDNS::begin(const String& hostName, const String& deviceIds) {
54+
#if defined(ESP8266)
55+
// On ESP8266, MDNS.begin() must be called with the desired hostname.
56+
// If user code already called it, calling again is harmless.
57+
MDNS.begin(hostName.c_str());
58+
#elif defined(ESP32)
59+
MDNS.begin(hostName.c_str());
60+
#endif
61+
announce(deviceIds);
62+
}
63+
64+
void SinricProMDNS::announce(const String& deviceIds) {
65+
// Remove any existing SinricPro service record before re-adding so that
66+
// the TXT record reflects the current device list.
67+
if (_announced) {
68+
#if defined(ESP8266)
69+
// ESP8266mDNS 3.x: removeService(instance, service, protocol)
70+
// Passing nullptr as instance removes the default (unnamed) instance.
71+
MDNS.removeService(nullptr, "sinricpro", "udp");
72+
#elif defined(ESP32)
73+
// ESPmDNS on ESP32 does not expose removeService; restart the responder.
74+
// The service port and hostname are small so this is fast.
75+
MDNS.end();
76+
MDNS.begin(WiFi.getHostname());
77+
#endif
78+
}
79+
80+
MDNS.addService("sinricpro", "udp", UDP_MULTICAST_PORT);
81+
MDNS.addServiceTxt("sinricpro", "udp", "deviceIds", deviceIds.c_str());
82+
MDNS.addServiceTxt("sinricpro", "udp", "sdk", SINRICPRO_VERSION);
83+
MDNS.addServiceTxt("sinricpro", "udp", "udp", "1");
84+
_announced = true;
85+
DEBUG_SINRIC("[SinricPro:mDNS]: announced _sinricpro._udp.local. port=%d deviceIds=%s\r\n",
86+
UDP_MULTICAST_PORT, deviceIds.c_str());
87+
}
88+
89+
void SinricProMDNS::update(const String& deviceIds) {
90+
announce(deviceIds);
91+
}
92+
93+
void SinricProMDNS::handle() {
94+
#if defined(ESP8266)
95+
MDNS.update();
96+
#endif
97+
// On ESP32 the mDNS stack runs in a FreeRTOS task; no polling needed.
98+
}
99+
100+
} // SINRICPRO_NAMESPACE
101+
102+
#endif // SINRICPRO_MDNS_ENABLED

src/SinricProSignature.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <WString.h>
99
#include <ArduinoJson.h>
1010
#include "SinricProSignature.h"
11+
#include "SinricProDebug.h"
1112

1213
#if defined (ESP8266) || defined(ARDUINO_ARCH_RP2040)
1314
#include <bearssl/bearssl_hmac.h>

0 commit comments

Comments
 (0)