Same pattern as writev — write() calls internal_write_() then
internal_output_(), each acquiring the lock separately. Hold
the lock at the outer scope so inner calls just bump the
recursion counter.
tcp_new() is an lwip core API call that must be bracketed with
the lwip lock on RP2040 per pico-sdk docs. Add LWIP_LOCK() to
socket() and socket_listen() factory functions.
Avoid repeated lock acquire/release cycles per iovec element.
The recursive mutex re-entry in inner calls is nearly free (counter
bump), while the outer lock prevents the expensive IRQ disable/enable
on each iteration.
On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1,
which means lwip callbacks (recv_fn, accept_fn, err_fn) run from a PendSV
interrupt — not the main loop. This allows them to preempt read(), write(),
close(), and accept() at any point, causing race conditions on shared state
like the rx_buf_ pbuf chain.
The most critical race: recv_fn calls pbuf_cat(rx_buf_, pb) while read() is
freeing nodes in the same chain, leading to use-after-free and lwip's
"Creating an infinite loop" assertion panic. This is the root cause of #10681.
Fix: implement RP2040's LwIPLock (previously a no-op) to call
cyw43_arch_lwip_begin/end, which acquires the pico-sdk async_context recursive
mutex. Add LWIP_LOCK() guards to all main-loop lwip API call sites in the
socket layer.
On ESP8266, lwip callbacks run cooperatively from the main loop, so
LwIPLock remains a no-op.
Closes#10681
Split the 256-byte UART read buffer into a separate noinline
read_and_send_() helper so the common "no data" path in loop()
only needs a 32-byte stack frame instead of 288 bytes.
Also reorder checks so api_connection_ == nullptr bails out
first, avoiding the more expensive disconnect detection when
no client is subscribed.
APIBuffer::resize() is already just a capacity check + store,
making the outer size-equality guard redundant. Saves ~8 bytes
of code size across both frame helpers.
Replace magic {0, 0} with {0, PROTO_VARINT_PARSE_FAILED} in parse_slow()
and parse_wide() for consistency with the named sentinel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On BLE builds where proto_varint_value_t is uint64_t, decode_zigzag32()
takes uint32_t — make the narrowing explicit to match the static_cast
pattern used for other type conversions in generated code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On BLE builds where proto_varint_value_t is uint64_t, decode_zigzag32()
takes uint32_t — make the narrowing explicit to match the static_cast
pattern used for other type conversions in generated code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Consistency with frame helper code — makes the uint64→uint32 narrowing
explicit on BLE builds where proto_varint_value_t is uint64_t.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cast msg_size/type_varint.value to uint32_t in HELPER_LOG to match PRIu32
format (proto_varint_value_t is uint64_t on BLE builds)
- Remove unused is_varint64 field from api_protobuf.py TypeInfo classes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address code review feedback:
- Remove as_uint16() and as_uint32() accessors from ProtoVarIntResult
- Use .value directly with static_cast where narrowing is needed
- Fix ESP_LOGV truncation: use PRIu64 with static_cast<uint64_t> for BLE builds
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After moving decode_varint to raw proto_varint_value_t, the type-
conversion accessors (as_bool, as_int32, as_sint32, as_uint64, etc.)
on ProtoVarIntResult are dead code. ProtoVarInt itself is now only
used as a static method container for parse(), so remove its
constructors, instance accessors, and value_ member.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace per-override #ifdef USE_API_VARINT64 conditionals with a
single type alias proto_varint_value_t, eliminating preprocessor
blocks from every decode_varint signature in api_pb2.cpp/h.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass uint32_t (or uint64_t when USE_API_VARINT64 is defined) directly
to decode_varint() instead of the ProtoVarIntResult struct. This
eliminates accessor method overhead in each of the 51 overrides,
replacing value.as_uint32() with direct value usage, value.as_bool()
with value != 0, etc. No vtable growth - single virtual method with
conditionally-typed parameter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
parse_non_empty() has no len==0 check (with debug assert) for callers
that guarantee len >= 1 (e.g. after while (ptr < end)).
parse() adds the len==0 guard and delegates to parse_non_empty() for
callers where the buffer may be empty (e.g. value parse after tag
advance in decode(), frame helper header parsing).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>