Great project, but I came across some issues. I used ChatGPT to work through them and to create this report:
Pico Walkie-Talkie – Pico 2 W / Arduino-Pico 5.6.1 Bug Report and Changes
Test configuration
Original project:
101 Things – Walkie Talkie with Pi-Pico W
Hardware used for testing:
- 2 × Raspberry Pi Pico 2 W
- MAX4466 microphone modules
- MAX98357A I²S amplifier modules
- 8-ohm speakers
- BOOTSEL used as PTT
- Built-in LED used for status
- 3 × AA battery supply
Software environment:
- Arduino IDE
- Earle Philhower Arduino-Pico core 5.6.1
- Board: Raspberry Pi Pico 2 W (
rp2040:rp2040:rpipico2w)
- Audio sample rate: 10 kHz
- P2P operation tested without an external Wi-Fi router
The original firmware compiled, but several runtime problems were encountered on the Pico 2 W. The following changes resulted in reliable two-unit P2P operation with working bidirectional audio.
1. WiFiManager captive portal did not start correctly
File: WiFiManager.cpp
Original behaviour
When WiFiManager was selected, the Pico created the WALKIE_TALKIE wireless network, but configuration stopped before the HTTP configuration portal became usable.
Serial debugging showed execution entering WiFiManager but not completing autoConnect().
The original startConfigPortal() contained logic equivalent to:
WiFi.config(AP_IP);
WiFi.beginAP(_apSSID, _apPassword);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
LOG_DEBUG(".");
}
The problem appears to be that station-style connection/status handling is being used while the Pico is operating as an Access Point.
Change
The AP was explicitly configured in AP mode and given its AP address before beginAP():
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(
AP_IP,
AP_IP,
IPAddress(255, 255, 255, 0)
);
int apResult = WiFi.beginAP(_apSSID, _apPassword);
LOGF_DEBUG("beginAP result = %d", apResult);
if (apResult != WL_CONNECTED)
{
LOG_DEBUG(F("Failed to start Access Point"));
return false;
}
delay(500);
The displayed AP address was also changed from:
to:
Result
The serial output then reached:
beginAP result = 3
AccessPoint IP address 192.168.42.1
Starting config portal
HTTP server started
The captive portal became accessible and Wi-Fi credentials could be entered successfully.
2. P2P mode selected for router-free testing
File: walkie_talkie.ino
The default settings were temporarily changed from WiFiManager mode to P2P mode:
Original:
s_settings settings = {
eeprom_version,
WIFI_MANAGER,
STA,
false,
0
};
Test version:
s_settings settings = {
eeprom_version,
P2P,
STA,
false,
0
};
The EEPROM version was also changed:
const int eeprom_version = 400;
to:
const int eeprom_version = 401;
This was done to invalidate previously stored WiFiManager settings and force the new P2P defaults to be loaded.
This is a test/configuration change rather than a bug fix.
3. P2P role negotiation works, but saved AP reconnection logic was incorrect
File: walkie_talkie.ino
Function: GetWIFIP2P()
Initial P2P negotiation successfully produced:
Unit 1:
Connected as STA
192.168.4.16
Unit 2:
Peer connected to AP
192.168.4.1
Therefore the basic automatic P2P AP/STA negotiation works correctly.
However, when a previously stored AP role is restored, the original code waits using:
while (WiFi.status() != WL_CONNECTED)
This is not an appropriate test for whether a station has joined an AP.
The saved AP path was changed to:
WiFi.mode(WIFI_AP);
WiFi.softAP(ssid, password);
while (WiFi.softAPgetStationNum() == 0)
{
blink(1);
}
Serial.println("Peer connected to AP");
Serial.println(WiFi.softAPIP());
return;
WiFi.softAPgetStationNum() is therefore used to determine when the other walkie-talkie has associated with the Pico AP.
4. STA rebooted immediately after a long PTT transmission
File: walkie_talkie.ino
Functions: sendData(), recvData() and loop()
Symptom
If the STA walkie-talkie transmitted for several seconds, audio was received correctly by the AP.
However, immediately after releasing PTT, the transmitting STA printed:
no connection to AP, reboot
and restarted.
The AP unit did not exhibit this behaviour.
Cause
The connection watchdog used:
millis() - peer_last_seen
to determine whether the AP had disappeared.
While PTT is held, sendData() remains in:
and the walkie-talkie is deliberately not executing the normal receive/heartbeat processing.
Consequently, peer_last_seen becomes stale during a long transmission.
When PTT is released, recvData() immediately sees a heartbeat age greater than the original 3000 ms timeout and reboots the STA even though the AP is still present.
Change 1 – reset heartbeat timer after TX
The main loop was changed to:
if(BOOTSEL)
{
led_on();
sendData();
led_off();
// We have deliberately not been listening while transmitting,
// so don't interpret that period as a lost P2P connection.
peer_last_seen = millis();
}
else
{
recvData();
}
Change 2 – increase watchdog period
The original:
if(settings.connection_method == P2P &&
settings.p2p_role == STA &&
millis()-peer_last_seen > 3000)
was changed to:
if(settings.connection_method == P2P &&
settings.p2p_role == STA &&
millis()-peer_last_seen > 6000)
Result
Approximately 15-second continuous PTT transmissions no longer cause the STA to reboot when PTT is released.
This appears to be a genuine half-duplex P2P watchdog bug.
5. Blocking LED blink routine interferes with real-time processing
File: walkie_talkie.ino
Function: blink()
Original implementation
The original status routine uses delay():
void blink(uint8_t blinks)
{
for(uint8_t blink=0; blink<blinks; blink++)
{
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
delay(100);
}
delay(500);
}
For the normal three-blink "paired" indication this can block execution for approximately 1.1 seconds.
Because blink() is called from the communication/audio loops, this is undesirable for real-time UDP audio handling and P2P connection negotiation.
Change
blink() was replaced by a millis()-based state machine that returns immediately rather than delaying.
The existing meanings of the indications were retained:
- 3 blinks = paired/peer present
- 2 blinks = peer not currently seen
- solid LED = PTT/audio activity
No delay() calls are required by the replacement status LED routine.
6. Non-blocking blink exposed excessive Serial dot output
File: walkie_talkie.ino
Function: GetWIFIP2P()
The original STA connection loop contained:
while (WiFi.status() != WL_CONNECTED)
{
Serial.print('.');
if(millis()-start > 6000) reboot();
blink(1);
}
With the original blocking blink(1), the delays inside blink() unintentionally throttled this loop.
After blink() was made non-blocking, the Serial Monitor was flooded with . characters.
Change
Serial status output was explicitly rate limited:
uint32_t lastDot = 0;
while (WiFi.status() != WL_CONNECTED)
{
if (millis() - lastDot >= 500)
{
lastDot = millis();
Serial.print('.');
}
if(millis() - start > 6000)
{
reboot();
}
blink(1);
}
This demonstrates that some of the P2P timing in the original implementation implicitly depends on delays inside the LED routine.
7. Receive jitter-buffer startup increased from two to three packets
File: walkie_talkie.ino
Function: recvData()
The original four-packet jitter buffer starts playback at:
if(fill == 2) play = true;
if(fill == 0) play = false;
During Pico 2 W testing, repeated:
Underflow in audio output
messages were observed on the receiving walkie-talkie.
The startup threshold was changed to:
if(fill >= 3) play = true;
if(fill == 0) play = false;
The four-packet ring buffer itself was not changed.
This gives I²S playback one additional packet of reserve before playback starts, at the cost of one packet of additional initial latency.
Result
In the latest test, an approximately 15-second continuous transmission produced only:
at the beginning of reception.
No continuing underflows were reported during the remainder of the transmission.
Audio remained intelligible and continuous.
8. I²S underflow diagnostics were rate limited
File: walkie_talkie.ino
Function: recvData()
The original code printed immediately for every underflow:
if(audioOutput.getUnderflow())
Serial.println("Underflow in audio output");
Repeated Serial output itself is undesirable inside a real-time audio loop.
Two diagnostic counters were added:
static uint32_t underflowCount = 0;
static uint32_t lastAudioReport = 0;
Underflows are counted:
if(audioOutput.getUnderflow())
{
underflowCount++;
}
and reported once per second:
if(millis() - lastAudioReport >= 1000)
{
lastAudioReport = millis();
if(underflowCount > 0)
{
Serial.print("Audio underflows/sec: ");
Serial.println(underflowCount);
underflowCount = 0;
}
}
This makes the diagnostic much less intrusive to the real-time audio path.
9. ADC overflow diagnostic removed from the real-time transmit loop
File: walkie_talkie.ino
Function: sendData()
Repeated messages such as:
were observed during transmission.
Printing from the audio processing loop can itself consume enough time to worsen a real-time buffering problem.
The transmit loop therefore no longer prints every ADC overflow.
Instead of:
if(audioInput.get_overflow())
Serial.println("overflow in audio input");
audioInput.input_samples(inputSamples);
the current test code checks/clears the condition without printing:
audioInput.get_overflow();
audioInput.input_samples(inputSamples);
This was primarily a diagnostic/performance change rather than evidence of a fault in ADCAudio.
10. Explicit reboot() forward declaration added
File: walkie_talkie.ino
With the modified sketch, Arduino compilation produced:
'reboot' was not declared in this scope
An explicit prototype was added before functions which call it:
The actual implementation remains later in the file.
This should be regarded mainly as an Arduino preprocessing/compilation compatibility change rather than a runtime bug in the walkie-talkie design.
11. Additional Serial diagnostics added during fault finding
Temporary/diagnostic messages were added around Wi-Fi and audio initialisation, including:
Serial.println("Before GetWIFIWIFIManager");
Serial.println("After GetWIFIWIFIManager");
Serial.println("Before GetWIFIP2P");
Serial.println("After GetWIFIP2P");
Serial.println("Starting audio input");
Serial.println("Starting audio output");
Serial.println("setup complete");
A one-second:
message was also added to the main loop.
These were debugging additions and are not required for normal operation.
They were useful in establishing that the original WiFiManager failure occurred during Wi-Fi setup rather than in the audio subsystem.
Current test result
With the changes above, two Raspberry Pi Pico 2 W units now operate directly in P2P mode without an external Wi-Fi router.
Typical negotiated addresses are:
AP : 192.168.4.1
STA : 192.168.4.16
Confirmed operation:
- automatic AP/STA P2P pairing works;
- stored AP/STA roles reconnect;
- BOOTSEL operates as PTT;
- status LED operates without blocking the program;
- audio works in both directions;
- approximately 15-second continuous transmissions work;
- the STA no longer reboots after a long transmission;
- the four-packet jitter buffer is stable with playback starting at three packets;
- latest receive test produced only one I²S underflow at the beginning of an approximately 15-second transmission, with no continuing underflow reports.
Main issues considered genuine bugs
The most significant issues found were:
-
WiFiManager AP startup incompatibility on Pico 2 W / Arduino-Pico 5.6.1.
The AP could appear, but the configuration portal did not progress correctly until AP-specific configuration/status handling was used.
-
P2P saved-AP connection test.
WiFi.status() == WL_CONNECTED is not the correct mechanism for determining whether a station has joined an AP; WiFi.softAPgetStationNum() was required.
-
False P2P connection-loss reboot after transmitting.
peer_last_seen is not updated while the unit is deliberately inside the transmit loop, causing the STA watchdog to interpret a long PTT period as loss of the AP.
-
Blocking status LED routine in real-time networking/audio paths.
The original blink() introduces substantial delays and also unintentionally controls the timing of some P2P loops.
Changes that are tuning/debugging rather than definite bugs
The following changes improved operation but should not necessarily be treated as defects in the original design:
- increasing jitter-buffer startup from 2 to 3 packets;
- aggregating I²S underflow reports;
- suppressing ADC overflow Serial messages;
- adding the explicit
reboot() prototype;
- changing EEPROM version 400 → 401;
- selecting P2P as the default connection method;
- adding startup and heartbeat diagnostic messages.
Suggested upstream fixes
For an upstream version I would suggest retaining the existing overall architecture but:
- make LED status completely non-blocking;
- make P2P timing independent of LED timing;
- use AP-specific station detection in AP mode;
- suspend/reset the STA heartbeat watchdog across intentional PTT transmit periods;
- avoid frequent Serial printing from ADC/I²S real-time paths;
- consider making the jitter-buffer startup threshold configurable between 2 and 3 packets;
- test WiFiManager AP startup specifically against current Arduino-Pico releases and Pico 2 W.
The underlying audio architecture, A-law compression, UDP transport, four-packet jitter buffer and automatic AP/STA P2P concept have otherwise worked successfully in the two-unit Pico 2 W test.
Great project, but I came across some issues. I used ChatGPT to work through them and to create this report:
Pico Walkie-Talkie – Pico 2 W / Arduino-Pico 5.6.1 Bug Report and Changes
Test configuration
Original project:
101 Things – Walkie Talkie with Pi-Pico W
Hardware used for testing:
Software environment:
rp2040:rp2040:rpipico2w)The original firmware compiled, but several runtime problems were encountered on the Pico 2 W. The following changes resulted in reliable two-unit P2P operation with working bidirectional audio.
1. WiFiManager captive portal did not start correctly
File:
WiFiManager.cppOriginal behaviour
When WiFiManager was selected, the Pico created the
WALKIE_TALKIEwireless network, but configuration stopped before the HTTP configuration portal became usable.Serial debugging showed execution entering WiFiManager but not completing
autoConnect().The original
startConfigPortal()contained logic equivalent to:The problem appears to be that station-style connection/status handling is being used while the Pico is operating as an Access Point.
Change
The AP was explicitly configured in AP mode and given its AP address before
beginAP():The displayed AP address was also changed from:
to:
Result
The serial output then reached:
The captive portal became accessible and Wi-Fi credentials could be entered successfully.
2. P2P mode selected for router-free testing
File:
walkie_talkie.inoThe default settings were temporarily changed from WiFiManager mode to P2P mode:
Original:
s_settings settings = { eeprom_version, WIFI_MANAGER, STA, false, 0 };Test version:
s_settings settings = { eeprom_version, P2P, STA, false, 0 };The EEPROM version was also changed:
to:
This was done to invalidate previously stored WiFiManager settings and force the new P2P defaults to be loaded.
This is a test/configuration change rather than a bug fix.
3. P2P role negotiation works, but saved AP reconnection logic was incorrect
File:
walkie_talkie.inoFunction:
GetWIFIP2P()Initial P2P negotiation successfully produced:
Therefore the basic automatic P2P AP/STA negotiation works correctly.
However, when a previously stored AP role is restored, the original code waits using:
This is not an appropriate test for whether a station has joined an AP.
The saved AP path was changed to:
WiFi.softAPgetStationNum()is therefore used to determine when the other walkie-talkie has associated with the Pico AP.4. STA rebooted immediately after a long PTT transmission
File:
walkie_talkie.inoFunctions:
sendData(),recvData()andloop()Symptom
If the STA walkie-talkie transmitted for several seconds, audio was received correctly by the AP.
However, immediately after releasing PTT, the transmitting STA printed:
and restarted.
The AP unit did not exhibit this behaviour.
Cause
The connection watchdog used:
millis() - peer_last_seento determine whether the AP had disappeared.
While PTT is held,
sendData()remains in:and the walkie-talkie is deliberately not executing the normal receive/heartbeat processing.
Consequently,
peer_last_seenbecomes stale during a long transmission.When PTT is released,
recvData()immediately sees a heartbeat age greater than the original 3000 ms timeout and reboots the STA even though the AP is still present.Change 1 – reset heartbeat timer after TX
The main loop was changed to:
Change 2 – increase watchdog period
The original:
was changed to:
Result
Approximately 15-second continuous PTT transmissions no longer cause the STA to reboot when PTT is released.
This appears to be a genuine half-duplex P2P watchdog bug.
5. Blocking LED blink routine interferes with real-time processing
File:
walkie_talkie.inoFunction:
blink()Original implementation
The original status routine uses
delay():For the normal three-blink "paired" indication this can block execution for approximately 1.1 seconds.
Because
blink()is called from the communication/audio loops, this is undesirable for real-time UDP audio handling and P2P connection negotiation.Change
blink()was replaced by amillis()-based state machine that returns immediately rather than delaying.The existing meanings of the indications were retained:
No
delay()calls are required by the replacement status LED routine.6. Non-blocking blink exposed excessive Serial dot output
File:
walkie_talkie.inoFunction:
GetWIFIP2P()The original STA connection loop contained:
With the original blocking
blink(1), the delays insideblink()unintentionally throttled this loop.After
blink()was made non-blocking, the Serial Monitor was flooded with.characters.Change
Serial status output was explicitly rate limited:
This demonstrates that some of the P2P timing in the original implementation implicitly depends on delays inside the LED routine.
7. Receive jitter-buffer startup increased from two to three packets
File:
walkie_talkie.inoFunction:
recvData()The original four-packet jitter buffer starts playback at:
During Pico 2 W testing, repeated:
messages were observed on the receiving walkie-talkie.
The startup threshold was changed to:
The four-packet ring buffer itself was not changed.
This gives I²S playback one additional packet of reserve before playback starts, at the cost of one packet of additional initial latency.
Result
In the latest test, an approximately 15-second continuous transmission produced only:
at the beginning of reception.
No continuing underflows were reported during the remainder of the transmission.
Audio remained intelligible and continuous.
8. I²S underflow diagnostics were rate limited
File:
walkie_talkie.inoFunction:
recvData()The original code printed immediately for every underflow:
Repeated Serial output itself is undesirable inside a real-time audio loop.
Two diagnostic counters were added:
Underflows are counted:
if(audioOutput.getUnderflow()) { underflowCount++; }and reported once per second:
This makes the diagnostic much less intrusive to the real-time audio path.
9. ADC overflow diagnostic removed from the real-time transmit loop
File:
walkie_talkie.inoFunction:
sendData()Repeated messages such as:
were observed during transmission.
Printing from the audio processing loop can itself consume enough time to worsen a real-time buffering problem.
The transmit loop therefore no longer prints every ADC overflow.
Instead of:
the current test code checks/clears the condition without printing:
This was primarily a diagnostic/performance change rather than evidence of a fault in
ADCAudio.10. Explicit
reboot()forward declaration addedFile:
walkie_talkie.inoWith the modified sketch, Arduino compilation produced:
An explicit prototype was added before functions which call it:
The actual implementation remains later in the file.
This should be regarded mainly as an Arduino preprocessing/compilation compatibility change rather than a runtime bug in the walkie-talkie design.
11. Additional Serial diagnostics added during fault finding
Temporary/diagnostic messages were added around Wi-Fi and audio initialisation, including:
A one-second:
message was also added to the main loop.
These were debugging additions and are not required for normal operation.
They were useful in establishing that the original WiFiManager failure occurred during Wi-Fi setup rather than in the audio subsystem.
Current test result
With the changes above, two Raspberry Pi Pico 2 W units now operate directly in P2P mode without an external Wi-Fi router.
Typical negotiated addresses are:
Confirmed operation:
Main issues considered genuine bugs
The most significant issues found were:
WiFiManager AP startup incompatibility on Pico 2 W / Arduino-Pico 5.6.1.
The AP could appear, but the configuration portal did not progress correctly until AP-specific configuration/status handling was used.
P2P saved-AP connection test.
WiFi.status() == WL_CONNECTEDis not the correct mechanism for determining whether a station has joined an AP;WiFi.softAPgetStationNum()was required.False P2P connection-loss reboot after transmitting.
peer_last_seenis not updated while the unit is deliberately inside the transmit loop, causing the STA watchdog to interpret a long PTT period as loss of the AP.Blocking status LED routine in real-time networking/audio paths.
The original
blink()introduces substantial delays and also unintentionally controls the timing of some P2P loops.Changes that are tuning/debugging rather than definite bugs
The following changes improved operation but should not necessarily be treated as defects in the original design:
reboot()prototype;Suggested upstream fixes
For an upstream version I would suggest retaining the existing overall architecture but:
The underlying audio architecture, A-law compression, UDP transport, four-packet jitter buffer and automatic AP/STA P2P concept have otherwise worked successfully in the two-unit Pico 2 W test.