Skip to content

Commit e0de2c9

Browse files
Merge pull request #1 from shailensobhee/udpates_cjkas_20260424
Merge cjkas upstream v3.0.13, bump to v3.2.0
2 parents 0b81541 + 0155cd9 commit e0de2c9

12 files changed

Lines changed: 329 additions & 59 deletions

File tree

data-src/appversion

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.1.0
1+
3.2.0

data-src/index.html

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
<meta name="apple-mobile-web-app-title" content="ESPSomfy RTS App">
99
<meta name="apple-mobile-web-app-status-bar-style" content="black">
1010

11-
<link rel="stylesheet" href="main.css?v=3.1.0c" type="text/css" />
12-
<link rel="stylesheet" href="widgets.css?v=3.1.0c" type="text/css" />
13-
<link rel="stylesheet" href="icons.css?v=3.1.0c" type="text/css" />
11+
<link rel="stylesheet" href="main.css?v=3.2.0c" type="text/css" />
12+
<link rel="stylesheet" href="widgets.css?v=3.2.0c" type="text/css" />
13+
<link rel="stylesheet" href="icons.css?v=3.2.0c" type="text/css" />
1414
<link rel="icon" type="image/png" href="favicon.png" />
1515

1616
<!-- iPad retina icon -->
@@ -114,7 +114,7 @@
114114
rel="apple-touch-startup-image">
115115

116116

117-
<script type="text/javascript" src="index.js?v=3.1.0c"></script>
117+
<script type="text/javascript" src="index.js?v=3.2.0c"></script>
118118
</head>
119119
<body>
120120
<div id="divContainer" class="container main" data-auth="false">
@@ -1027,7 +1027,7 @@ <h1 style="text-align: center;"><img src="icon.png" style="width:50px;float:left
10271027
</div>
10281028
</div>
10291029
<div id="divFrameLog" class="subtab-content frame-log" style="display:none;margin:0px;padding:0px;">
1030-
<div class="frame-header"><span>Key</span><span>Address</span><span>Command</span><span>Code</span><span>RSSI</span><span>Bits</span><span style="text-align:center;width:77px;">Time</span></div>
1030+
<div class="frame-header"><span>Key</span><span>Address</span><span>Command</span><span>Code</span><span>RSSI</span><span>Bits</span><span style="text-align:center;width:77px;">Time</span><span>Raw</span></div>
10311031
<div id="divFrames" class="frame-list"></div>
10321032
<div class="button-container" style="text-align:center">
10331033
<button type="button" class="btnCopyFrame" style="display:inline-block;width:44%;" onclick="somfy.framesToClipboard();">Copy</button>
@@ -1061,21 +1061,23 @@ <h1 style="text-align: center;"><img src="icon.png" style="width:127px;margin-le
10611061
<label for="fldPinEntry" style="margin-top:7px;">Enter Pin</label>
10621062
</div>
10631063
</div>
1064-
<div id="divLoginPassword" style="display:none;" onkeyup="if (event.code === 'Enter') security.login();">
1064+
<form id="frmUnauthLogin" action="/login" method="post" onsubmit="security.login(event); return false;">
1065+
<div id="divLoginPassword" style="display:none;">
10651066
<div class="field-group">
1066-
<input id="fldLoginUsername" name="username" type="text" data-bind="login.username" length=32 placeholder="Username" />
1067+
<input id="fldLoginUsername" name="username" type="text" autocomplete="username" data-bind="login.username" length=32 placeholder="Username" />
10671068
<label for="fldLoginUsername">Username</label>
10681069
</div>
10691070
<div class="field-group">
1070-
<input id="fldLoginPassword" name="password" type="password" data-bind="login.password" length=32 placeholder="Password" />
1071+
<input id="fldLoginPassword" name="password" type="password" autocomplete="current-password" data-bind="login.password" length=32 placeholder="Password" />
10711072
<label for="fldLoginPassword">Password</label>
10721073
</div>
10731074
</div>
10741075
<div style="text-align:center;"><span id="spanLoginMessage" style="color:red"></span></div>
10751076
<div id="loginButtons" class="button-container" style="display:none;text-align:center;">
1076-
<button id="btnLogin" type="button" onclick="security.login();" style="display:inline-block;width:42%;">Login</button>
1077+
<button id="btnLogin" type="submit" style="display:inline-block;width:42%;">Login</button>
10771078
<button id="btnCancelLogin" type="button" onclick="security.cancelLogin();" style="display:none;width:42%;">Cancel</button>
10781079
</div>
1080+
</form>
10791081
</div>
10801082
</div>
10811083
<script type="text/javascript">

data-src/index.js

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,8 @@ class Security {
12321232
document.getElementById('divUnauthenticated').style.display = 'none';
12331233
document.getElementById('divContainer').dispatchEvent(evt);
12341234
}
1235-
login() {
1235+
login(evt) {
1236+
if (evt && evt.preventDefault) evt.preventDefault();
12361237
console.log('Logging in...');
12371238
let pnl = document.getElementById('divUnauthenticated');
12381239
let msg = pnl.querySelector('#spanLoginMessage');
@@ -1245,7 +1246,7 @@ class Security {
12451246
for (let i = 0; i < 4; i++) {
12461247
pin += sec.pin[`d${i}`];
12471248
}
1248-
if (pin.length !== 4) return;
1249+
if (pin.length !== 4) return false;
12491250
break;
12501251
case 2:
12511252
break;
@@ -1256,6 +1257,16 @@ class Security {
12561257
else {
12571258
console.log(log);
12581259
if (log.success) {
1260+
if (sec.type === 2 && window.PasswordCredential && navigator.credentials) {
1261+
try {
1262+
const cred = new PasswordCredential({
1263+
id: sec.username,
1264+
password: sec.password,
1265+
name: sec.username
1266+
});
1267+
navigator.credentials.store(cred);
1268+
} catch (e) { /* ignore; browsers without support fall back to the form-submit heuristic */ }
1269+
}
12591270
if (typeof socket === 'undefined' || !socket) (async () => { await initSockets(); })();
12601271
//ui.setMode(mode);
12611272

@@ -1264,21 +1275,22 @@ class Security {
12641275
document.getElementById('divContainer').setAttribute('data-auth', true);
12651276
this.apiKey = log.apiKey;
12661277
this.authenticated = true;
1267-
let evt = new CustomEvent('afterlogin', { detail: { authenticated: true } });
1268-
document.getElementById('divContainer').dispatchEvent(evt);
1278+
let evt2 = new CustomEvent('afterlogin', { detail: { authenticated: true } });
1279+
document.getElementById('divContainer').dispatchEvent(evt2);
12691280
}
12701281
else
12711282
msg.innerHTML = log.msg;
12721283
}
12731284
});
1285+
return false;
12741286
}
12751287
}
12761288
var security = new Security();
12771289

1278-
// let appVersion = 'v3.1.0'; // Default placeholder
1290+
// let appVersion = 'v3.2.0'; // Default placeholder
12791291
async function getAppVersion() {
12801292
try {
1281-
const response = await fetch('/appversion');
1293+
const response = await fetch('/appversion?v='+Date.now());
12821294
if (!response.ok) throw new Error('File not found');
12831295

12841296
const data = await response.text();
@@ -1288,14 +1300,14 @@ async function getAppVersion() {
12881300
// Trigger any UI updates here
12891301
} catch (error) {
12901302
console.error("Error loading App version:", error);
1291-
appVersion = 'v3.1.0'; // Default placeholder
1303+
appVersion = 'v3.2.0'; // Default placeholder
12921304
}
12931305
return appVersion;
12941306
}
12951307

12961308
class General {
1297-
initialized = false;
1298-
appVersion = getAppVersion();
1309+
initialized = false;
1310+
appVersion = '';
12991311
reloadApp = false;
13001312
init() {
13011313
if (this.initialized) return;
@@ -1472,7 +1484,10 @@ class General {
14721484
}
14731485
});
14741486
}
1475-
setAppVersion() { document.getElementById('spanAppVersion').innerText = this.appVersion; }
1487+
async setAppVersion() {
1488+
this.appVersion = await getAppVersion();
1489+
document.getElementById('spanAppVersion').innerText = this.appVersion;
1490+
}
14761491
setTimeZones() {
14771492
let dd = document.getElementById('selTimeZone');
14781493
dd.length = 0;
@@ -2938,7 +2953,8 @@ class Somfy {
29382953
proto = '-V';
29392954
break;
29402955
}
2941-
let html = `<span>${frame.encKey}</span><span>${frame.address}</span><span>${frame.command}<sup>${frame.stepSize ? frame.stepSize : ''}</sup></span><span>${frame.rcode}</span><span>${frame.rssi}dBm</span><span>${frame.bits}${proto}</span><span>${fnFmtTime(frame.time)}</span><div class="frame-pulses">`;
2956+
let rawCmdHex = (typeof frame.rawCmd === 'number') ? `0x${frame.rawCmd.toString(16).toUpperCase()}` : '';
2957+
let html = `<span>${frame.encKey}</span><span>${frame.address}</span><span>${frame.command}<sup>${frame.stepSize ? frame.stepSize : ''}</sup></span><span>${frame.rcode}</span><span>${frame.rssi}dBm</span><span>${frame.bits}${proto}</span><span>${fnFmtTime(frame.time)}</span><span>${rawCmdHex}</span><div class="frame-pulses">`;
29422958
for (let i = 0; i < frame.pulses.length; i++) {
29432959
if (i !== 0) html += ',';
29442960
html += `${frame.pulses[i]}`;

data-src/login.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<h1 style="text-align: center;"><img src="icon.png" style="width:127px;margin-left:1px;margin-top:-10px;" /></h1>
1515
<div id="divLoginPnl" class="login-content" style="position:relative;">
1616
<div style="max-width:270px;margin:0px auto;">
17-
<form id="frmLogin" action="/login" method="post" class="login-form">
17+
<form id="frmLogin" action="/login" method="post" class="login-form" onsubmit="general.login(event); return false;">
1818
<input id="fldPin" type="hidden" name="pin">
1919
<div id="divPinSecurity" style="display:none;">
2020
<div class="field-group" style="text-align:center;">
@@ -29,17 +29,17 @@ <h1 style="text-align: center;"><img src="icon.png" style="width:127px;margin-le
2929
</div>
3030
<div id="divPasswordSecurity" style="display:none;">
3131
<div class="field-group">
32-
<input id="fldUsername" name="username" type="text" data-bind="security.username" length=32 placeholder="Username">
32+
<input id="fldUsername" name="username" type="text" autocomplete="username" data-bind="security.username" length=32 placeholder="Username">
3333
<label for="fldUsername">Username</label>
3434
</div>
3535
<div class="field-group">
36-
<input id="fldPassword" name="password" type="password" data-bind="security.password" length=32 placeholder="Password">
36+
<input id="fldPassword" name="password" type="password" autocomplete="current-password" data-bind="security.password" length=32 placeholder="Password">
3737
<label for="fldPassword">Password</label>
3838
</div>
3939
</div>
4040
<div style="text-align:center;"><span id="spanLoginMessage" style="color:red"></span></div>
4141
<div class="button-container">
42-
<button id="btnLogin" type="button" value="Submit" onclick="general.login();">
42+
<button id="btnLogin" type="submit" value="Submit">
4343
Login
4444
</button>
4545
</div>

data-src/main.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,11 @@ div.frame-header > span {
832832
text-align: right;
833833
white-space:nowrap;
834834
}
835+
div.frame-row > span:nth-child(8),
836+
div.frame-header > span:nth-child(8) {
837+
width: 40px;
838+
text-align: center;
839+
}
835840

836841
div.frame-list > div:nth-child(2n+1) {
837842
background: beige;

docs/KNOWN_ISSUES.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Known Issues
2+
3+
## Concurrent mutation during chunked `/controller` response
4+
5+
**Status:** open
6+
**Introduced:** 2026-04-19, alongside the chunked-response conversion of `/controller` (see [`ControllerChunker` in src/Web.cpp](../src/Web.cpp) and the original crash report about `cbuf::resize` aborting `async_tcp`).
7+
8+
### Summary
9+
10+
The chunked streaming of `/controller` lazily reads `somfy.rooms`, `somfy.shades`, `somfy.groups`, and `somfy.repeaters` across many `async_tcp` callback invocations as the client's TCP send window opens. If another task mutates those arrays mid-stream, the rendered JSON can be internally inconsistent.
11+
12+
### Why this is strictly worse than the pre-chunked code
13+
14+
The old `handleController` ran the serializers synchronously inside a single request-handler invocation, so the full JSON was built in memory *before* any bytes were sent. Window of exposure: a few milliseconds.
15+
16+
The new `ControllerChunker` reads state as the response drains. On a slow link or under backpressure, that window is hundreds of ms to seconds.
17+
18+
### Concrete failure modes
19+
20+
1. A shade is deleted mid-stream (e.g. via `/deleteShade`) — it may appear in the `shades` array but be missing from a later group's `linkedShades`, or vice versa.
21+
2. `lastRollingCode` / position fields change between the `shades` pass and a later group's `linkedShades` pass — the client sees two values for the same shade in one document.
22+
3. A group's `linkedShades` list is mutated while the chunker iterates inside `S_GROUPS` — an entry is skipped or emitted twice.
23+
24+
### Fix options (pick one later)
25+
26+
- **(a) Document and accept.** In practice users rarely mutate shades while the config UI is loading. Zero code change.
27+
- **(b) FreeRTOS mutex around `somfy` reads/writes.** Acquire for the full duration of the chunked response and in every mutating path (RF RX, MQTT, web handlers). Cleanest but wide-reaching.
28+
- **(c) Up-front snapshot.** At handler start, copy the subset of `somfy` state the response will serialize into the `ControllerChunker`. Defeats part of the memory benefit — a full snapshot of shades + groups is close in size to the old growing cbuf. Could be reduced by snapshotting only minimal fields (IDs, names, rolling codes) and reading the rest live.
29+
30+
### Related
31+
32+
- Same exposure exists in any other endpoint converted to chunked responses next (`/discovery`, `/shades`). Resolve this issue before expanding the pattern.
33+
34+
## Silent truncation of large websocket events
35+
36+
**Status:** open
37+
**Location:** [`JsonSockEvent` in src/WResp.cpp](../src/WResp.cpp), buffer defined at [src/Sockets.cpp:45-46](../src/Sockets.cpp#L45-L46) as `g_response[MAX_SOCK_RESPONSE]` = 2048 bytes.
38+
39+
### Summary
40+
41+
Socket events are built into a fixed 2 KB static buffer. On overflow, [`JsonSockEvent::_safecat`](../src/WResp.cpp) logs an error and returns without appending — the event is sent truncated, producing malformed socket.io text that the client drops silently.
42+
43+
Unlike the `/controller` HTTP crash, this path does **not** abort — there is no growing cbuf and no `new[]` on the send path. Per-client frame allocations inside `AsyncWebSocket` are bounded by the 2 KB buffer size and have their own overflow guard (queue drop / client disconnect).
44+
45+
### Concrete failure modes
46+
47+
1. A single event serializing a fully-populated shade (~1.3–1.5 KB for a shade with all `SOMFY_MAX_LINKED_REMOTES` = 7 populated) gets close to the 2 KB limit. Any additional fields or long names push it over and the JSON is silently cut mid-value.
48+
2. Any event that loops over a collection (e.g. frequency-scan results, batch emits in `Somfy.cpp` around lines 1870–1975) can exceed 2 KB depending on size, with no indication to the client beyond the ESP-side `ESP_LOGE` line.
49+
50+
### Fix options (pick one later)
51+
52+
- **(a) Fail loud.** Keep truncation but emit a sentinel/error frame so the client knows the event was lost, instead of sending a malformed one.
53+
- **(b) Split large events across frames.** Use the socket.io ack/chunk pattern to send an event in multiple frames when it wouldn't fit. Requires matching client-side reassembly.
54+
- **(c) Raise `MAX_SOCK_RESPONSE`.** Cheapest, but just pushes the limit — doesn't eliminate the failure mode.
55+
56+
### Related
57+
58+
- Not the same code path as the `/controller` crash. Solve independently.
59+
- Worth grepping for `JsonSockEvent` usages that iterate collections (see references in `Somfy.cpp`, `ESPNetwork.cpp`, `GitOTA.cpp`) to identify the most at-risk events.

src/ConfigSettings.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#include "WResp.h"
88

99
#ifndef FW_VERSION
10-
#define FW_VERSION "v3.1.0" // Fallback if app_version.py script fails
10+
#define FW_VERSION "v3.2.0" // Fallback if app_version.py script fails
1111
#endif
1212

1313
enum class conn_types_t : byte {

src/Somfy.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ void somfy_frame_t::decodeFrame(byte* frame) {
161161
this->checksum = decoded[1] & 0b1111;
162162
this->encKey = decoded[0];
163163
// Lets first determine the protocol.
164-
this->cmd = (somfy_commands)((decoded[1] >> 4));
164+
this->rawCmd = decoded[1] >> 4;
165+
this->cmd = (somfy_commands)(this->rawCmd);
165166
if(this->cmd == somfy_commands::RTWProto) {
166167
if(this->encKey >= 160) {
167168
this->proto = radio_proto::RTS;
@@ -173,7 +174,7 @@ void somfy_frame_t::decodeFrame(byte* frame) {
173174
}
174175
else if(this->encKey >= 133) {
175176
this->proto = radio_proto::RTW;
176-
this->cmd = this->encKey == 133 ? somfy_commands::My : (somfy_commands)(this->encKey - 133);
177+
this->cmd = (somfy_commands)(this->encKey - 132);
177178
}
178179
}
179180
else this->proto = radio_proto::RTS;
@@ -4429,6 +4430,11 @@ bool Transceiver::receive(somfy_rx_t *rx) {
44294430
//Serial.printf("Processing receive %d\n", rx_queue.length);
44304431
rx_queue.pop(rx);
44314432
this->frame.decodeFrame(rx);
4433+
if(this->frame.valid) {
4434+
ESP_LOGI(TAG, "RX ADDR:%d CMD:%s RAW_CMD:0x%X KEY:0x%02X PROTO:%u",
4435+
this->frame.remoteAddress, translateSomfyCommand(this->frame.cmd).c_str(),
4436+
this->frame.rawCmd, this->frame.encKey, (uint8_t)this->frame.proto);
4437+
}
44324438
this->emitFrame(&this->frame, rx);
44334439
return this->frame.valid;
44344440
}
@@ -4442,6 +4448,7 @@ void Transceiver::emitFrame(somfy_frame_t *frame, somfy_rx_t *rx) {
44424448
json->addElem("address", (uint32_t)frame->remoteAddress);
44434449
json->addElem("rcode", (uint32_t)frame->rollingCode);
44444450
json->addElem("command", translateSomfyCommand(frame->cmd).c_str());
4451+
json->addElem("rawCmd", frame->rawCmd);
44454452
json->addElem("rssi", (int32_t)frame->rssi);
44464453
json->addElem("bits", rx->bit_length);
44474454
json->addElem("proto", static_cast<uint8_t>(frame->proto));

src/Somfy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ struct somfy_frame_t {
189189
uint8_t bitLength = 56;
190190
uint16_t pulseCount = 0;
191191
uint8_t stepSize = 0;
192+
uint8_t rawCmd = 0;
192193
void print();
193194
void encode80BitFrame(byte *frame, uint8_t repeat);
194195
byte calc80Checksum(byte b0, byte b1, byte b2);

src/WResp.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ void JsonSockEvent::_safecat(const char *val, bool escape) {
3535
else strcat(this->buff, val);
3636
if(escape) strcat(this->buff, "\"");
3737
}
38+
void BufferedJsonFormatter::setBuffer(char *b, size_t sz) {
39+
this->buff = b;
40+
this->buffSize = sz;
41+
this->buff[0] = 0;
42+
this->_nocomma = true;
43+
this->_objects = 0;
44+
this->_arrays = 0;
45+
}
46+
size_t BufferedJsonFormatter::length() const {
47+
return strlen(this->buff);
48+
}
3849
void AsyncJsonResp::beginResponse(AsyncWebServerRequest *request, char *buff, size_t buffSize) {
3950
this->_request = request;
4051
this->buff = buff;

0 commit comments

Comments
 (0)