From 8c3633573576d8b1beaf2a5d8398293743479071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Miko=C4=8Devi=C4=87?= Date: Wed, 15 May 2024 14:48:54 +0200 Subject: [PATCH] Add support for Prusa Connect --- README.md | 63 ++++++++++++++++++++++--------- include/certificate.h | 71 +++++++++++++++++++++++++++++++++++ include/credentials_sample.h | 3 ++ include/mywebserver.h | 1 + src/MozzCam.cpp | 36 +++++++++++++++++- src/asyncWebServer.cpp | 14 +++---- src/prusa.cpp | 72 ++++++++++++++++++++++++++++++++++++ 7 files changed, 233 insertions(+), 27 deletions(-) create mode 100644 include/certificate.h create mode 100644 src/prusa.cpp diff --git a/README.md b/README.md index 43cda85..3bbdc82 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ # esp32-cam -ESP32 Cam - TimeLapse, Streaming, .. +ESP32 Cam - TimeLapse, Streaming, Prusa Connect .. ## Hardware -- ESP32-CAM board with OV2640 camera module -there are several hardware versions of the board +ESP32-CAM board with OV2640 camera module, there are several hardware versions of the board - original AI-Thinker board - different copies, some exact some with notable differences -- S3 version with more PSRAM, faster +- S3 version with more PSRAM, faster
+ ## Flashing HW helper boards -- CH340 chip, USB micro +- CH340 chip, USB micro
-- CH340 chip, USB C, passtru dupont connectors for prototyping +- CH340 chip, USB C, passtru dupont connectors for prototyping
## Pinout @@ -21,7 +21,7 @@ there are several hardware versions of the board ## Instalation Insert ESP32-CAM into helper board and connect it to PC. -#### Visual Studio Code with PlatformIO IDE +### Visual Studio Code with PlatformIO IDE Open Visual Studio Code and choose 'Open Folder' where this source is unpacked. If this is the very first time using PlatformIO, VSC will do some install and config magic and probably few GUI restarts. Before compiling check file include/credentials_sample.h, fill it with actual WiFi credentials and rename the file to credentials.h. Next check file platformio.ini for upload/flashing details, first time should be wire connection - @@ -40,26 +40,55 @@ Two more steps and we're done - - Explorer - PlatformIO: Upload - PlatformIO - esp32cam - Upload Filesystem Image -#### Arduino IDE +### Arduino IDE Gremlins ate this part - rewrite needed .. ## Using Camera Connect to the DHCP assigned IP and enjoy! -#### Notable Weblinks -- {CAM_IP}/login
+ +### Notable Weblinks +Web server listening port is 8080, changed from default 80 for easier router port-mapping, configurable at start of Web Server code +- {CAM_IP:8080}/login
enter credentials -- {CAM_IP}/espReset
+- {CAM_IP:8080}/espReset
force complete ESP32-CAM reset -- {CAM_IP}/sdcard
+- {CAM_IP:8080}/sdcard
reinit SD Card after (re)inserting microSD -- {CAM_IP}/metrics
+- {CAM_IP:8080}/metrics
output metrics for prometheus/grafana nerds -- {CAM_IP}/scan
+- {CAM_IP:8080}/scan
JSON display of neighbour WiFi SSID/Channels -- {CAM_IP}/archive
+- {CAM_IP:8080}/archive
browse saved timelapse pictures +- {CAM_IP:8080}/prusa
+ force upload of last captured photo to Prusa Connect + +## Configuration details +- Camera Model
+ uncomment only one of the #define that is correct for your camera board
+ if you are using ESP32S3-CAM also copy ESP32S3-CAM board definition to PlatformIO dir C:\Users\...\.platformio\platforms\espressif32\boards dir and use 'board = esp32s3cam' in PlatformIO ini +- Config Definitions
+ several interesting #defines + - set `#undef HAVE_CAMERA` if you don't want to use camera (OV sensor misbehaving or similar) + - set `ESP_CAM_HOSTNAME` for your cam board name + - set `CAM_SERIAL` if you have several camera boards + - set `FLASH_ENABLED true` if you want to use flash LED, currently working only on AI-Thinkers + - set `#undef HAVE_SDCARD` if you don't want to use microSD + - set `TIME_LAPSE_MODE true` for camera board to start saving interval photos to microSD +- Prusa Connect Setup
+ - login to Prusa Connect + - choose registered printer + - open 'Camera' menu + - click on 'Add new other camera' and note Token text (copy to clipboard) + - paste that text in credentials line
+ `static const char* prusaToken = "paste_here";`
+ - generate fingerprint text with ```uuidgen``` command and paste that text in credentials also
+ - compile and upload the firmware to the cam board, in a minute or so picture should be appearing on Prusa Printer Web ## ToDo -- add AP/Config mode at the very first start -- rewrite archive GUI to be much more prettier +- [] add AP/Config mode at the very first start +- [] add GUI config mode for hardcoded #defines +- [] rewrite archive GUI to be much more pwetty +- [] better doc about all hardcoded links +- [] better doc about PC USB drivers diff --git a/include/certificate.h b/include/certificate.h new file mode 100644 index 0000000..d27c716 --- /dev/null +++ b/include/certificate.h @@ -0,0 +1,71 @@ +#ifndef _CERTIFICATE_H_ +#define _CERTIFICATE_H_ + +/* +#raspi4# echo -n | openssl s_client -servername connect.prusa3d.com -connect connect.prusa3d.com:443 | sed --quiet --expression='/-.BEGIN/,/-.END/p' +#raspi4# echo -n | openssl s_client -showcerts -connect connect.prusa3d.com:443 | sed --quiet --expression='/-.BEGIN/,/-.END/p' + */ + +const char root_CAs[] PROGMEM = R"rawliteral( +-----BEGIN CERTIFICATE----- +MIIE8jCCA9qgAwIBAgISA5WPixrzuzQqW6S4eTCMs/QwMA0GCSqGSIb3DQEBCwUA +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD +EwJSMzAeFw0yNDAzMzExMjA3MzJaFw0yNDA2MjkxMjA3MzFaMB4xHDAaBgNVBAMT +E2Nvbm5lY3QucHJ1c2EzZC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC913NEofbO3KUwed8auduRGIsnJDOPcv0lOdgGmm8awPHPt7jXREnMEyQV +gLVTQ43GddrCWN2GNzY4LMX5s6jZxsZrDQhPyJDqTA2mf2y5JEaNt6pphmW+d37R +sy7vejgxMmN9OIf7jX5zjJ4aDoRfzaSB36GHyP5rqXEtYWbIVMW8niBo+qdywyJb +7FvgWb8ddJMdl/vBih+arR979EeAUXLk73uNGK9T1qZYlOmZ6+Yg6tLlNvoSudqH +l2y7BpicW9W9B8GhQDGhaFcmc3kJlTg/RZ617Th3+L4m3NYXMCppIdSD9Y7HSGJG +0yg4DYdMl0lbMR+XeMyDIbFmV+XhAgMBAAGjggIUMIICEDAOBgNVHQ8BAf8EBAMC +BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAw +HQYDVR0OBBYEFC4UuVBhFH+WQtKmHKi7f3ZEW4bWMB8GA1UdIwQYMBaAFBQusxe3 +WFbLrlAJQOYfr52LFMLGMFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0 +cDovL3IzLm8ubGVuY3Iub3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5j +ci5vcmcvMB4GA1UdEQQXMBWCE2Nvbm5lY3QucHJ1c2EzZC5jb20wEwYDVR0gBAww +CjAIBgZngQwBAgEwggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdgA7U3d1Pi25gE6L +MFsG/kA7Z9hPw/THvQANLXJv4frUFwAAAY6Un1BTAAAEAwBHMEUCIQCoXoxmXQ0F +bk4Uwsig9jMTT9xz2qneCQa75orGJPjGxgIgSH81c5btJgvBGOXksf8O9hpU7g8G +2NjJr1f0UxczIXcAdQDuzdBk1dsazsVct520zROiModGfLzs3sNRSFlGcR+1mwAA +AY6Un1BfAAAEAwBGMEQCIEfpKGsm7ucXRA8OKtf2FA7jjkh3scrtLlPVKmRm5AVE +AiBQ9KgGAzdgrPHSt7QVkeVgzEH4u8HadLBSIgtdTU0Z4DANBgkqhkiG9w0BAQsF +AAOCAQEAkdLOXMI03XaGbSk65w/U8IuviYlSmDFd2dSG1KVZHMbLQe+pIWVGlZK6 +DS/Nb+LodWiIIdbHBCdjRbk5nb0oIXTnFbaLfC+IEADRwxyMMNOnIL0Fs5cr+WDe +rx1KeDCYQrV5Qzai6ANzAetOy/TqIQmVtejM837LIZzbN963jU+TqlzQytfxxeQe +W/ZKQ9qlLI6804QMPGvsFZhiRVaJjndspBzBIN2RiWwqgWqCr+57oEbQJsATF7hH +ze2yMgg5WmMYOXRmd7fugLNR53JdWpeWogXRmnWc0DZ/M68rgcqpACpCSFzyIkUE +lkKX45bNBdAhLDz/0miY0SZwwVH7eQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw +WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP +R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx +sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm +NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg +Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG +/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB +Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA +FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw +AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw +Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB +gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W +PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl +ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz +CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm +lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4 +avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2 +yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O +yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids +hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+ +HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv +MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX +nLRbwHOoq7hHwg== +-----END CERTIFICATE----- +)rawliteral"; + +#endif diff --git a/include/credentials_sample.h b/include/credentials_sample.h index 7b65eba..b9ca41c 100644 --- a/include/credentials_sample.h +++ b/include/credentials_sample.h @@ -7,4 +7,7 @@ static const char* password = "****"; static const char* http_username = "admin"; static const char* http_password = "admin"; +static const char* prusaToken = "get from connect.prusa3d.com"; +static const char* camFingerPrint = "run uuidgen"; + #endif diff --git a/include/mywebserver.h b/include/mywebserver.h index 31dbf64..a754f6d 100644 --- a/include/mywebserver.h +++ b/include/mywebserver.h @@ -32,6 +32,7 @@ void listDirectory( String, AsyncWebServerRequest* ); esp_err_t loadFromSDCard( AsyncWebServerRequest* ); void initAsyncWebServer( void ); void doSnapSavePhoto( void ); +extern String photoSendPrusaConnect( void ); String getHTMLRootText( void ); String getHTMLStatisticsText( void ); diff --git a/src/MozzCam.cpp b/src/MozzCam.cpp index 03c018c..8d5466c 100644 --- a/src/MozzCam.cpp +++ b/src/MozzCam.cpp @@ -31,12 +31,13 @@ bool SDCardOK; bool bme280Found; -Ticker tickerCam, tickerBME; -boolean tickerCamFired, tickerBMEFired; +Ticker tickerCam, tickerBME, tickerPrusa; +boolean tickerCamFired, tickerBMEFired, tickerPrusaFired; int tickerCamCounter, tickerCamMissed; int waitTime = 60; int oldTickerValue; int tickerBMECounter, tickerBMEMissed; +int tickerPrusaCounter, tickerPrusaMissed; void funcCamTicker( void ) { tickerCamFired = true; @@ -50,6 +51,12 @@ void funcBMETicker( void ) { tickerBMEMissed++; } +void funcPrusaTicker( void ) { + tickerPrusaFired = true; + tickerPrusaCounter++; + tickerPrusaMissed++; +} + void prnEspStats( void ) { uint64_t chipid; @@ -313,6 +320,13 @@ void setup() { tickerCamFired = true; oldTickerValue = waitTime; +#ifdef PRUSA_CONNECT + tickerPrusaCounter = 0; + tickerPrusaMissed = 0; + tickerPrusa.attach( PRUSA_CONNECT_INTERVAL, funcPrusaTicker ); + tickerPrusaFired = true; +#endif + #ifdef HAVE_BME280 tickerBMECounter = 0; tickerBMEMissed = 0; @@ -370,4 +384,22 @@ void loop() { } #endif +#ifdef PRUSA_CONNECT + if( tickerPrusaFired ) { + tickerPrusaFired = false; + + fnElapsedStr( elapsedTimeString ); + log_d( "PrusaConnect tick - %s", elapsedTimeString ); + + String htmlResponse = photoSendPrusaConnect(); + log_d( "PrusaConnect response - %s", htmlResponse.c_str() ); + + if( tickerPrusaMissed > 1 ) { + log_e( "Missed %d tickers", tickerPrusaMissed - 1 ); + } + tickerPrusaMissed = 0; + + } +#endif + } diff --git a/src/asyncWebServer.cpp b/src/asyncWebServer.cpp index 2abfea7..e4b9373 100644 --- a/src/asyncWebServer.cpp +++ b/src/asyncWebServer.cpp @@ -315,15 +315,13 @@ void asyncHandleCapture( AsyncWebServerRequest *request ) { } -void asyncHandleConnectPrusa( AsyncWebServerRequest *request ) { +void asyncHandlePrusaConnect( AsyncWebServerRequest *request ) { - // WiFiClient prusa; + log_d( " asyncHandleConnectPrusa " ); - // prusa.connect( ?.prusa.com, 443 ); - // prusa.println( ALL_HEADERS ); - // prusa.print( photoFrame ); - // prusa.println(); - // prusa.stop(); + String response; + response = photoSendPrusaConnect(); + request->send( 200, "text/plain", response ); } @@ -603,7 +601,7 @@ void initAsyncWebServer( void ) { asyncWebServer.on( "/metrics", HTTP_GET, asyncHandleMetrics ); - asyncWebServer.on( "/prusa", HTTP_POST, asyncHandleConnectPrusa ); // TODO + asyncWebServer.on( "/prusa", HTTP_POST, asyncHandlePrusaConnect ); asyncWebServer.onNotFound( asyncHandleNotFound ); diff --git a/src/prusa.cpp b/src/prusa.cpp new file mode 100644 index 0000000..05d55b6 --- /dev/null +++ b/src/prusa.cpp @@ -0,0 +1,72 @@ + +#include +#include + +#include "variables.h" +#include "credentials.h" +#include "certificate.h" + +extern String photoFrameLength; + +String photoSendPrusaConnect( void ) { + +#define SEND_BLOCK_SIZE 1024 + + WiFiClientSecure prusa; + + prusa.setCACert( root_CAs ); + prusa.setTimeout( 1000 ); + + prusa.connect( "connect.prusa3d.com", 443 ); + prusa.println( "PUT https://connect.prusa3d.com/c/snapshot HTTP/1.0" ); + prusa.println( "Host: connect.prusa3d.com" ); + prusa.println( "User-Agent: ESP32-CAM Family" ); + prusa.println( "Connection: close" ); + + // camFingerPrint += String( ESP.getEfuseMac() ); + + prusa.println( "fingerprint: " + String( camFingerPrint ) ); + log_v( "fingerprint: %s", String( camFingerPrint ).c_str() ); + prusa.println( "token: " + String( prusaToken ) ); + log_v( "token: %s", String( prusaToken ).c_str() ); + prusa.println( "Content-Type: image/jpeg" ); + uint32_t photoFrameLength = photoFrame.length(); + prusa.println( "Content-Length: " + String( photoFrameLength ) ); + log_v( "Content-Length: %s", String( photoFrameLength ) ); + prusa.println( ); + + char *photoFBPointer = &photoFrame[0]; + size_t loopNum = photoFrameLength / SEND_BLOCK_SIZE; + while( loopNum > 0 ) { + log_v( "FB dump loop number %d", loopNum ); + prusa.write( (const uint8_t*)photoFBPointer, SEND_BLOCK_SIZE ); + prusa.flush(); + photoFBPointer += SEND_BLOCK_SIZE; + loopNum--; + } + size_t theRest = photoFrameLength % SEND_BLOCK_SIZE; + if( theRest != 0 ) { // corner case of photo length exactly multiple of SEND_BLOCK_SIZE + prusa.write( (const uint8_t*)photoFBPointer, theRest ); + } + prusa.println( "\r\n" ); + prusa.flush(); + + String response = ""; + String fullResponse = ""; + while( prusa.connected() ) { + if( prusa.available() ) { + response = prusa.readStringUntil( '\n' ); + log_v( "%s", response.c_str() ); + // fullResponse += response; + if( response[0] == '{' ) { + fullResponse = response; // line with - Content-Type: application/json + } + } + } + prusa.stop(); + + // log_v( "%s", fullResponse.c_str() ); // bugged output - no proper CRLF + + return fullResponse; + +}