Espressif ESP32 boards support Over The Air (OTA) software uploads, allowing you to update the code on-chip without having to reconnect it to your PC.
This can be especially useful if you have embedded the board into a project in a hard to reach or inconvenient place, such as your loft or garden.
OTA with Auto-Discovery
In versions before 2022.0611.12 "Apple Bonjor for Windows" needs be installed for IP address auto discovery to work.
After 2022.0611.12 Apple Bonjour is no longer required for Auto-Discovery to work.
This will automatically look for IP addresses on the local network.
If an upload password has been set in the espcode, a password dialog will appear when OTA begins
OTA configuration is automatic. The image shows one esp board has been discovered using the Apple Bonjour Service.

OTA Without Auto-Discovery (optional)
Ip addresses can be manually entered into the Visual Micro ports (serial) list when the combo box is in the collapsed state. After entering the Ip address, press the ENTER key to confirm.
mDNS properties such as {network.port} will not be available when an ip address is entered manually (and the address has not been discovered by the Bonjour Service)
Instead of using mDNS a local project "board.txt" file can be added to the project.
The image shows the project's board.txt in the lower editor. Apple Bonjour or IP Auto-Discovery is not required. You can manually enter an ip address and press the ENTER key.

Visual Micro Pro users can use board.txt in the project folder to provide overrides (instead of using Bonjour)
Board.txt password required example:-
# local project board.txt overrides
network.password=123
network.auth_upload=yes
network.port=3232
Board.txt password not required example:-
# local project board.txt overrides
network.port=3232
Full working ESP32 OTA Code Example:-
Tested with Esp OTA code (esp core v2.0.3) :-
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
const char* ssid = "YOUR_ROUTER_SSID";
const char* password = "YOUR_ROUTER_WIFI_PASSWORD";
void setup() {
Serial.begin(115200);
Serial.println("Booting");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
// Port defaults to 3232
// ArduinoOTA.setPort(3232);
// Hostname defaults to esp3232-[MAC]
// ArduinoOTA.setHostname("myesp32");
// No authentication by default
// ArduinoOTA.setPassword("admin");
// Password can be set with it's md5 value as well
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
ArduinoOTA
.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem";
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Serial.println("Start updating " + type);
})
.onEnd([]() {
Serial.println("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
})
.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
});
ArduinoOTA.begin();
Serial.println("Ready");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void loop() {
ArduinoOTA.handle();
}