diff --git a/.github/workflows/ota-update.yml b/.github/workflows/ota-update.yml new file mode 100644 index 0000000..1478615 --- /dev/null +++ b/.github/workflows/ota-update.yml @@ -0,0 +1,46 @@ +name: ESP32 OTA update + +on: + push: + branches: + - 'main' + +jobs: + build: + runs-on: ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Connecting the GitHub workflow to Husarnet VPN network + uses: husarnet/husarnet-action@v2 + with: + join-code: ${{ secrets.HUSARNET_JOINCODE }} + + - name: ESP32 software reset + run: curl -X POST 'http://${{ secrets.HUSARNET_HOSTNAME }}:8080/reset' + + - name: Installing platformio + run: pip3 install -U platformio + + - name: Building a firmware for ESP32 + run: | + export SSID=${{ secrets.WIFI_SSID }} + export PASS=${{ secrets.WIFI_PASS }} + export JOINCODE=${{ secrets.HUSARNET_JOINCODE }} + export HOSTNAME=${{ secrets.HUSARNET_HOSTNAME }} + pio run + + - name: Uploading a firmware to ESP32 + run: > + curl --http0.9 -# -v + -H 'Accept: */*' + -H 'Accept-Encoding: gzip, deflate' + -H 'Connection: keep-alive' + -F "MD5="$(md5sum "${{ github.workspace }}/.pio/build/esp32dev/firmware.bin" | cut -d ' ' -f 1)"" + -F 'firmware=@${{ github.workspace }}/.pio/build/esp32dev/firmware.bin' + 'http://${{ secrets.HUSARNET_HOSTNAME }}:8080/update' + + - name: Stop Husarnet + run: sudo systemctl stop husarnet diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66465ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +src/credentials.h +.vscode/ +firmware.bin +node_modules/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c2f79fa --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Husarnet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a6ecdf --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# esp32-internet-ota + +ESP32 + GitHub Actions + Husarnet. + +A boilerplate project for ESP32 allowing in-field firmware update using GitHub Actions workflow. + +> **Prerequisites** +> +> Install [Visual Studio Code](https://code.visualstudio.com/) with [PlatformIO extension](https://platformio.org/install/ide?install=vscode). + +## Quick start + +### First setup + +1. Click **[Use this template](https://github.com/husarnet/esp32-internet-ota/generate)** button to create your own copy of this repo. + +2. Clone the repo you have just created and open it in Visual Studio Code. Platformio should automatically install all project dependencies. + +3. Rename `credentials-template.h` to `credentials.h` and type your WiFi an Husarnet credentials there (you will find you Husarnet Join Code at https://app.husarnet.com). + +4. Click "PlatformIO: upload" button to flash your ESP32 board connected to your laptop. You will find the following log in the serial monitor: + + ```bash + ************************************** + GitHub Actions OTA example + ************************************** + + 📻 1. Connecting to: FreeWifi Wi-Fi network .. done + + ⌛ 2. Waiting for Husarnet to be ready ... done + + 🚀 HTTP server started + + Visit: + http://my-esp32:8080/ + + Known hosts: + my-laptop (fc94:a4c1:1f22:ab3b:b04a:1a3b:ba15:84bc) + my-esp32 (fc94:f632:c8d9:d2a6:ad18:ed16:ed7e:9f3f) + ``` + +### Internet OTA with GitHub Actions + +1. Create the folowing GitHub repository secrets (`Settings` > `Secrets` > `New repository secret`): + + | Secret | Sample Value | Desription | + | - | - | - | + | `WIFI_SSID` | FreeWifi | just your WiFi network name | + | `WIFI_PASS` | hardtoguess | ... and password | + | `HUSARNET_HOSTNAME` | my-esp32 | hostname under which you want your ESP32 to be available by other peers | + | `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 | + +2. Push changes to your repo: + + ```bash + git add * + git commit -m "triggering the workflow" + git push + ``` + +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. + + + 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/ + + +## Tips + +### Erasing flash memory of ESP32 + +1. Connect ESP32 to your laptop + +2. Install platformio CLI + + ```bash + pip install -U platformio + ``` + +3. Make flash erase: + + ```bash + pio run --target erase + ``` + +### Monitoring network traffic on `hnet0` interface + +```bash +sudo tcpflow -p -c -i hnet0 +``` + +### Accesing a webserver hosted by ESP32 using a public domain + +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 + +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`. \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..ce01c1f --- /dev/null +++ b/platformio.ini @@ -0,0 +1,29 @@ +[env] +platform = espressif32 +framework = arduino +platform_packages = + framework-arduinoespressif32 @ https://github.com/husarnet/arduino-esp32/releases/download/1.0.4-1/arduino-husarnet-esp32.zip +lib_deps = + ; Until our pull requests are merged you need to use AsyncTCP with our fixes for IPv6 + https://github.com/husarnet/AsyncTCP.git + Husarnet ESP32 + ESP Async WebServer + ayushsharma82/AsyncElegantOTA @ ^2.2.6 + +[env:esp32dev] +board = esp32dev +monitor_speed = 115200 +upload_speed = 921600 + +monitor_filters = esp32_exception_decoder, default + +board_build.partitions = min_spiffs.csv +board_build.embed_txtfiles = + src/index.html + +build_flags = + '-DWIFI_SSID="${sysenv.SSID}"' + '-DWIFI_PASS="${sysenv.PASS}"' + '-DHUSARNET_HOSTNAME="${sysenv.HOSTNAME}"' + '-DHUSARNET_JOINCODE="${sysenv.JOINCODE}"' + diff --git a/src/credentials-template.h b/src/credentials-template.h new file mode 100644 index 0000000..d0404e4 --- /dev/null +++ b/src/credentials-template.h @@ -0,0 +1,8 @@ +// WiFi credentials +const char *ssid = "FreeWifi"; +const char *password = "hardtoguess"; + +// Husarnet credentials +const char *hostName = "my-esp32"; +const char *husarnetJoinCode = "fc94:b01d:1803:8dd8:b293:5c7d:7639:932a/xhfqwPxxxetyCExsSPRPn9"; // find at app.husarnet.com +const char *dashboardURL = "default"; // in you use Husarnet self-hosted Dashboard you can specify URL here. Otherwise keep "default" \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..f2a9435 --- /dev/null +++ b/src/index.html @@ -0,0 +1,9 @@ + + + + ESP32 boilerplate + + +

Hello world!

+ + diff --git a/src/simple-webserver.ino b/src/simple-webserver.ino new file mode 100644 index 0000000..20b0f39 --- /dev/null +++ b/src/simple-webserver.ino @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include + +#define HTTP_PORT 8080 + +#if __has_include("credentials.h") + +// For local development (rename credenials-template.h and type your WiFi and +// Husarnet credentials there) +#include "credentials.h" + +#else + +// For GitHub Actions OTA deploment + +// WiFi credentials +const char *ssid = WIFI_SSID; +const char *password = WIFI_PASS; + +// Husarnet credentials +const char *hostName = HUSARNET_HOSTNAME; +const char *husarnetJoinCode = HUSARNET_JOINCODE; // find at app.husarnet.com +const char *dashboardURL = "default"; + +#endif + +AsyncWebServer server(HTTP_PORT); + +// index.html available in "index_html" const String +extern const char index_html_start[] asm("_binary_src_index_html_start"); +const String index_html = String((const char*)index_html_start); + +void setup(void) { + // =============================================== + // Wi-Fi, OTA and Husarnet VPN configuration + // =============================================== + + // remap default Serial (used by Husarnet logs) + Serial.begin(115200, SERIAL_8N1, 16, 17); // from P3 & P1 to P16 & P17 + Serial1.begin(115200, SERIAL_8N1, 3, 1); // remap Serial1 from P9 & P10 to P3 & P1 + + Serial1.println("\r\n**************************************"); + Serial1.println("GitHub Actions OTA example"); + Serial1.println("**************************************\r\n"); + + // Init Wi-Fi + Serial1.printf("📻 1. Connecting to: %s Wi-Fi network ", ssid); + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + static int cnt = 0; + delay(500); + Serial1.print("."); + cnt++; + if (cnt > 10) { + ESP.restart(); + } + } + + Serial1.println(" done\r\n"); + + // Init Husarnet P2P VPN service + Serial1.printf("⌛ 2. Waiting for Husarnet to be ready "); + + Husarnet.selfHostedSetup(dashboardURL); + Husarnet.join(husarnetJoinCode, hostName); + Husarnet.start(); + + // Before Husarnet is ready peer list contains: + // master (0000:0000:0000:0000:0000:0000:0000:0001) + const uint8_t addr_comp[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + bool husarnetReady = 0; + while (husarnetReady == 0) { + Serial1.print("."); + for (auto const &host : Husarnet.listPeers()) { + if (host.first == addr_comp) { + ; + } else { + husarnetReady = 1; + } + } + delay(1000); + } + + Serial1.println(" done\r\n"); + + // define HTTP API for remote reset + server.on("/reset", HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "Reseting ESP32 after 1s ..."); + Serial1.println("Software reset on POST request"); + delay(1000); + ESP.restart(); + }); + + // Init OTA webserver (available under /update path) + AsyncElegantOTA.begin(&server); + server.begin(); + + // =============================================== + // PLACE YOUR APPLICATION CODE BELOW + // =============================================== + + // Example webserver hosting table with known Husarnet Hosts + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/html", index_html); + }); + + Serial1.println("🚀 HTTP server started\r\n"); + Serial1.printf("Visit:\r\nhttp://%s:%d/\r\n\r\n", hostName, HTTP_PORT); + + Serial1.printf("Known hosts:\r\n"); + for (auto const &host : Husarnet.listPeers()) { + Serial1.printf("%s (%s)\r\n", host.second.c_str(), host.first.toString().c_str()); + } +} + +void loop(void) { ; } \ No newline at end of file