diff --git a/boards.txt b/boards.txt index 2363fe2a6..faa7f94a9 100644 --- a/boards.txt +++ b/boards.txt @@ -41448,7 +41448,7 @@ sensebox_eye.menu.PartitionScheme.tinyuf2=TinyUF2 Compatibility (2MB APP/12MB FF sensebox_eye.menu.PartitionScheme.tinyuf2.build.custom_bootloader=bootloader_tinyuf2 sensebox_eye.menu.PartitionScheme.tinyuf2.build.custom_partitions=partitions_tinyuf2 sensebox_eye.menu.PartitionScheme.tinyuf2.upload.maximum_size=2097152 -sensebox_eye.menu.PartitionScheme.tinyuf2.upload.extra_flags=0x410000 "{runtime.platform.path}/variants/{build.variant}/tinyuf2.bin" +sensebox_eye.menu.PartitionScheme.tinyuf2.upload.extra_flags=0x410000 "{runtime.platform.path}/variants/{build.variant}/tinyuf2.bin" 0x210000 "{runtime.platform.path}/variants/{build.variant}/APOTA.bin" sensebox_eye.menu.PartitionScheme.gen4esp32scheme4=Huge App (16MB APP) sensebox_eye.menu.PartitionScheme.gen4esp32scheme4.build.custom_partitions=gen4esp32_16MBapp sensebox_eye.menu.PartitionScheme.gen4esp32scheme4.upload.maximum_size=16646144 diff --git a/variants/sensebox_eye/APOTA.bin b/variants/sensebox_eye/APOTA.bin new file mode 100644 index 000000000..571336d37 Binary files /dev/null and b/variants/sensebox_eye/APOTA.bin differ diff --git a/variants/sensebox_eye/APOTA.ino b/variants/sensebox_eye/APOTA.ino new file mode 100644 index 000000000..afb741499 --- /dev/null +++ b/variants/sensebox_eye/APOTA.ino @@ -0,0 +1,287 @@ +// APOTA is a arduino fallback sketch that is written to OTA1_Partition. +// APOTA opens an access point which waits to receive a .binfile on /sketch. +// After succesful upload, the file is written to OTA0_Partition and the microcontroller reboots to the newly uploaded sketch. + +#define DISPLAY_ENABLED + +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_ENABLED +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define OLED_RESET -1 +#include +#include +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); +#include +Adafruit_NeoPixel rgb_led_1 = Adafruit_NeoPixel(1, 1, NEO_GRB + NEO_KHZ800); + +#endif +#include "esp_partition.h" +#include "esp_ota_ops.h" +#include "esp_system.h" + +String ssid; +uint8_t mac[6]; + +// Create an instance of the server +WebServer server(80); +bool displayEnabled; + +const int BUTTON_PIN = 0; // GPIO for the button +volatile unsigned long lastPressTime = 0; // Time of last button press +volatile bool doublePressDetected = false; // Flag for double press +const unsigned long doublePressInterval = 500; // Max. time (in ms) between two presses for double press +volatile int pressCount = 0; // Counts the button presses + +const unsigned char epd_bitmap_wifi[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x1f, 0xe0, 0xff, 0x00, 0x00, + 0x00, 0x3f, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x7c, 0x00, 0x03, 0xe0, 0x00, 0x00, 0xf0, 0x00, 0x01, 0xf0, 0x00, 0x01, 0xe0, 0x00, 0x00, 0x78, 0x00, + 0x03, 0xc0, 0x00, 0x00, 0x38, 0x00, 0x07, 0x80, 0x00, 0x00, 0x1c, 0x00, 0x0f, 0x00, 0x06, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x7f, 0xe0, 0x0e, 0x00, + 0x0c, 0x01, 0xff, 0xf0, 0x06, 0x00, 0x00, 0x07, 0xff, 0xfc, 0x02, 0x00, 0x00, 0x0f, 0x80, 0x3e, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x0f, 0x00, 0x00, + 0x00, 0x1c, 0x00, 0x07, 0x80, 0x00, 0x00, 0x38, 0x00, 0x03, 0xc0, 0x00, 0x00, 0x70, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x70, 0x00, 0x00, 0xc0, 0x00, + 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x00, + 0x00, 0x01, 0xe0, 0xf0, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x78, 0x00, 0x00, 0x00, 0x03, 0x80, 0x38, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +// 'checkmark', 44x44px +const unsigned char epd_bitmap_checkmark[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x0e, 0x00, 0xf0, 0x00, 0x00, + 0x00, 0x0f, 0x01, 0xe0, 0x00, 0x00, 0x00, 0x0f, 0x83, 0xc0, 0x00, 0x00, 0x00, 0x07, 0xc7, 0x80, 0x00, 0x00, 0x00, 0x03, 0xef, 0x00, 0x00, 0x00, + 0x00, 0x01, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +void IRAM_ATTR handleButtonPress() { + unsigned long currentTime = millis(); // Get current time + + // Debounce: If the current press is too close to the last one, ignore it + if (currentTime - lastPressTime > 50) { + pressCount++; // Count the button press + + // Check if this is the second press within the double-press interval + if (pressCount == 2 && (currentTime - lastPressTime <= doublePressInterval)) { + doublePressDetected = true; // Double press detected + pressCount = 0; // Reset counter + } + + lastPressTime = currentTime; // Update the time of the last press + } +} + +// Function to switch the boot partition to OTA1 +void setBootPartitionToOTA0() { + const esp_partition_t *ota0_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL); + + if (ota0_partition) { + // Set OTA1 as new boot partition + esp_ota_set_boot_partition(ota0_partition); + Serial.println("Boot partition changed to OTA0. Restarting..."); + + // Restart to boot from the new partition + esp_restart(); + } else { + Serial.println("OTA1 partition not found!"); + } +} + +void setupDisplay() { + displayEnabled = display.begin(SSD1306_SWITCHCAPVCC, 0x3D); + if (displayEnabled) { + display.display(); + delay(100); + display.clearDisplay(); + } +} + +void displayStatusBar(int progress) { + display.clearDisplay(); + display.setCursor(24, 8); + display.println("Sketch wird"); + display.setCursor(22, 22); + display.println("hochgeladen!"); + + display.fillRect(0, SCREEN_HEIGHT - 24, SCREEN_WIDTH - 4, 8, BLACK); // Clear status bar area + display.drawRect(0, SCREEN_HEIGHT - 24, SCREEN_WIDTH - 4, 8, WHITE); // Draw border + int filledWidth = (progress * SCREEN_WIDTH - 4) / 100; // Calculate progress width + display.fillRect(1, SCREEN_HEIGHT - 23, filledWidth - 4, 6, WHITE); // Fill progress bar + + display.setCursor((SCREEN_WIDTH / 2) - 12, SCREEN_HEIGHT - 10); + display.setTextSize(1); + display.setTextColor(WHITE, BLACK); + display.print(progress); + display.println(" %"); + display.display(); +} + +void displayWelcomeScreen() { + display.clearDisplay(); + + // Draw WiFi symbol + display.drawBitmap(0, 12, epd_bitmap_wifi, 44, 44, WHITE); + + // Display SSID text + display.setCursor(40, 13); + display.setTextSize(1); + display.setTextColor(WHITE, BLACK); + display.println("Verbinde dich"); // "Connect" + display.setCursor(60, 27); + display.println("mit:"); // "with" + + // Display SSID + display.setCursor(40, 43); + display.setTextSize(1); // Larger text for SSID + display.print(ssid); + + display.display(); +} + +void displaySuccessScreen() { + display.clearDisplay(); + + // Draw WiFi symbol + display.drawBitmap(0, 12, epd_bitmap_checkmark, 44, 44, WHITE); + + // Display SSID text + display.setCursor(48, 22); + display.setTextSize(1); + display.setTextColor(WHITE, BLACK); + display.println("Erfolgreich"); // "Successfully" + display.setCursor(48, 36); + display.println("hochgeladen!"); // "uploaded!" + + display.display(); +} + +void wipeDisplay() { + display.clearDisplay(); + display.println(""); + display.display(); +} + +void setupWiFi() { + WiFi.macAddress(mac); + char macLastFour[5]; + snprintf(macLastFour, sizeof(macLastFour), "%02X%02X", mac[4], mac[5]); + ssid = "senseBox:" + String(macLastFour); + + // Define the IP address, gateway, and subnet mask + IPAddress local_IP(192, 168, 1, 1); // The new IP address + IPAddress gateway(192, 168, 1, 1); // Gateway address (can be the same as the AP's IP) + IPAddress subnet(255, 255, 255, 0); // Subnet mask + + // Set the IP address, gateway, and subnet mask of the access point + WiFi.softAPConfig(local_IP, gateway, subnet); + + // Start the access point + WiFi.softAP(ssid.c_str()); +} + +void setupOTA() { + // Handle updating process + server.on( + "/sketch", HTTP_POST, + []() { + server.sendHeader("Connection", "close"); + server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); + ESP.restart(); + }, + []() { + HTTPUpload &upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + Serial.setDebugOutput(true); + size_t fsize = UPDATE_SIZE_UNKNOWN; + if (server.clientContentLength() > 0) { + fsize = server.clientContentLength(); + } + Serial.printf("Receiving Update: %s, Size: %d\n", upload.filename.c_str(), fsize); + + Serial.printf("Update: %s\n", upload.filename.c_str()); + if (!Update.begin(fsize)) { //start with max available size + Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + /* flashing firmware to ESP*/ + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + Update.printError(Serial); + } else { + int progress = (Update.progress() * 100) / Update.size(); + displayStatusBar(progress); // Update progress on status bar + } + } else if (upload.status == UPLOAD_FILE_END) { + if (Update.end(true)) { //true to set the size to the current progress + displaySuccessScreen(); + delay(3000); + wipeDisplay(); + } else { + Update.printError(Serial); + } + Serial.setDebugOutput(false); + } + yield(); + } + ); +} + +void setup() { + // Start Serial communication + Serial.begin(115200); + rgb_led_1.begin(); + rgb_led_1.setBrightness(30); + rgb_led_1.setPixelColor(0, rgb_led_1.Color(51, 51, 255)); + rgb_led_1.show(); + + // Configure button pin as input + pinMode(BUTTON_PIN, INPUT_PULLUP); + + // Interrupt for the button + attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), handleButtonPress, FALLING); + +#ifdef DISPLAY_ENABLED + setupDisplay(); +#endif + setupWiFi(); + // Set the ESP32 as an access point + setupOTA(); + server.begin(); +} + +void loop() { + // Handle client requests + server.handleClient(); + +#ifdef DISPLAY_ENABLED + displayWelcomeScreen(); +#endif + + if (doublePressDetected) { + Serial.println("Doppeldruck erkannt!"); // "Double press detected!" + setBootPartitionToOTA0(); +#ifdef DISPLAY_ENABLED + display.setCursor(0, 0); + display.setTextSize(1); + display.setTextColor(WHITE, BLACK); + display.println(""); + display.display(); + delay(50); +#endif + // Restart to boot from the new partition + esp_restart(); + } +} diff --git a/variants/sensebox_eye/variant.cpp b/variants/sensebox_eye/variant.cpp index 42269d113..c802c614c 100644 --- a/variants/sensebox_eye/variant.cpp +++ b/variants/sensebox_eye/variant.cpp @@ -26,9 +26,25 @@ #include "pins_arduino.h" #include "driver/rmt_tx.h" #include "esp_log.h" +#include "esp_partition.h" +#include "esp_system.h" +#include "esp_ota_ops.h" extern "C" { +void blinkLED(uint8_t color[3], rmt_channel_handle_t led_chan, rmt_encoder_handle_t ws2812_encoder, rmt_transmit_config_t tx_config) { + ESP_ERROR_CHECK(rmt_transmit(led_chan, ws2812_encoder, color, sizeof(color), &tx_config)); + rmt_tx_wait_all_done(led_chan, portMAX_DELAY); + + // Wait a moment + delay(50); + + // Turn LED off + uint8_t pixel_off[3] = { 0x00, 0x00, 0x00 }; + ESP_ERROR_CHECK(rmt_transmit(led_chan, ws2812_encoder, pixel_off, sizeof(pixel_off), &tx_config)); + rmt_tx_wait_all_done(led_chan, portMAX_DELAY); +} + void initVariant(void) { rmt_channel_handle_t led_chan = NULL; rmt_tx_channel_config_t tx_chan_config = {}; @@ -52,20 +68,49 @@ void initVariant(void) { ESP_ERROR_CHECK(rmt_enable(led_chan)); - // WS2812 GRB data (green pixel) - uint8_t pixel[3] = { 0x10, 0x00, 0x00 }; // green rmt_transmit_config_t tx_config = { .loop_count = 0 }; - ESP_ERROR_CHECK(rmt_transmit(led_chan, ws2812_encoder, pixel, sizeof(pixel), &tx_config)); - rmt_tx_wait_all_done(led_chan, portMAX_DELAY); - // Wait a moment - delay(50); + uint8_t pixel[3] = { 0x10, 0x00, 0x00 }; // green + blinkLED(pixel, led_chan, ws2812_encoder, tx_config); - // Turn LED off - uint8_t pixel_off[3] = { 0x00, 0x00, 0x00 }; - ESP_ERROR_CHECK(rmt_transmit(led_chan, ws2812_encoder, pixel_off, sizeof(pixel_off), &tx_config)); - rmt_tx_wait_all_done(led_chan, portMAX_DELAY); + // define button pin + pinMode(0, INPUT_PULLUP); + + // keep button pressed + unsigned long pressStartTime = 0; + bool buttonPressed = false; + + // Wait 3.5 seconds for the button to be pressed + unsigned long startTime = millis(); + + // Check if button is pressed + while (millis() - startTime < 3500) { + if (digitalRead(0) == LOW) { + if (!buttonPressed) { + // The button was pressed + buttonPressed = true; + } + } else if (buttonPressed) { + // When the button is pressed and then released, boot into the OTA1 partition + const esp_partition_t *ota1_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); + + if (ota1_partition) { + esp_err_t err = esp_ota_set_boot_partition(ota1_partition); + if (err == ESP_OK) { + uint8_t pixel[3] = { 0x00, 0x00, 0x10 }; // blue + blinkLED(pixel, led_chan, ws2812_encoder, tx_config); + esp_restart(); // restart, to boot OTA1 partition + } else { + uint8_t pixel[3] = { 0x00, 0x10, 0x00 }; // red + blinkLED(pixel, led_chan, ws2812_encoder, tx_config); + ESP_LOGE("OTA", "Error setting OTA1 partition: %s", esp_err_to_name(err)); + } + } + // Abort after releasing the button + break; + } + } } }