I learned that the ESP32 requires something like 90mA to maintain a wifi connection. This would limit the battery life -- the UV-5R's stock battery is 1800mAH. If using a linear regulator, the ESP32 in this mode would drain the battery in 20 hours on its own. With the HT's load, we probably couldn't expect more than about 12 hours of battery -- not super great in an emergency! (DC-DC converters could improve this, but would add to the BOM cost.)
However, I think we can do better. We only really need a network connection to the phone when there's data to send either direction. Can we shut down the connection until we receive audio from the HT (for receiving), or the user presses a button or something (for sending)? I'm not sure this is really possible with wifi -- if a phone isn't awake, I don't know if it bothers to look for wifi networks. (I still need to test this.) However, I think a bluetooth device can initiate a connection to a phone with which it has already paired.
Since I'm trying to avoid the need for any custom apps, I'm pretty much limited to TCP/IP. For Bluetooth, this means the PAN profile.
It doesn't seem like ESP-IDF supports PAN, but this github issue pointed me to BTStack, an alternate bluetooth stack that can run on the esp32, and has a PAN demo in its examples folder. Unfortunately, this demo is not supported on the esp32 -- it wants a POSIX system so it can dump packets into a TAP interface. However, reading the code a bit, I realized that if I implemented the functions exported in btstack_network.h, I could maybe get this to work. That was my goal with this repository, which tries to bridge btstack and lwip (the TCP/IP stack on esp32), and runs a DHCP server and a demo HTTP server.
After running into a few crashes and learning how to use core dumps and the gdb stub, I was able to get it to run without crashing when someone tries to pair with it, but it does not function properly yet. My laptop doesn't get an IP address assigned by the dhcp server, and cannot talk to the HTTP server. I suspect I'm doing something silly - maybe I need to unwrap the received packets a bit before forwarding them to LWIP.
After sharing my progress on the github issue, the maintainer of BTStack showed up with a new example that should do more or less the same thing, but hopefully work better than mine.