├── .gitignore ├── .env.template ├── src ├── index.html └── simple-webserver.ino ├── LICENSE ├── platformio.ini ├── .github └── workflows │ └── ota-update.yml ├── platformio_upload.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | src/credentials.h 3 | .vscode/ 4 | firmware.bin 5 | node_modules/ 6 | .env -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | export WIFI_SSID="" 2 | export WIFI_PASS='' 3 | export HUSARNET_JOINCODE="" 4 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ESP32 boilerplate 5 | 6 | 7 |

Hello world!

8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Husarnet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [env] 2 | board = esp32dev 3 | platform = espressif32@2.1.0 4 | framework = arduino 5 | platform_packages = 6 | framework-arduinoespressif32 @ https://github.com/husarnet/arduino-esp32/releases/download/1.0.4-1/arduino-husarnet-esp32.zip 7 | lib_deps = 8 | ; Until our pull requests are merged you need to use AsyncTCP with our fixes for IPv6 9 | https://github.com/husarion/AsyncTCP.git 10 | Husarnet ESP32 11 | https://github.com/me-no-dev/ESPAsyncWebServer 12 | ayushsharma82/AsyncElegantOTA @ ^2.2.6 13 | 14 | monitor_speed = 115200 15 | monitor_filters = esp32_exception_decoder, default 16 | 17 | board_build.partitions = min_spiffs.csv 18 | board_build.embed_txtfiles = 19 | src/index.html 20 | 21 | build_flags = 22 | '-DWIFI_SSID="${sysenv.WIFI_SSID}"' 23 | '-DWIFI_PASS="${sysenv.WIFI_PASS}"' 24 | '-DHUSARNET_JOINCODE="${sysenv.HUSARNET_JOINCODE}"' 25 | 26 | [env:serial_upload] 27 | 28 | upload_speed = 921600 ; valid for esptool protocol 29 | 30 | [env:ota_upload] 31 | 32 | extra_scripts = platformio_upload.py 33 | 34 | upload_protocol = custom 35 | upload_url = http://my-esp32:8080/update ; valid for custom protocol 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/ota-update.yml: -------------------------------------------------------------------------------- 1 | name: ESP32 OTA update 2 | 3 | on: 4 | # push: 5 | # branches: 6 | # - 'main' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Husarnet VPN 18 | uses: husarnet/husarnet-action@v5 19 | with: 20 | join-code: ${{ secrets.HUSARNET_JOINCODE }} 21 | dashboard-login: ${{ secrets.HUSARNET_DASHBOARD_LOGIN }} 22 | dashboard-password: ${{ secrets.HUSARNET_DASHBOARD_PASSWORD }} 23 | 24 | - name: Wait until my-esp32 host is available 25 | run: while ! grep -q "my-esp32" /etc/hosts; do sleep 1; done 26 | 27 | - name: ESP32 software reset 28 | run: curl -X POST 'http://my-esp32:8080/reset' 29 | 30 | - name: Installing platformio 31 | run: pip3 install -U platformio 32 | 33 | - name: Building and uploading a firmware for ESP32 34 | run: | 35 | export WIFI_SSID=${{ secrets.WIFI_SSID }} 36 | export WIFI_PASS=${{ secrets.WIFI_PASS }} 37 | export HUSARNET_JOINCODE=${{ secrets.HUSARNET_JOINCODE }} 38 | pio run -e ota_upload --target upload 39 | 40 | # - name: Uploading a firmware to ESP32 41 | # run: > 42 | # curl --http0.9 -# -v 43 | # --connect-timeout 30 44 | # --retry 5 45 | # --retry-delay 5 46 | # --retry-connrefused 47 | # -H 'Accept: */*' 48 | # -H 'Accept-Encoding: gzip, deflate' 49 | # -H 'Connection: keep-alive' 50 | # -F "MD5="$(md5sum "${{ github.workspace }}/.pio/build/esp32dev/firmware.bin" | cut -d ' ' -f 1)"" 51 | # -F 'firmware=@${{ github.workspace }}/.pio/build/esp32dev/firmware.bin' 52 | # 'http://my-esp32:8080/update' 53 | 54 | -------------------------------------------------------------------------------- /platformio_upload.py: -------------------------------------------------------------------------------- 1 | # From https://github.com/ayushsharma82/AsyncElegantOTA/blob/master/platformio_upload.py 2 | # Allows PlatformIO to upload directly to AsyncElegantOTA 3 | # 4 | # To use: 5 | # - copy this script into the same folder as your platformio.ini 6 | # - set the following for your project in platformio.ini: 7 | # 8 | # extra_scripts = platformio_upload.py 9 | # upload_protocol = custom 10 | # upload_url = 11 | # 12 | # An example of an upload URL: 13 | # upload_URL = http://192.168.1.123/update 14 | 15 | import requests 16 | import hashlib 17 | Import('env') 18 | 19 | try: 20 | from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor 21 | from tqdm import tqdm 22 | except ImportError: 23 | env.Execute("$PYTHONEXE -m pip install requests_toolbelt") 24 | env.Execute("$PYTHONEXE -m pip install tqdm") 25 | from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor 26 | from tqdm import tqdm 27 | 28 | def on_upload(source, target, env): 29 | firmware_path = str(source[0]) 30 | upload_url = env.GetProjectOption('upload_url') 31 | 32 | with open(firmware_path, 'rb') as firmware: 33 | md5 = hashlib.md5(firmware.read()).hexdigest() 34 | firmware.seek(0) 35 | encoder = MultipartEncoder(fields={ 36 | 'MD5': md5, 37 | 'firmware': ('firmware', firmware, 'application/octet-stream')} 38 | ) 39 | 40 | bar = tqdm(desc='Upload Progress', 41 | total=encoder.len, 42 | dynamic_ncols=True, 43 | unit='B', 44 | unit_scale=True, 45 | unit_divisor=1024 46 | ) 47 | 48 | monitor = MultipartEncoderMonitor(encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n)) 49 | 50 | try: 51 | response = requests.post(upload_url, data=monitor, headers={'Content-Type': monitor.content_type}, timeout=180) 52 | bar.close() 53 | 54 | # Basic error checking 55 | if response.status_code != 200 or 'error' in response.text.lower(): 56 | raise Exception(f"Upload failed with status {response.status_code}: {response.text}") 57 | else: 58 | print("Upload completed successfully!") 59 | print(response.text) 60 | except requests.Timeout: 61 | print("Request timed out!") 62 | except requests.RequestException as e: 63 | print(f"Error occurred: {e}") 64 | 65 | 66 | 67 | env.Replace(UPLOADCMD=on_upload) 68 | -------------------------------------------------------------------------------- /src/simple-webserver.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define HTTP_PORT 8080 8 | 9 | // For GitHub Actions OTA deploment 10 | 11 | // WiFi credentials 12 | const char *ssid = WIFI_SSID; 13 | const char *password = WIFI_PASS; 14 | 15 | // Husarnet credentials 16 | const char *husarnetJoinCode = HUSARNET_JOINCODE; // find at app.husarnet.com 17 | const char *dashboardURL = "default"; 18 | 19 | AsyncWebServer server(HTTP_PORT); 20 | const char *hostName = "my-esp32"; 21 | 22 | // index.html available in "index_html" const String 23 | extern const char index_html_start[] asm("_binary_src_index_html_start"); 24 | const String index_html = String((const char*)index_html_start); 25 | 26 | void setup(void) { 27 | // =============================================== 28 | // Wi-Fi, OTA and Husarnet VPN configuration 29 | // =============================================== 30 | 31 | // remap default Serial (used by Husarnet logs) 32 | Serial.begin(115200, SERIAL_8N1, 16, 17); // from P3 & P1 to P16 & P17 33 | Serial1.begin(115200, SERIAL_8N1, 3, 1); // remap Serial1 from P9 & P10 to P3 & P1 34 | 35 | Serial1.println("\r\n**************************************"); 36 | Serial1.println("GitHub Actions OTA example"); 37 | Serial1.println("**************************************\r\n"); 38 | 39 | // Init Wi-Fi 40 | Serial1.printf("📻 1. Connecting to: %s Wi-Fi network ", ssid); 41 | 42 | WiFi.mode(WIFI_STA); 43 | WiFi.begin(ssid, password); 44 | while (WiFi.status() != WL_CONNECTED) { 45 | static int cnt = 0; 46 | delay(500); 47 | Serial1.print("."); 48 | cnt++; 49 | if (cnt > 10) { 50 | ESP.restart(); 51 | } 52 | } 53 | 54 | Serial1.println(" done\r\n"); 55 | 56 | // Init Husarnet P2P VPN service 57 | Serial1.printf("⌛ 2. Waiting for Husarnet to be ready "); 58 | 59 | Husarnet.selfHostedSetup(dashboardURL); 60 | Husarnet.join(husarnetJoinCode, hostName); 61 | Husarnet.start(); 62 | 63 | // Before Husarnet is ready peer list contains: 64 | // master (0000:0000:0000:0000:0000:0000:0000:0001) 65 | const uint8_t addr_comp[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; 66 | bool husarnetReady = 0; 67 | while (husarnetReady == 0) { 68 | Serial1.print("."); 69 | for (auto const &host : Husarnet.listPeers()) { 70 | if (host.first == addr_comp) { 71 | ; 72 | } else { 73 | husarnetReady = 1; 74 | } 75 | } 76 | delay(1000); 77 | } 78 | 79 | Serial1.println(" done\r\n"); 80 | 81 | // define HTTP API for remote reset 82 | server.on("/reset", HTTP_POST, [](AsyncWebServerRequest *request) { 83 | request->send(200, "text/plain", "Reseting ESP32 after 1s ..."); 84 | Serial1.println("Software reset on POST request"); 85 | delay(2000); 86 | ESP.restart(); 87 | }); 88 | 89 | // Init OTA webserver (available under /update path) 90 | AsyncElegantOTA.begin(&server); 91 | server.begin(); 92 | 93 | // =============================================== 94 | // PLACE YOUR APPLICATION CODE BELOW 95 | // =============================================== 96 | 97 | // Example webserver hosting table with known Husarnet Hosts 98 | server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { 99 | request->send(200, "text/html", index_html); 100 | }); 101 | 102 | Serial1.println("🚀 HTTP server started\r\n"); 103 | Serial1.printf("Visit:\r\nhttp://%s:%d/\r\n\r\n", hostName, HTTP_PORT); 104 | 105 | Serial1.printf("Known hosts:\r\n"); 106 | for (auto const &host : Husarnet.listPeers()) { 107 | Serial1.printf("%s (%s)\r\n", host.second.c_str(), host.first.toString().c_str()); 108 | } 109 | } 110 | 111 | void loop(void) { ; } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > Husarnet for ESP32 has undergone a major refactor making it compatible with the latest upstream core codebase, latest ESP-IDF releases and newest Espressif microcontrollers. Because of that, code in **this repository is outdated** and it should not be used for new designs. It may or may not be updated to reflect the changes later in the future. 3 | > 4 | > [Learn more about our latest changes here!](https://husarnet.com/docs/esp32-idf/) 5 | 6 | --- 7 | 8 | # esp32-internet-ota 9 | 10 | ESP32 + GitHub Actions + Husarnet. 11 | 12 | A boilerplate project for ESP32 allowing in-field firmware update using GitHub Actions workflow. 13 | 14 | > **Prerequisites** 15 | > 16 | > Install platformio CLI: 17 | > 18 | > ```bash 19 | > pip3 install -U platformio 20 | > ``` 21 | > 22 | > If you are working in [Visual Studio Code](https://code.visualstudio.com/) the [PlatformIO extension](https://platformio.org/install/ide?install=vscode) will be helpful. 23 | 24 | ## Quick start 25 | 26 | ### First setup 27 | 28 | 1. Click **[Use this template](https://github.com/husarnet/esp32-internet-ota/generate)** button to create your own copy of this repo. Clone the repo, and open it: 29 | 30 | ```bash 31 | git clone https://github.com/husarnet/esp32-internet-ota 32 | cd esp32-internet-ota/ 33 | ``` 34 | 35 | 2. Erase the ESP32 flash 36 | 37 | ```bash 38 | pio run --target erase 39 | # or 40 | # esptool.py erase_flash 41 | ``` 42 | 43 | 3. Prepare the `.env` file: 44 | 45 | ```bash 46 | cp .env.template .env 47 | ``` 48 | 49 | And edit its content: 50 | 51 | ```bash 52 | export WIFI_SSID="" 53 | export WIFI_PASS="" 54 | export HUSARNET_JOINCODE=" **Tip** 59 | > 60 | > If your SSID or WLAN Password contains special characters (ex. ```$ & ( ) ? ; : , . < > | ` ```) you need to escape them like this:: 61 | > 62 | > If WiFi password is ```$&()?``` then: 63 | > ```bash 64 | > export WIFI_PASS="\$\&\(\)\?" 65 | > ``` 66 | > Some special characters (ex. ```' " \ ```) can't be escaped. 67 | > We recommend to not use special characters and emojis in WiFi SSID/Password. 68 | > 69 | 70 | 3. The first time you need to upload the firmware over the USB cable: 71 | 72 | ```bash 73 | source .env 74 | pio run -e serial_upload --target upload 75 | ``` 76 | 77 | 4. Open the Platformio Device Monitor: 78 | 79 | ```bash 80 | pio device monitor 81 | ``` 82 | 83 | If you will open a serial monitor you will see the similar output (the first time you may wait up to 2 minutes): 84 | 85 | ```bash 86 | ************************************** 87 | GitHub Actions OTA example 88 | ************************************** 89 | 90 | 📻 1. Connecting to: FreeWifi Wi-Fi network .. done 91 | 92 | ⌛ 2. Waiting for Husarnet to be ready ... done 93 | 94 | 🚀 HTTP server started 95 | 96 | Visit: 97 | http://my-esp32:8080/ 98 | 99 | Known hosts: 100 | my-laptop (fc94:a4c1:1f22:ab3b:b04a:1a3b:ba15:84bc) 101 | my-esp32 (fc94:f632:c8d9:d2a6:ad18:ed16:ed7e:9f3f) 102 | ``` 103 | 104 | 4. Visit http://my-esp32:8080/ from your laptop (that should be in the same Husarnet group) and if the website is avaialble you can test OTA upgrade from the level of your laptop: 105 | 106 | ```bash 107 | source .env 108 | pio run -e ota_upload --target upload 109 | ``` 110 | 111 | 5. If it works you can configure your GitHub repository. 112 | 113 | ### Editing in Visual Studio Code with a PlatformIO extenstion 114 | 115 | Source environment variables from `.env` file before opening VS Code. 116 | 117 | ```bash 118 | cd esp32-internet-ota/ 119 | source .env 120 | code . 121 | ``` 122 | 123 | ### Internet OTA with GitHub Actions 124 | 125 | > **warning!** 126 | > 127 | > Before launching an Internet OTA, you need to upload the [initial firmware](#first-setup) at first to make your board accessible over the Husarnet VPN. 128 | 129 | 1. Create the folowing GitHub repository secrets (`Settings` > `Secrets` > `New repository secret`): 130 | 131 | | Secret | Sample Value | Desription | 132 | | - | - | - | 133 | | `WIFI_SSID` | FreeWifi | just your WiFi network name | 134 | | `WIFI_PASS` | hardtoguess | ... and password | 135 | | `HUSARNET_JOINCODE` | fc94:...:932a/xhfqwPxxxetyCExsSPRPn9 | find your own **secret** Join Code at your user account at https://app/husarnet.com > `choosen network` > `add element` button. Anyone with this Join Code can connect to your Husarnet network | 136 | | `HUSARNET_DASHBOARD_LOGIN` | me@acme.com | A login for your account at https://app.husarnet.com (needed by [Husarnet Action](https://github.com/husarnet/husarnet-action/)) | 137 | | `HUSARNET_DASHBOARD_PASSWORD` | hardtoguess | A password for your account at https://app.husarnet.com (needed by [Husarnet Action](https://github.com/husarnet/husarnet-action/)) | 138 | 139 | 2. Push changes to your repo: 140 | 141 | ```bash 142 | git add * 143 | git commit -m "triggering the workflow" 144 | git push 145 | ``` 146 | 147 | And trigger the workflow manually (`workflow_dispatch`) in your GitHub repository. 148 | 149 | 3. In ~3 minutes the GitHub workflow should finish its job. Visit: `http://my-esp32:8080` URL with a sample "Hello world" website hosted by your ESP32. 150 | 151 | 152 | Of course your laptop need to be connected to the same Husarnet network - you will find quick start guide showing how to do it here: https://husarnet.com/docs/ 153 | 154 | 155 | ## Tips 156 | 157 | ### Monitoring network traffic on `hnet0` interface 158 | 159 | ```bash 160 | sudo tcpflow -p -c -i hnet0 161 | ``` 162 | 163 | ### Accesing a webserver hosted by ESP32 using a public domain 164 | 165 | Here is a blog post showing how to configure Nginx Proxy Manager to **provide a public access to web servers hosted by Husarnet connected devices**: https://husarnet.com/blog/reverse-proxy-gui 166 | 167 | It can be also used o provide the access to a web server hosted by ESP32 using a nice looking link like: `https://my-awesome-esp32.mydomain.com`. 168 | 169 | --------------------------------------------------------------------------------