Skip to content

Commit

Permalink
Add support for Prusa Connect
Browse files Browse the repository at this point in the history
  • Loading branch information
mozgy committed May 15, 2024
1 parent 81d64e1 commit 8c36335
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 27 deletions.
63 changes: 46 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<br>
<img src="doc/esp32-cam-2.jpg" width=20% height=20%>

## Flashing HW helper boards
- CH340 chip, USB micro
- CH340 chip, USB micro<br>
<img src="doc/esp32-cam-mb-1.jpg" width=20% height=20%>

- CH340 chip, USB C, passtru dupont connectors for prototyping
- CH340 chip, USB C, passtru dupont connectors for prototyping<br>
<img src="doc/esp32-cam-mb-2.jpg" width=20% height=20%>

## Pinout
Expand All @@ -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 -
Expand All @@ -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<br>

### Notable Weblinks
Web server listening port is 8080, changed from default 80 for easier router port-mapping, configurable at start of <a href=src/asyncWebServer.cpp>Web Server code</a>
- {CAM_IP:8080}/login<br>
enter credentials
- {CAM_IP}/espReset<br>
- {CAM_IP:8080}/espReset<br>
force complete ESP32-CAM reset
- {CAM_IP}/sdcard<br>
- {CAM_IP:8080}/sdcard<br>
reinit SD Card after (re)inserting microSD
- {CAM_IP}/metrics<br>
- {CAM_IP:8080}/metrics<br>
output metrics for prometheus/grafana nerds
- {CAM_IP}/scan<br>
- {CAM_IP:8080}/scan<br>
JSON display of neighbour WiFi SSID/Channels
- {CAM_IP}/archive<br>
- {CAM_IP:8080}/archive<br>
browse saved timelapse pictures
- {CAM_IP:8080}/prusa<br>
force upload of last captured photo to Prusa Connect

## Configuration details
- <a href=include/camera_model.h>Camera Model</a><br>
uncomment only one of the #define that is correct for your camera board<br>
if you are using ESP32S3-CAM also copy <a href=doc/esp32s3cam.json>ESP32S3-CAM board definition</a> to PlatformIO dir C:\Users\...\.platformio\platforms\espressif32\boards dir and use 'board = esp32s3cam' in <a href=platformio.ini>PlatformIO ini</a>
- <a href=include/variables.h>Config Definitions</a><br>
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<br>
- login to <a href=https://connect.prusa3d.com>Prusa Connect</a>
- choose registered printer
- open 'Camera' menu
- click on 'Add new other camera' and note Token text (copy to clipboard)
- paste that text in <a href=include/credentials.h>credentials</a> line<br>
`static const char* prusaToken = "paste_here";`<br>
- generate fingerprint text with ```uuidgen``` command and paste that text in <a href=include/credentials.h>credentials</a> also<br>
- 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

71 changes: 71 additions & 0 deletions include/certificate.h
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions include/credentials_sample.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions include/mywebserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
36 changes: 34 additions & 2 deletions src/MozzCam.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,6 +51,12 @@ void funcBMETicker( void ) {
tickerBMEMissed++;
}

void funcPrusaTicker( void ) {
tickerPrusaFired = true;
tickerPrusaCounter++;
tickerPrusaMissed++;
}

void prnEspStats( void ) {

uint64_t chipid;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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

}
14 changes: 6 additions & 8 deletions src/asyncWebServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

}

Expand Down Expand Up @@ -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 );

Expand Down
72 changes: 72 additions & 0 deletions src/prusa.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@

#include <stddef.h>
#include <WiFiClientSecure.h>

#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;

}

0 comments on commit 8c36335

Please sign in to comment.