Skip to content

Walkie Talkie using pi pico 2w #10

Description

@iflybywireless

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:

WiFi.localIP()

to:

WiFi.softAPIP()

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:

while (BOOTSEL)

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:

Audio underflows/sec: 1

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:

overflow in audio input

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:

void reboot();

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:

Heartbeat

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions