|
2 | 2 | #include "espbridge/protocol.h" |
3 | 3 | #include "espbridge/modules.h" |
4 | 4 | #include <Wire.h> |
| 5 | +#include <esp_heap_caps.h> |
5 | 6 |
|
6 | 7 | static bool i2c_inited[2]; |
7 | 8 |
|
@@ -32,11 +33,28 @@ void i2c_handle(uint8_t op, uint8_t seq, const uint8_t* p, uint16_t len) { |
32 | 33 | // BLE link connected runs within a few KB of — so fall back to |
33 | 34 | // smaller buffers instead of failing, and report the size that stuck |
34 | 35 | // (hosts chunk their writes to it). |
35 | | - static const uint16_t sizes[] = {MAX_PAYLOAD, 512, 128}; |
| 36 | + // |
| 37 | + // The fallback must be heap-aware, not try-and-see (all measured on |
| 38 | + // classic ESP32 + BLE link, core 3.3.6): |
| 39 | + // - the IDF i2c driver install after the buffer allocation eats |
| 40 | + // ~2 KB and WEDGES rx_task silently below ~7 KB free (no panic, |
| 41 | + // no error return — the board just stops answering); |
| 42 | + // - the radio stacks need ~6.5 KB free *for the rest of the |
| 43 | + // session*, or Bluedroid starts dropping replies under load |
| 44 | + // (6.7 KB rest = solid; 5.9 KB rest = host-visible timeouts). |
| 45 | + // So a buffer above the 128-byte floor must leave 2*size + ~9 KB, |
| 46 | + // plus a contiguous chunk to spare. |
| 47 | + static const uint16_t sizes[] = {MAX_PAYLOAD, 1024, 512, 256, 128}; |
36 | 48 | uint16_t got = 0; |
37 | | - for (uint8_t i = 0; i < 3 && !got; i++) |
| 49 | + for (uint8_t i = 0; i < 5 && !got; i++) { |
| 50 | + if (sizes[i] > 128) { |
| 51 | + if (ESP.getFreeHeap() < 2u * sizes[i] + 9216) continue; |
| 52 | + if (heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) |
| 53 | + < (size_t)sizes[i] + 2048) continue; |
| 54 | + } |
38 | 55 | if (w->setBufferSize(sizes[i]) && w->begin(p[1], p[2], rd32(p + 3))) |
39 | 56 | got = sizes[i]; |
| 57 | + } |
40 | 58 | if (!got) { |
41 | 59 | proto_log_heap("i2c: init failed"); // ST_IO alone is opaque |
42 | 60 | proto_reply_err(seq, cmd, ST_IO); |
@@ -67,12 +85,29 @@ void i2c_handle(uint8_t op, uint8_t seq, const uint8_t* p, uint16_t len) { |
67 | 85 | w->beginTransmission(p[1]); |
68 | 86 | // write() returns bytes buffered — short means TX buffer overflow, |
69 | 87 | // which would silently corrupt the transfer (drops the tail bytes) |
| 88 | + uint8_t st = ST_OK; |
70 | 89 | if (len > 2 && w->write(p + 2, len - 2) != (size_t)(len - 2)) { |
71 | 90 | w->endTransmission(); |
72 | | - proto_reply_err(seq, cmd, ST_BAD_ARGS); |
| 91 | + st = ST_BAD_ARGS; |
| 92 | + } else if (w->endTransmission() != 0) { |
| 93 | + st = ST_IO; |
| 94 | + } |
| 95 | + if (st != ST_OK) { |
| 96 | + // Fire-and-forget (seq 0) gets no error reply, so a failed write |
| 97 | + // in a pipelined burst (OLED frame push) would vanish without a |
| 98 | + // trace — visible only as display corruption. Warn, rate-limited. |
| 99 | + if (seq == 0) { |
| 100 | + static uint32_t last_warn = 0; |
| 101 | + uint32_t now = millis(); |
| 102 | + if (now - last_warn > 1000) { |
| 103 | + last_warn = now; |
| 104 | + proto_log(2, st == ST_IO ? "i2c: unacked write failed (NACK/bus)" |
| 105 | + : "i2c: unacked write exceeds wire buffer"); |
| 106 | + } |
| 107 | + } |
| 108 | + proto_reply_err(seq, cmd, st); |
73 | 109 | return; |
74 | 110 | } |
75 | | - if (w->endTransmission() != 0) { proto_reply_err(seq, cmd, ST_IO); return; } |
76 | 111 | proto_reply_ok(seq, cmd); |
77 | 112 | break; |
78 | 113 | } |
|
0 commit comments