|
17 | 17 | #include <AsyncTCP.h> |
18 | 18 | #include <ESPAsyncWebServer.h> |
19 | 19 | #include <AsyncJson.h> |
| 20 | +#include <memory> |
20 | 21 |
|
21 | 22 | extern ConfigSettings settings; |
22 | 23 | extern SSDPClass SSDP; |
@@ -418,6 +419,194 @@ static void serializeGitRelease(GitRelease *rel, JsonFormatter &json) { |
418 | 419 | json.endObject(); |
419 | 420 | } |
420 | 421 |
|
| 422 | +// Memory-bounded streaming builder for /controller. Produces the JSON payload |
| 423 | +// one small unit at a time into a fixed 3KB buffer, so a chunked HTTP response |
| 424 | +// can drain it as the TCP send window allows — no growing cbuf in the async |
| 425 | +// response stream. |
| 426 | +class ControllerChunker { |
| 427 | +public: |
| 428 | + enum : uint8_t { S_HEADER, S_ROOMS, S_SHADES, S_GROUPS, S_REPEATERS, S_DONE }; |
| 429 | + static constexpr uint16_t LS_OPEN = 0xFFFF; |
| 430 | + |
| 431 | + BufferedJsonFormatter fmt; |
| 432 | + char unit[3072]; |
| 433 | + size_t unitLen = 0; |
| 434 | + size_t consumed = 0; |
| 435 | + uint8_t section = S_HEADER; |
| 436 | + uint16_t idx = 0; |
| 437 | + uint16_t lsIdx = LS_OPEN; |
| 438 | + bool firstInArray = true; |
| 439 | + bool firstInLSArray = true; |
| 440 | + |
| 441 | + size_t generate(uint8_t *out, size_t maxLen) { |
| 442 | + size_t written = 0; |
| 443 | + while(written < maxLen) { |
| 444 | + if(consumed < unitLen) { |
| 445 | + size_t n = unitLen - consumed; |
| 446 | + if(n > maxLen - written) n = maxLen - written; |
| 447 | + memcpy(out + written, unit + consumed, n); |
| 448 | + consumed += n; |
| 449 | + written += n; |
| 450 | + } else { |
| 451 | + produceNext(); |
| 452 | + if(unitLen == 0) return written; |
| 453 | + } |
| 454 | + } |
| 455 | + return written; |
| 456 | + } |
| 457 | + |
| 458 | +private: |
| 459 | + void resetFmt(size_t offset = 0) { |
| 460 | + unit[offset] = 0; |
| 461 | + fmt.setBuffer(unit + offset, sizeof(unit) - offset); |
| 462 | + } |
| 463 | + |
| 464 | + void produceNext() { |
| 465 | + consumed = 0; |
| 466 | + unitLen = 0; |
| 467 | + switch(section) { |
| 468 | + case S_HEADER: { |
| 469 | + resetFmt(); |
| 470 | + fmt.beginObject(); |
| 471 | + fmt.addElem("maxRooms", (uint8_t)SOMFY_MAX_ROOMS); |
| 472 | + fmt.addElem("maxShades", (uint8_t)SOMFY_MAX_SHADES); |
| 473 | + fmt.addElem("maxGroups", (uint8_t)SOMFY_MAX_GROUPS); |
| 474 | + fmt.addElem("maxGroupedShades", (uint8_t)SOMFY_MAX_GROUPED_SHADES); |
| 475 | + fmt.addElem("maxLinkedRemotes", (uint8_t)SOMFY_MAX_LINKED_REMOTES); |
| 476 | + fmt.addElem("startingAddress", (uint32_t)somfy.startingAddress); |
| 477 | + fmt.beginObject("transceiver"); |
| 478 | + fmt.beginObject("config"); |
| 479 | + serializeTransceiverConfig(fmt); |
| 480 | + fmt.endObject(); |
| 481 | + fmt.endObject(); |
| 482 | + fmt.beginObject("version"); |
| 483 | + serializeGitVersion(fmt); |
| 484 | + fmt.endObject(); |
| 485 | + fmt.beginArray("rooms"); |
| 486 | + unitLen = strlen(unit); |
| 487 | + section = S_ROOMS; |
| 488 | + idx = 0; |
| 489 | + firstInArray = true; |
| 490 | + return; |
| 491 | + } |
| 492 | + case S_ROOMS: { |
| 493 | + while(idx < SOMFY_MAX_ROOMS && somfy.rooms[idx].roomId == 0) idx++; |
| 494 | + if(idx >= SOMFY_MAX_ROOMS) { |
| 495 | + strcpy(unit, "],\"shades\":["); |
| 496 | + unitLen = strlen(unit); |
| 497 | + section = S_SHADES; |
| 498 | + idx = 0; |
| 499 | + firstInArray = true; |
| 500 | + return; |
| 501 | + } |
| 502 | + size_t pos = 0; |
| 503 | + if(!firstInArray) unit[pos++] = ','; |
| 504 | + resetFmt(pos); |
| 505 | + fmt.beginObject(); |
| 506 | + serializeRoom(&somfy.rooms[idx], fmt); |
| 507 | + fmt.endObject(); |
| 508 | + unitLen = pos + strlen(unit + pos); |
| 509 | + idx++; |
| 510 | + firstInArray = false; |
| 511 | + return; |
| 512 | + } |
| 513 | + case S_SHADES: { |
| 514 | + while(idx < SOMFY_MAX_SHADES && somfy.shades[idx].getShadeId() == 255) idx++; |
| 515 | + if(idx >= SOMFY_MAX_SHADES) { |
| 516 | + strcpy(unit, "],\"groups\":["); |
| 517 | + unitLen = strlen(unit); |
| 518 | + section = S_GROUPS; |
| 519 | + idx = 0; |
| 520 | + lsIdx = LS_OPEN; |
| 521 | + firstInArray = true; |
| 522 | + return; |
| 523 | + } |
| 524 | + size_t pos = 0; |
| 525 | + if(!firstInArray) unit[pos++] = ','; |
| 526 | + resetFmt(pos); |
| 527 | + fmt.beginObject(); |
| 528 | + serializeShade(&somfy.shades[idx], fmt); |
| 529 | + fmt.endObject(); |
| 530 | + unitLen = pos + strlen(unit + pos); |
| 531 | + idx++; |
| 532 | + firstInArray = false; |
| 533 | + return; |
| 534 | + } |
| 535 | + case S_GROUPS: { |
| 536 | + while(idx < SOMFY_MAX_GROUPS && somfy.groups[idx].getGroupId() == 255) idx++; |
| 537 | + if(idx >= SOMFY_MAX_GROUPS) { |
| 538 | + strcpy(unit, "],\"repeaters\":["); |
| 539 | + unitLen = strlen(unit); |
| 540 | + section = S_REPEATERS; |
| 541 | + idx = 0; |
| 542 | + firstInArray = true; |
| 543 | + return; |
| 544 | + } |
| 545 | + SomfyGroup *g = &somfy.groups[idx]; |
| 546 | + if(lsIdx == LS_OPEN) { |
| 547 | + size_t pos = 0; |
| 548 | + if(!firstInArray) unit[pos++] = ','; |
| 549 | + resetFmt(pos); |
| 550 | + fmt.beginObject(); |
| 551 | + serializeGroupRef(g, fmt); |
| 552 | + fmt.beginArray("linkedShades"); |
| 553 | + unitLen = pos + strlen(unit + pos); |
| 554 | + lsIdx = 0; |
| 555 | + firstInArray = false; |
| 556 | + firstInLSArray = true; |
| 557 | + return; |
| 558 | + } |
| 559 | + SomfyShade *lshade = nullptr; |
| 560 | + while(lsIdx < SOMFY_MAX_GROUPED_SHADES) { |
| 561 | + uint8_t sid = g->linkedShades[lsIdx]; |
| 562 | + if(sid > 0 && sid < 255) { |
| 563 | + lshade = somfy.getShadeById(sid); |
| 564 | + if(lshade) break; |
| 565 | + } |
| 566 | + lsIdx++; |
| 567 | + } |
| 568 | + if(!lshade) { |
| 569 | + strcpy(unit, "]}"); |
| 570 | + unitLen = 2; |
| 571 | + idx++; |
| 572 | + lsIdx = LS_OPEN; |
| 573 | + return; |
| 574 | + } |
| 575 | + size_t pos = 0; |
| 576 | + if(!firstInLSArray) unit[pos++] = ','; |
| 577 | + resetFmt(pos); |
| 578 | + fmt.beginObject(); |
| 579 | + serializeShadeRef(lshade, fmt); |
| 580 | + fmt.endObject(); |
| 581 | + unitLen = pos + strlen(unit + pos); |
| 582 | + lsIdx++; |
| 583 | + firstInLSArray = false; |
| 584 | + return; |
| 585 | + } |
| 586 | + case S_REPEATERS: { |
| 587 | + while(idx < SOMFY_MAX_REPEATERS && somfy.repeaters[idx] == 0) idx++; |
| 588 | + if(idx >= SOMFY_MAX_REPEATERS) { |
| 589 | + strcpy(unit, "]}"); |
| 590 | + unitLen = 2; |
| 591 | + section = S_DONE; |
| 592 | + return; |
| 593 | + } |
| 594 | + size_t pos = 0; |
| 595 | + if(!firstInArray) unit[pos++] = ','; |
| 596 | + pos += snprintf(unit + pos, sizeof(unit) - pos, "%lu", (unsigned long)somfy.repeaters[idx]); |
| 597 | + unitLen = pos; |
| 598 | + idx++; |
| 599 | + firstInArray = false; |
| 600 | + return; |
| 601 | + } |
| 602 | + case S_DONE: |
| 603 | + default: |
| 604 | + unitLen = 0; |
| 605 | + return; |
| 606 | + } |
| 607 | + } |
| 608 | +}; |
| 609 | + |
421 | 610 | // -- Async handler implementations -- |
422 | 611 | void Web::handleDiscovery(AsyncWebServerRequest *request) { |
423 | 612 | if(request->method() == HTTP_POST || request->method() == HTTP_GET) { |
@@ -501,37 +690,12 @@ void Web::handleController(AsyncWebServerRequest *request) { |
501 | 690 | if(!this->isAuthenticated(request)) return; |
502 | 691 | if(request->method() == HTTP_POST || request->method() == HTTP_GET) { |
503 | 692 | settings.printAvailHeap(); |
504 | | - AsyncJsonResp resp; |
505 | | - resp.beginResponse(request, g_async_content, sizeof(g_async_content)); |
506 | | - resp.beginObject(); |
507 | | - resp.addElem("maxRooms", (uint8_t)SOMFY_MAX_ROOMS); |
508 | | - resp.addElem("maxShades", (uint8_t)SOMFY_MAX_SHADES); |
509 | | - resp.addElem("maxGroups", (uint8_t)SOMFY_MAX_GROUPS); |
510 | | - resp.addElem("maxGroupedShades", (uint8_t)SOMFY_MAX_GROUPED_SHADES); |
511 | | - resp.addElem("maxLinkedRemotes", (uint8_t)SOMFY_MAX_LINKED_REMOTES); |
512 | | - resp.addElem("startingAddress", (uint32_t)somfy.startingAddress); |
513 | | - resp.beginObject("transceiver"); |
514 | | - resp.beginObject("config"); |
515 | | - serializeTransceiverConfig(resp); |
516 | | - resp.endObject(); |
517 | | - resp.endObject(); |
518 | | - resp.beginObject("version"); |
519 | | - serializeGitVersion(resp); |
520 | | - resp.endObject(); |
521 | | - resp.beginArray("rooms"); |
522 | | - serializeRooms(resp); |
523 | | - resp.endArray(); |
524 | | - resp.beginArray("shades"); |
525 | | - serializeShades(resp); |
526 | | - resp.endArray(); |
527 | | - resp.beginArray("groups"); |
528 | | - serializeGroups(resp); |
529 | | - resp.endArray(); |
530 | | - resp.beginArray("repeaters"); |
531 | | - serializeRepeaters(resp); |
532 | | - resp.endArray(); |
533 | | - resp.endObject(); |
534 | | - resp.endResponse(); |
| 693 | + auto state = std::make_shared<ControllerChunker>(); |
| 694 | + AsyncWebServerResponse *response = request->beginChunkedResponse(_encoding_json, |
| 695 | + [state](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { |
| 696 | + return state->generate(buffer, maxLen); |
| 697 | + }); |
| 698 | + request->send(response); |
535 | 699 | } |
536 | 700 | else request->send(404, _encoding_text, _response_404); |
537 | 701 | } |
|
0 commit comments