diff --git a/ESP32_PrusaConnectCam/WebPage.h b/ESP32_PrusaConnectCam/WebPage.h
index 49ac7ea..254768d 100644
--- a/ESP32_PrusaConnectCam/WebPage.h
+++ b/ESP32_PrusaConnectCam/WebPage.h
@@ -252,7 +252,7 @@ const char page_config_html[] PROGMEM = R"rawliteral(
-
+ pixels
| |
@@ -260,6 +260,16 @@ const char page_config_html[] PROGMEM = R"rawliteral(
| Contrast | Low High |
| Saturation | Low High |
| |
+
+ | Image rotation |
+
+ |
+
| Horizontal mirror | |
| Vertical flip | |
| |
@@ -921,6 +931,7 @@ function get_data(val) {
document.getElementById('brightnessid').value = obj.brightness;
document.getElementById('contrastid').value = obj.contrast;
document.getElementById('saturationid').value = obj.saturation;
+ document.getElementById('image_rotationid').value = obj.image_rotation;
document.getElementById('hmirrorid').checked = obj.hmirror;
document.getElementById('vflipid').checked = obj.vflip;
document.getElementById('lencid').checked = obj.lensc;
diff --git a/ESP32_PrusaConnectCam/camera.cpp b/ESP32_PrusaConnectCam/camera.cpp
index f8202a5..76fc7ba 100644
--- a/ESP32_PrusaConnectCam/camera.cpp
+++ b/ESP32_PrusaConnectCam/camera.cpp
@@ -22,9 +22,14 @@ Camera SystemCamera(&SystemConfig, &SystemLog, FLASH_GPIO_NUM);
Camera::Camera(Configuration* i_conf, Logs* i_log, uint8_t i_FlashPin) {
config = i_conf;
log = i_log;
+
CameraFlashPin = i_FlashPin;
StreamOnOff = false;
frameBufferSemaphore = xSemaphoreCreateMutex();
+
+ PhotoExifData.header = NULL;
+ PhotoExifData.len = 0;
+ PhotoExifData.offset = 0;
}
/**
@@ -101,6 +106,7 @@ void Camera::InitCameraModule() {
/* Camera init */
err = esp_camera_init(&CameraConfig);
+
if (err != ESP_OK) {
log->AddEvent(LogLevel_Warning, "Camera init failed. Error: " + String(err, HEX));
log->AddEvent(LogLevel_Warning, F("Reset ESP32-cam!"));
@@ -138,6 +144,7 @@ void Camera::LoadCameraCfgFromEeprom() {
exposure_ctrl = config->LoadExposureCtrl();
CameraFlashEnable = config->LoadCameraFlashEnable();
CameraFlashTime = config->LoadCameraFlashTime();
+ imageExifRotation = config->LoadCameraImageExifRotation();
}
/**
@@ -250,6 +257,7 @@ void Camera::ReinitCameraModule() {
if (err != ESP_OK) {
log->AddEvent(LogLevel_Warning, "Camera error deinit camera module. Error: " + String(err, HEX));
}
+ delay(100);
InitCameraModule();
ApplyCameraCfg();
}
@@ -266,6 +274,7 @@ void Camera::CapturePhoto() {
return;
}
+ CameraCaptureSuccess = false;
/* check flash, and enable FLASH LED */
if (true == CameraFlashEnable) {
ledcWrite(FLASH_PWM_CHANNEL, FLASH_ON_STATUS);
@@ -277,11 +286,14 @@ void Camera::CapturePhoto() {
if (FrameBuffer) {
esp_camera_fb_return(FrameBuffer);
} else {
+ esp_camera_fb_return(FrameBuffer);
log->AddEvent(LogLevel_Error, F("Camera capture failed training photo"));
+ //ReinitCameraModule();
}
int attempts = 0;
const int maxAttempts = 5;
+ PhotoExifData.header = NULL;
do {
log->AddEvent(LogLevel_Info, F("Taking photo..."));
@@ -303,6 +315,11 @@ void Camera::CapturePhoto() {
FrameBuffer->len = 0;
} else {
log->AddEvent(LogLevel_Info, "Photo OK! " + String(ControlFlag, HEX));
+
+ update_exif_from_cfg(imageExifRotation);
+ get_exif_header(FrameBuffer, &PhotoExifData.header, &PhotoExifData.len);
+ PhotoExifData.offset = get_jpeg_data_offset(FrameBuffer);
+ CameraCaptureSuccess = true;
}
attempts++;
@@ -318,6 +335,33 @@ void Camera::CapturePhoto() {
ledcWrite(FLASH_PWM_CHANNEL, FLASH_OFF_STATUS);
}
xSemaphoreGive(frameBufferSemaphore);
+/*
+ // Save picture
+ File file = SD_MMC.open("/photo.jpg", FILE_WRITE);
+
+ if (file) {
+ size_t ret = 0;
+ if (PhotoExifData.header != NULL) {
+ ret = file.write(PhotoExifData.header, PhotoExifData.len);
+ if (ret != PhotoExifData.len) {
+ Serial.println("Failed\nError while writing header to file");
+ PhotoExifData.offset = 0;
+ }
+ } else {
+ PhotoExifData.offset = 0;
+ }
+
+ ret = file.write(&FrameBuffer->buf[PhotoExifData.offset], FrameBuffer->len - PhotoExifData.offset);
+ if (ret != FrameBuffer->len - PhotoExifData.offset) {
+ Serial.println("Failed\nError while writing to file");
+ } else {
+ Serial.printf("Saved as %s\n", "photo.jpg");
+ }
+ file.close();
+ } else {
+ Serial.printf("Failed\nCould not open file: %s\n", "photo.jpg");
+ }
+ */
}
}
@@ -372,6 +416,10 @@ bool Camera::GetStreamStatus() {
return StreamOnOff;
}
+bool Camera::GetCameraCaptureSuccess() {
+ return CameraCaptureSuccess;
+}
+
/**
@brief Set Frame Size
@param uint16_t - frame size
@@ -440,6 +488,15 @@ camera_fb_t* Camera::GetPhotoFb() {
return FrameBuffer;
}
+/**
+ @brief Get Photo Exif Data
+ @param none
+ @return PhotoExifData_t* - photo exif data
+*/
+PhotoExifData_t* Camera::GetPhotoExifData() {
+ return &PhotoExifData;
+}
+
/**
@brief Copy Photo
@param camera_fb_t* - pointer to camera_fb_t
@@ -741,6 +798,11 @@ void Camera::SetCameraFlashTime(uint16_t i_data) {
CameraFlashTime = i_data;
}
+void Camera::SetCameraImageRotation(uint8_t i_data) {
+ config->SaveCameraImageExifRotation(i_data);
+ imageExifRotation = i_data;
+}
+
/**
@brief Get Photo Quality
@param none
@@ -1030,4 +1092,8 @@ uint16_t Camera::GetCameraFlashTime() {
return CameraFlashTime;
}
+uint8_t Camera::GetCameraImageRotation() {
+ return imageExifRotation;
+}
+
/* EOF */
\ No newline at end of file
diff --git a/ESP32_PrusaConnectCam/camera.h b/ESP32_PrusaConnectCam/camera.h
index ec33ffd..0a5e551 100644
--- a/ESP32_PrusaConnectCam/camera.h
+++ b/ESP32_PrusaConnectCam/camera.h
@@ -19,12 +19,22 @@
#include "soc/rtc_cntl_reg.h"
#include "cfg.h"
+#include "exif.h"
+#include "FS.h"
+#include "SD_MMC.h"
+
#include "Camera_cfg.h"
#include "Arduino.h"
#include "mcu_cfg.h"
#include "var.h"
#include "log.h"
+struct PhotoExifData_t {
+ const uint8_t *header;
+ size_t len;
+ size_t offset;
+};
+
class Camera {
private:
uint8_t PhotoQuality; ///< photo quality
@@ -51,6 +61,9 @@ private:
uint16_t CameraFlashTime; ///< camera fash duration time
uint8_t CameraFlashPin; ///< GPIO pin for LED
framesize_t TFrameSize; ///< framesize_t type for camera module
+ uint8_t imageExifRotation; ///< image rotation. 0 degree: value 1, 90 degree: value 6, 180 degree: value 3, 270 degree: value 8
+
+ bool CameraCaptureSuccess; ///< camera capture success
/* OV2640 camera module pinout and cfg*/
camera_config_t CameraConfig; ///< camera configuration
@@ -60,6 +73,7 @@ private:
SemaphoreHandle_t frameBufferSemaphore; ///< semaphore for frame buffer
float StreamAverageFps; ///< stream average fps
uint16_t StreamAverageSize; ///< stream average size
+ PhotoExifData_t PhotoExifData; ///< photo exif data
Configuration *config; ///< pointer to Configuration object
Logs *log; ///< pointer to Logs object
@@ -78,6 +92,7 @@ public:
void CaptureReturnFrameBuffer();
void SetStreamStatus(bool);
bool GetStreamStatus();
+ bool GetCameraCaptureSuccess();
void StreamSetFrameSize(uint16_t);
void StreamSetFrameFps(float);
@@ -92,6 +107,7 @@ public:
int GetPhotoSize();
String GetPhoto();
camera_fb_t *GetPhotoFb();
+ PhotoExifData_t * GetPhotoExifData();
framesize_t TransformFrameSizeDataType(uint8_t);
void SetFlashStatus(bool);
@@ -119,6 +135,7 @@ public:
void SetExposureCtrl(bool);
void SetCameraFlashEnable(bool);
void SetCameraFlashTime(uint16_t);
+ void SetCameraImageRotation(uint8_t);
uint8_t GetPhotoQuality();
uint8_t GetFrameSize();
@@ -144,6 +161,7 @@ public:
bool GetExposureCtrl();
bool GetCameraFlashEnable();
uint16_t GetCameraFlashTime();
+ uint8_t GetCameraImageRotation();
};
extern Camera SystemCamera; ///< Camera object
diff --git a/ESP32_PrusaConnectCam/cfg.cpp b/ESP32_PrusaConnectCam/cfg.cpp
index 9a594d4..367b6ae 100644
--- a/ESP32_PrusaConnectCam/cfg.cpp
+++ b/ESP32_PrusaConnectCam/cfg.cpp
@@ -166,6 +166,7 @@ void Configuration::DefaultCfg() {
SaveNetworkMask(FACTORY_CFG_NETWORK_STATIC_MASK);
SaveNetworkGateway(FACTORY_CFG_NETWORK_STATIC_GATEWAY);
SaveNetworkDns(FACTORY_CFG_NETWORK_STATIC_DNS);
+ SaveCameraImageExifRotation(FACTORY_CFG_IMAGE_EXIF_ROTATION);
Log->AddEvent(LogLevel_Warning, F("+++++++++++++++++++++++++++"));
}
@@ -823,6 +824,16 @@ void Configuration::SaveNetworkDns(String i_data) {
SaveIpAddress(EEPROM_ADDR_NETWORK_STATIC_DNS_START, i_data);
}
+/**
+ @info Save camera image rotation
+ @param uint8_t - value
+ @return none
+*/
+void Configuration::SaveCameraImageExifRotation(uint8_t i_data) {
+ Log->AddEvent(LogLevel_Verbose, "Save camera image exif rotation: " + String(i_data));
+ SaveUint8(EEPROM_ADDR_IMAGE_ROTATION_START, i_data);
+}
+
/**
@info load refresh interval from eeprom
@param none
@@ -1300,4 +1311,17 @@ String Configuration::LoadNetworkDns() {
return ret;
}
+uint8_t Configuration::LoadCameraImageExifRotation() {
+ uint8_t ret = EEPROM.read(EEPROM_ADDR_IMAGE_ROTATION_START);
+
+ /* check if value is 255. When value is 255, then set default value */
+ if (ret == 255) {
+ ret = 1;
+ }
+
+ Log->AddEvent(LogLevel_Info, "Camera image rotation: " + String(ret));
+
+ return ret;
+}
+
/* EOF */
\ No newline at end of file
diff --git a/ESP32_PrusaConnectCam/cfg.h b/ESP32_PrusaConnectCam/cfg.h
index c0be20f..c3a7583 100644
--- a/ESP32_PrusaConnectCam/cfg.h
+++ b/ESP32_PrusaConnectCam/cfg.h
@@ -71,6 +71,7 @@ public:
void SaveNetworkMask(String);
void SaveNetworkGateway(String);
void SaveNetworkDns(String);
+ void SaveCameraImageExifRotation(uint8_t);
uint8_t LoadRefreshInterval();
String LoadToken();
@@ -111,6 +112,7 @@ public:
String LoadNetworkMask();
String LoadNetworkGateway();
String LoadNetworkDns();
+ uint8_t LoadCameraImageExifRotation();
private:
Logs *Log; ///< Pointer to Logs object
diff --git a/ESP32_PrusaConnectCam/connect.cpp b/ESP32_PrusaConnectCam/connect.cpp
index a37bef0..7aafbf5 100644
--- a/ESP32_PrusaConnectCam/connect.cpp
+++ b/ESP32_PrusaConnectCam/connect.cpp
@@ -122,9 +122,22 @@ bool PrusaConnect::SendDataToBackend(String *i_data, int i_data_length, String i
if (SendPhoto == i_data_type) {
log->AddEvent(LogLevel_Verbose, F("Sendig photo"));
- /* sending photo */
+ /* get photo buffer */
+ bool SendWithExif = false;
uint8_t *fbBuf = camera->GetPhotoFb()->buf;
size_t fbLen = camera->GetPhotoFb()->len;
+
+ /* sending exif data */
+ if (camera->GetPhotoExifData()->header != NULL) {
+ SendWithExif = true;
+ sendet_data += client.write(camera->GetPhotoExifData()->header, camera->GetPhotoExifData()->len);
+ fbBuf += camera->GetPhotoExifData()->offset;
+ fbLen -= camera->GetPhotoExifData()->offset;
+ } else {
+ SendWithExif = false;
+ }
+
+ /* sending photo */
for (size_t i=0; i < fbLen; i += PHOTO_FRAGMENT_SIZE) {
if ((i + PHOTO_FRAGMENT_SIZE) < fbLen) {
sendet_data += client.write(fbBuf, PHOTO_FRAGMENT_SIZE);
@@ -138,6 +151,13 @@ bool PrusaConnect::SendDataToBackend(String *i_data, int i_data_length, String i
client.println("\r\n");
client.flush();
+ /* log message */
+ if (SendWithExif) {
+ SystemLog.AddEvent(LogLevel_Verbose, F("Photo with EXIF data sent"));
+ } else {
+ SystemLog.AddEvent(LogLevel_Warning, F("Photo without EXIF data sent"));
+ }
+
/* sending device information */
} else if (SendInfo == i_data_type) {
log->AddEvent(LogLevel_Verbose, F("Sending info"));
@@ -200,7 +220,14 @@ bool PrusaConnect::SendDataToBackend(String *i_data, int i_data_length, String i
void PrusaConnect::SendPhotoToBackend() {
log->AddEvent(LogLevel_Info, F("Start sending photo to prusaconnect"));
String Photo = "";
- SendDataToBackend(&Photo, camera->GetPhotoFb()->len, "image/jpg", "Photo", HOST_URL_CAM_PATH, SendPhoto);
+ size_t total_len = 0;
+
+ if (camera->GetPhotoExifData()->header != NULL) {
+ total_len = camera->GetPhotoExifData()->len + camera->GetPhotoFb()->len - camera->GetPhotoExifData()->offset;
+ } else {
+ total_len = camera->GetPhotoFb()->len;
+ }
+ SendDataToBackend(&Photo, total_len, "image/jpg", "Photo", HOST_URL_CAM_PATH, SendPhoto);
}
/**
@@ -247,7 +274,11 @@ void PrusaConnect::SendInfoToBackend() {
*/
void PrusaConnect::TakePictureAndSendToBackend() {
camera->CapturePhoto();
- SendPhotoToBackend();
+ if (camera->GetCameraCaptureSuccess() == true) {
+ SendPhotoToBackend();
+ } else {
+ log->AddEvent(LogLevel_Error, F("Error capturing photo. Stop sending to backend!"));
+ }
camera->CaptureReturnFrameBuffer();
}
diff --git a/ESP32_PrusaConnectCam/exif.cpp b/ESP32_PrusaConnectCam/exif.cpp
new file mode 100644
index 0000000..5438ff4
--- /dev/null
+++ b/ESP32_PrusaConnectCam/exif.cpp
@@ -0,0 +1,452 @@
+/**
+ * exif.c - Functions to generate Exif metadata
+ *
+ * Copyright (c) 2019, David Imhoff
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of the author nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include
+#include
+
+#include "exif.h"
+#include "exif_defines.h"
+
+// TIFF header byte order
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+# define TIFF_BYTE_ORDER 0x4949
+#else // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+# define TIFF_BYTE_ORDER 0x4d4d
+#endif // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+
+// Reimplementation of htons() that can be used to initialize a struct member.
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+# define htons_macro(x) \
+ (((x) & 0x00ff) << 8 | \
+ ((x) & 0xff00) >> 8)
+#else // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+# define htons_macro(x) (x)
+#endif // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+
+/**
+ * Type for storing Tiff Rational typed data
+ */
+#pragma pack(1)
+typedef struct {
+ uint32_t num;
+ uint32_t denom;
+} TiffRational;
+#pragma pack()
+
+/**
+ * Type used for IFD entries
+ *
+ * This type is used to store a value within the Tiff IFD.
+ */
+#pragma pack(1)
+typedef struct {
+ uint16_t tag; // Data tag
+ uint16_t type; // Data type
+ uint32_t length; // length of data
+ // Offset of data from start of TIFF data, or data it self if length <= 4.
+ // Always use the IFD_SET_*() macros to modify the value.
+ uint32_t value;
+} IfdEntry;
+#pragma pack()
+
+// Some helper macros to initialize the IFD value's. The types shorter then
+// 4-bytes need to be aligned such that they are directly after the previous
+// field. These macros take care of this.
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+# define IFD_SET_BYTE(VAL) (VAL)
+# define IFD_SET_SHORT(VAL) (VAL)
+# define IFD_SET_LONG(VAL) (VAL)
+# define IFD_SET_OFFSET(REF, VAR) offsetof(REF, VAR)
+# define IFD_SET_UNDEF(V0, V1, V2, V3) \
+ (((V3) & 0xff) << 24 | \
+ ((V2) & 0xff) << 16 | \
+ ((V1) & 0xff) << 8 | \
+ ((V0) & 0xff))
+#else // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+# define IFD_SET_BYTE(VAL) (((VAL) & 0xff) << 24)
+# define IFD_SET_SHORT(VAL) (((VAL) & 0xffff) << 16)
+# define IFD_SET_LONG(VAL) (VAL)
+# define IFD_SET_OFFSET(REF, VAR) offsetof(REF, VAR)
+# define IFD_SET_UNDEF(V0, V1, V2, V3) \
+ (((V0) & 0xff) << 24 | \
+ ((V1) & 0xff) << 16 | \
+ ((V2) & 0xff) << 8 | \
+ ((V3) & 0xff))
+#endif // __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+
+// Amount of entries in the 0th IFD
+#ifdef WITH_GNSS
+# define IFD0_ENTRY_CNT 11
+#else
+# define IFD0_ENTRY_CNT 12
+#endif
+
+// Amount of entries in the Exif private IFD
+#define IFD_EXIF_ENTRY_CNT 6
+
+// Amount of entries in the GPS private IFD
+#define IFD_GPS_ENTRY_CNT 12
+
+// GPS map datum, probably always 'WGS-84'
+#define GPS_MAP_DATUM "WGS-84"
+
+/**
+ * New Jpeg/Exif header
+ *
+ * This defines the new JPEG/Exif header that is added to the images. To keep
+ * it simple, the structure of the header is completely static.
+ */
+#pragma pack(1)
+struct JpegExifHdr {
+ uint16_t jpeg_soi; // htons(0xffd8)
+ uint16_t marker; // htons(0xffe1)
+ uint16_t len; // htons(length)
+ uint8_t exif_identifier[6]; // 'Exif\0\0'
+
+ struct TiffData {
+ struct {
+ uint16_t byte_order; // 'II' || 'MM'
+ uint16_t signature; // 0x002A
+ uint32_t ifd_offset; // 0x8
+ } tiff_hdr;
+ struct {
+ uint16_t cnt; // amount of entries
+ IfdEntry entries[IFD0_ENTRY_CNT];
+ uint32_t next_ifd; // Offset of next IFD, or 0x0 if last IFD
+ } ifd0;
+ struct {
+ TiffRational XYResolution;
+ char make[sizeof(CAMERA_MAKE)];
+ char model[sizeof(CAMERA_MODEL)];
+ char software[sizeof(CAMERA_SOFTWARE)];
+ char datetime[20];
+ } ifd0_data;
+ struct {
+ uint16_t cnt; // amount of entries
+ IfdEntry entries[IFD_EXIF_ENTRY_CNT];
+ uint32_t next_ifd; // Offset of next IFD, or 0x0 if last IFD
+ } ifd_exif;
+#ifdef WITH_GNSS
+ struct {
+ uint16_t cnt; // amount of entries
+ IfdEntry entries[IFD_GPS_ENTRY_CNT];
+ uint32_t next_ifd; // Offset of next IFD, or 0x0 if last IFD
+ } ifd_gps;
+ struct {
+ TiffRational latitude[3];
+ TiffRational longitude[3];
+ TiffRational altitude;
+ TiffRational time_stamp[3];
+ char map_datum[sizeof(GPS_MAP_DATUM)];
+ uint8_t processing_method[8 + 3];
+ char date_stamp[11];
+ } ifd_gps_data;
+#endif //WITH_GNSS
+ } tiff_data;
+} exif_hdr = {
+ htons_macro(0xffd8),
+ htons_macro(0xffe1),
+ htons_macro(sizeof(JpegExifHdr) - offsetof(JpegExifHdr, len)),
+ { 'E', 'x', 'i', 'f', 0, 0 },
+ {
+ .tiff_hdr = { TIFF_BYTE_ORDER, 0x002A, 0x8 },
+ .ifd0 = {
+ .cnt = IFD0_ENTRY_CNT,
+ .entries = {
+ { TagTiffMake,
+ TiffTypeAscii,
+ sizeof(exif_hdr.tiff_data.ifd0_data.make),
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data.make) },
+ { TagTiffModel,
+ TiffTypeAscii,
+ sizeof(exif_hdr.tiff_data.ifd0_data.model),
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data.model) },
+#define TAG_IFD0_ORIENTATION_IDX 2
+ { TagTiffOrientation,
+ TiffTypeShort, 1,
+ IFD_SET_SHORT(1) },
+ { TagTiffXResolution,
+ TiffTypeRational, 1,
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data) },
+ { TagTiffYResolution,
+ TiffTypeRational, 1,
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data) },
+ { TagTiffResolutionUnit,
+ TiffTypeShort, 1,
+ IFD_SET_SHORT(0x0002) },
+ { TagTiffSoftware,
+ TiffTypeAscii,
+ sizeof(exif_hdr.tiff_data.ifd0_data.software),
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data.software) },
+ { TagTiffDateTime,
+ TiffTypeAscii,
+ sizeof(exif_hdr.tiff_data.ifd0_data.datetime),
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd0_data.datetime) },
+ { TagTiffYCbCrPositioning,
+ TiffTypeShort, 1,
+ IFD_SET_SHORT(0x0001) },
+ { TagTiffExifIFD,
+ TiffTypeLong, 1,
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_exif) },
+#ifdef WITH_GNSS
+ { TagTiffGPSIFD,
+ TiffTypeLong, 1,
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps) },
+#endif // WITH_GNSS
+ },
+ .next_ifd = 0
+ },
+ .ifd0_data = {
+ { 72, 1 },
+ CAMERA_MAKE,
+ CAMERA_MODEL,
+ CAMERA_SOFTWARE,
+ " : : : : ",
+ },
+ .ifd_exif = {
+ .cnt = IFD_EXIF_ENTRY_CNT,
+ .entries = {
+ { TagExifVersion,
+ TiffTypeUndef, 4,
+ IFD_SET_UNDEF(0x30, 0x32, 0x33, 0x30) },
+ { TagExifComponentsConfiguration,
+ TiffTypeUndef, 4,
+ IFD_SET_UNDEF(0x01, 0x02, 0x03, 0x00) },
+#define TAG_EXIF_SUBSEC_TIME_IDX 2
+ { TagExifSubSecTime,
+ TiffTypeAscii, 4,
+ IFD_SET_UNDEF(0x20, 0x20, 0x20, 0x00) },
+ { TagExifColorSpace,
+ TiffTypeShort, 1,
+ IFD_SET_SHORT(1) },
+#define TAG_EXIF_PIXEL_X_DIMENSION_IDX 4
+ { TagExifPixelXDimension,
+ TiffTypeShort, 1,
+ IFD_SET_SHORT(1600) },
+#define TAG_EXIF_PIXEL_Y_DIMENSION_IDX (TAG_EXIF_PIXEL_X_DIMENSION_IDX + 1)
+ { TagExifPixelYDimension,
+ TiffTypeShort, 1,
+ IFD_SET_SHORT(1200) },
+ },
+ .next_ifd = 0
+ },
+#ifdef WITH_GNSS
+ .ifd_gps = {
+ .cnt = IFD_GPS_ENTRY_CNT,
+ .entries = {
+ { TagGPSVersionID,
+ TiffTypeByte, 4,
+ IFD_SET_UNDEF(2, 3, 0, 0) },
+#define TAG_GPS_LATITUDE_REF_IDX 1
+ { TagGPSLatitudeRef,
+ TiffTypeAscii, 2,
+ IFD_SET_UNDEF(0x00, 0x00, 0x00, 0x00) },
+ { TagGPSLatitude,
+ TiffTypeRational, 3,
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.latitude) },
+#define TAG_GPS_LONGITUDE_REF_IDX 3
+ { TagGPSLongitudeRef,
+ TiffTypeAscii, 2,
+ IFD_SET_UNDEF(0x00, 0x00, 0x00, 0x00) },
+ { TagGPSLongitude,
+ TiffTypeRational, 3,
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.longitude) },
+#define TAG_GPS_ALTITUDE_REF_IDX 5
+ { TagGPSAltitudeRef,
+ TiffTypeByte, 1,
+ IFD_SET_UNDEF(0x00, 0x00, 0x00, 0x00) },
+ { TagGPSAltitude,
+ TiffTypeRational, 1,
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.altitude) },
+ { TagGPSTimeStamp,
+ TiffTypeRational, 3,
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.time_stamp) },
+#define TAG_GPS_STATUS_IDX 8
+ { TagGPSStatus,
+ TiffTypeAscii, 2,
+ IFD_SET_UNDEF('V', 0x00, 0x00, 0x00) },
+/*TODO: currently not available from MicroNMEA
+ { TagGPSMeasureMode,
+ TiffTypeAscii, 2,
+ IFD_SET_UNDEF('2', 0x00, 0x00, 0x00) },*/
+ { TagGPSMapDatum,
+ TiffTypeAscii,
+ sizeof(exif_hdr.tiff_data.ifd_gps_data.map_datum),
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.map_datum) },
+ { TagGPSProcessingMethod,
+ TiffTypeUndef,
+ sizeof(exif_hdr.tiff_data.ifd_gps_data.processing_method),
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.processing_method) },
+ { TagGPSDateStamp,
+ TiffTypeAscii,
+ sizeof(exif_hdr.tiff_data.ifd_gps_data.date_stamp),
+ IFD_SET_OFFSET(JpegExifHdr::TiffData, ifd_gps_data.date_stamp) },
+ },
+ .next_ifd = 0
+ },
+ .ifd_gps_data = {
+ { { 0, 1000*1000 }, { 0, 1 }, { 0, 1 } },
+ { { 0, 1000*1000 }, { 0, 1 }, { 0, 1 } },
+ { 0, 1000 },
+ { { 0, 1 }, { 0, 1 }, { 0, 1 } },
+ GPS_MAP_DATUM,
+ { 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00, 'G', 'P', 'S' },
+ " : : ",
+ }
+#endif // WITH_GNSS
+ }
+};
+#pragma pack()
+
+bool update_exif_from_cfg(const uint8_t c)
+{
+ exif_hdr.tiff_data.ifd0.entries[TAG_IFD0_ORIENTATION_IDX].value = IFD_SET_SHORT(c);
+
+ return true;
+}
+
+#ifdef WITH_GNSS
+void update_exif_gps(const MicroNMEA& nmea)
+{
+ // Latitude
+ long lat = nmea.getLatitude();
+ if (lat < 0) {
+ exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_LATITUDE_REF_IDX].value = IFD_SET_BYTE('S');
+ lat *= -1;
+ } else {
+ exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_LATITUDE_REF_IDX].value = IFD_SET_BYTE('N');
+ }
+ exif_hdr.tiff_data.ifd_gps_data.latitude[0].num = lat;
+
+ // Longitude
+ long lon = nmea.getLongitude();
+ if (lon < 0) {
+ exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_LONGITUDE_REF_IDX].value = IFD_SET_BYTE('W');
+ lon *= -1;
+ } else {
+ exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_LONGITUDE_REF_IDX].value = IFD_SET_BYTE('E');
+ }
+ exif_hdr.tiff_data.ifd_gps_data.longitude[0].num = lon;
+
+ // Altitude
+ long alt;
+ if (nmea.getAltitude(alt)) {
+ if (alt < 0) {
+ exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_ALTITUDE_REF_IDX].value = IFD_SET_BYTE(1);
+ alt *= -1;
+ } else {
+ exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_ALTITUDE_REF_IDX].value = IFD_SET_BYTE(0);
+ }
+ exif_hdr.tiff_data.ifd_gps_data.altitude.num = alt;
+ } else {
+ exif_hdr.tiff_data.ifd_gps_data.altitude.num = 0;
+ }
+
+ // time stamp
+ exif_hdr.tiff_data.ifd_gps_data.time_stamp[0].num = nmea.getHour();
+ exif_hdr.tiff_data.ifd_gps_data.time_stamp[1].num = nmea.getMinute();
+ exif_hdr.tiff_data.ifd_gps_data.time_stamp[2].num = nmea.getSecond();
+
+ // GPS Status
+ if (nmea.isValid()) {
+ exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_STATUS_IDX].value = IFD_SET_BYTE('A');
+ } else {
+ exif_hdr.tiff_data.ifd_gps.entries[TAG_GPS_STATUS_IDX].value = IFD_SET_BYTE('V');
+ }
+
+ // date stamp
+ snprintf(exif_hdr.tiff_data.ifd_gps_data.date_stamp,
+ sizeof(exif_hdr.tiff_data.ifd_gps_data.date_stamp),
+ "%04u:%02u:%02u",
+ nmea.getYear(),
+ nmea.getMonth(),
+ nmea.getDay());
+}
+#endif // WITH_GNSS
+
+const uint8_t *get_exif_header(camera_fb_t *fb, const uint8_t **exif_buf, size_t *exif_len)
+{
+ // TODO: pass config to function and use that to set some of the image
+ // taking conditions. Or do this only once, with a update config
+ // function????
+
+ // Get current time
+ struct timeval now_tv;
+ if (gettimeofday(&now_tv, NULL) != 0) {
+ now_tv.tv_sec = time(NULL);
+ now_tv.tv_usec = 0;
+ }
+
+ // Set date time
+ struct tm timeinfo;
+ localtime_r(&now_tv.tv_sec, &timeinfo);
+ strftime(exif_hdr.tiff_data.ifd0_data.datetime,
+ sizeof(exif_hdr.tiff_data.ifd0_data.datetime),
+ "%Y:%m:%d %H:%M:%S", &timeinfo);
+
+ // Set sub-seconds time
+ snprintf( (char *) &(exif_hdr.tiff_data.ifd_exif.entries[TAG_EXIF_SUBSEC_TIME_IDX].value), 9, "%03ld", now_tv.tv_usec/1000);
+
+ // Update image dimensions
+ exif_hdr.tiff_data.ifd_exif.entries[TAG_EXIF_PIXEL_X_DIMENSION_IDX].value = IFD_SET_SHORT(fb->width);
+ exif_hdr.tiff_data.ifd_exif.entries[TAG_EXIF_PIXEL_Y_DIMENSION_IDX].value = IFD_SET_SHORT(fb->height);
+
+ *exif_len = sizeof(exif_hdr);
+
+ if (exif_buf != NULL) {
+ *exif_buf = (uint8_t *) &exif_hdr;
+ }
+
+ return (uint8_t *) &exif_hdr;
+}
+
+size_t get_jpeg_data_offset(camera_fb_t *fb)
+{
+ if (fb->len < 6) {
+ return 0;
+ }
+
+ // Check JPEG SOI
+ if (fb->buf[0] != 0xff || fb->buf[1] != 0xd8) {
+ return 0;
+ }
+ size_t data_offset = 2; // Offset to first JPEG segment after header
+
+ // Check if JFIF header
+ if (fb->buf[2] == 0xff && fb->buf[3] == 0xe0) {
+ uint16_t jfif_len = fb->buf[4] << 8 | fb->buf[5];
+
+ data_offset += 2 + jfif_len;
+ }
+
+ if (data_offset >= fb->len) {
+ return 0;
+ }
+
+ return data_offset;
+}
diff --git a/ESP32_PrusaConnectCam/exif.h b/ESP32_PrusaConnectCam/exif.h
new file mode 100644
index 0000000..b37f30e
--- /dev/null
+++ b/ESP32_PrusaConnectCam/exif.h
@@ -0,0 +1,91 @@
+/**
+ * exif.h - Functions to generate Exif metadata
+ *
+ * Copyright (c) 2019, David Imhoff
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of the author nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Project URL: https://github.com/dimhoff/ESP32-CAM_Interval
+ *
+ */
+#ifndef __EXIF_H__
+#define __EXIF_H__
+
+#include
+#include
+#include "esp_camera.h"
+#ifdef WITH_GNSS
+# include
+#endif
+
+#include "mcu_cfg.h"
+
+/**
+ * Update the configurable EXIF header fields
+ *
+ * Update all field in the EXIF header that depend on the configuration. Needs
+ * to be called every time the configuration is changed.
+ *
+ * @returns True on success, else false
+ */
+bool update_exif_from_cfg(const uint8_t);
+
+#ifdef WITH_GNSS
+/**
+ * Update GPS data in EXIF header
+ *
+ * Should be called for every call to get_exif_header() if GNSS support is enabled.
+ */
+void update_exif_gps(const MicroNMEA& nmea);
+#endif // WITH_GNSS
+
+/**
+ * Get Exif header
+ *
+ * Get a buffer with a new Exif header that can be prepended to a JPEG image.
+ * The JPEG does need to be stripped from its original header first, see
+ * get_jpeg_data_offset().
+ *
+ * The returned pointer point so a static buffer, and should not be altered.
+ * This function is not reentrant safe.
+ *
+ * @param fb Frame buffer of captured image. Encoding is expected to
+ * be JPEG
+ * @param exif_buf If not NULL, used to return pointer to Exif buffer in
+ * @param exif_buf Used to return the size of the Exif buffer
+ *
+ * @returns Pointer to Exif buffer, or NULL on error
+ */
+const uint8_t *get_exif_header(camera_fb_t *fb, const uint8_t **exif_buf, size_t *exif_len);
+
+/**
+ * Get offset of first none header byte in buffer
+ *
+ * Get the offset of the first none JPEG header byte in capture buffer. This
+ * can be used to strip the JPEG SOI and JFIF headers from an image.
+ *
+ * @returns offset of first non header byte, or 0 on error
+ */
+size_t get_jpeg_data_offset(camera_fb_t *fb);
+
+#endif // __EXIF_H__
diff --git a/ESP32_PrusaConnectCam/exif_defines.h b/ESP32_PrusaConnectCam/exif_defines.h
new file mode 100644
index 0000000..1552af7
--- /dev/null
+++ b/ESP32_PrusaConnectCam/exif_defines.h
@@ -0,0 +1,214 @@
+/**
+ * exif_defines.h - Defines for constants from Exif v2.3 standard
+ *
+ * Written 2019, David Imhoff
+ *
+ * This is free and unencumbered software released into the public domain.
+ *
+ * Anyone is free to copy, modify, publish, use, compile, sell, or
+ * distribute this software, either in source code form or as a compiled
+ * binary, for any purpose, commercial or non-commercial, and by any
+ * means.
+ *
+ * In jurisdictions that recognize copyright laws, the author or authors
+ * of this software dedicate any and all copyright interest in the
+ * software to the public domain. We make this dedication for the benefit
+ * of the public at large and to the detriment of our heirs and
+ * successors. We intend this dedication to be an overt act of
+ * relinquishment in perpetuity of all present and future rights to this
+ * software under copyright law.
+ *
+ * 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 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.
+ */
+#ifndef __EXIF_DEFINES_H__
+#define __EXIF_DEFINES_H__
+
+// TIFF Rev. 6.0 Attribute Information Used in Exif
+enum {
+ // A. Tags relating to image data structure
+ TagTiffImageWidth = 0x100, // Image width
+ TagTiffImageLength = 0x101, // Image height
+ TagTiffBitsPerSample = 0x102, // Number of bits per component
+ TagTiffCompression = 0x103, // Compression scheme
+ TagTiffPhotometricInterpretation = 0x106, // Pixel composition
+ TagTiffOrientation = 0x112, // Orientation of image
+ TagTiffSamplesPerPixel = 0x115, // Number of components
+ TagTiffPlanarConfiguration = 0x11C, // Image data arrangement
+ TagTiffYCbCrSubSampling = 0x212, // Subsampling ratio of Y to C
+ TagTiffYCbCrPositioning = 0x213, // Y and C positioning
+ TagTiffXResolution = 0x11A, // Image resolution in width direction
+ TagTiffYResolution = 0x11B, // Image resolution in height direction
+ TagTiffResolutionUnit = 0x128, // Unit of X and Y resolution
+
+ // B. Tags relating to recording offset
+ TagTiffStripOffsets = 0x111, // Image data location
+ TagTiffRowsPerStrip = 0x116, // Number of rows per strip
+ TagTiffStripByteCounts = 0x117, // Bytes per compressed strip
+ TagTiffJPEGInterchangeFormat = 0x201, // Offset to JPEG SOI
+ TagTiffJPEGInterchangeFormatLength = 0x202, // Bytes of JPEG data
+
+ // C. Tags relating to image data characteristics
+ TagTiffTransferFunction = 0x12D, // Transfer function
+ TagTiffWhitePoint = 0x13E, // White point chromaticity
+ TagTiffPrimaryChromaticities = 0x13F, // Chromaticities of primaries
+ TagTiffYCbCrCoefficients = 0x211, // Color space transformation matrix coefficients
+ TagTiffReferenceBlackWhite = 0x214, // Pair of black and white reference values
+
+ // D. Other tags
+ TagTiffDateTime = 0x132, // File change date and time
+ TagTiffImageDescription = 0x10E, // Image title
+ TagTiffMake = 0x10F, // Image input equipment manufacturer
+ TagTiffModel = 0x110, // Image input equipment model
+ TagTiffSoftware = 0x131, // Software used
+ TagTiffArtist = 0x13B, // Person who created the image
+ TagTiffCopyright = 0x8298, // Copyright holder
+
+ // Exif private IFD's
+ TagTiffExifIFD = 0x8769, // Exif IFD pointer
+ TagTiffGPSIFD = 0x8825, // Exif IFD pointer
+ TagTiffInteroperabilityIFD = 0xA005, // Exif IFD pointer
+};
+
+// Exif IFD Attribute Information
+enum {
+ // Tags Relating to Version
+ TagExifVersion = 0x9000, // Exif version
+ TagExifFlashpixVersion = 0xA000, // Supported Flashpix version
+
+ // Tag Relating to Image Data Characteristics
+ TagExifColorSpace = 0xA001, // Color space information
+ TagExifGamma = 0xA500, // Gamma
+
+ // Tags Relating to Image Configuration
+ TagExifComponentsConfiguration = 0x9101, // Meaning of each component
+ TagExifCompressedBitsPerPixel = 0x9102, // Image compression mode
+ TagExifPixelXDimension = 0xA002, // Valid image width
+ TagExifPixelYDimension = 0xA003, // Valid image height
+
+ // Tags Relating to User Information
+ TagExifMakerNote = 0x927C, // Manufacturer notes
+ TagExifUserComment = 0x9286, // User comments
+
+ // Tag Relating to Related File Information
+ TagExifRelatedSoundFile = 0xA004, // Related audio file
+
+ // Tags Relating to Date and Time
+ TagExifDateTimeOriginal = 0x9003, // Date and time of original data generation
+ TagExifDateTimeDigitized = 0x9004, // Date and time of digital data generation
+ TagExifSubSecTime = 0x9290, // DateTime subseconds
+ TagExifSubSecTimeOriginal = 0x9291, // DateTimeOriginal subseconds
+ TagExifSubSecTimeDigitized = 0x9292, // DateTimeDigitized subseconds
+
+ // Tags Relating to Picture-Taking Conditions
+ TagExifExposureTime = 0x829A, // Exposure time
+ TagExifFNumber = 0x829D, // F number
+ TagExifExposureProgram = 0x8822, // Exposure program
+ TagExifSpectralSensitivity = 0x8824, // Spectral sensitivity
+ TagExifPhotographicSensitivity = 0x8827, // Photographic Sensitivity
+ TagExifOECF = 0x8828, // Optoelectric conversion factor
+ TagExifSensitivityType = 0x8830, // Sensitivity Type
+ TagExifStandardOutputSensitivity = 0x8831, // Standard Output Sensitivity
+ TagExifRecommendedExposureIndex = 0x8832, // Recommended ExposureIndex
+ TagExifISOSpeed = 0x8833, // ISO Speed
+ TagExifISOSpeedLatitudeyyy = 0x8834, // ISO Speed Latitude yyy
+ TagExifISOSpeedLatitudezzz = 0x8835, // ISO Speed Latitude zzz
+ TagExifShutterSpeedValue = 0x9201, // Shutter speed
+ TagExifApertureValue = 0x9202, // Aperture
+ TagExifBrightnessValue = 0x9203, // Brightness
+ TagExifExposureBiasValue = 0x9204, // Exposure bias
+ TagExifMaxApertureValue = 0x9205, // Maximum lens aperture
+ TagExifSubjectDistance = 0x9206, // Subject distance
+ TagExifMeteringMode = 0x9207, // Metering mode
+ TagExifLightSource = 0x9208, // Light source
+ TagExifFlash = 0x9209, // Flash
+ TagExifFocalLength = 0x920A, // Lens focal length
+ TagExifSubjectArea = 0x9214, // Subject area
+ TagExifFlashEnergy = 0xA20B, // Flash energy
+ TagExifSpatialFrequencyResponse = 0xA20C, // Spatial frequency response
+ TagExifFocalPlaneXResolution = 0xA20E, // Focal plane X resolution
+ TagExifFocalPlaneYResolution = 0xA20F, // Focal plane Y resolution
+ TagExifFocalPlaneResolutionUnit = 0xA210, // Focal plane resolution unit
+ TagExifSubjectLocation = 0xA214, // Subject location
+ TagExifExposureIndex = 0xA215, // Exposure index
+ TagExifSensingMethod = 0xA217, // Sensing method
+ TagExifFileSource = 0xA300, // File source
+ TagExifSceneType = 0xA301, // Scene type
+ TagExifCFAPattern = 0xA302, // CFA pattern
+ TagExifCustomRendered = 0xA401, // Custom image processing
+ TagExifExposureMode = 0xA402, // Exposure mode
+ TagExifWhiteBalance = 0xA403, // White balance
+ TagExifDigitalZoomRatio = 0xA404, // Digital zoom ratio
+ TagExifFocalLengthIn35mmFilm = 0xA405, // Focal length in 35 mm film
+ TagExifSceneCaptureType = 0xA406, // Scene capture type
+ TagExifGainControl = 0xA407, // Gain control
+ TagExifContrast = 0xA408, // Contrast
+ TagExifSaturation = 0xA409, // Saturation
+ TagExifSharpness = 0xA40A, // Sharpness
+ TagExifDeviceSettingDescription = 0xA40B, // Device settings description
+ TagExifSubjectDistanceRange = 0xA40C, // Subject distance range
+
+ // Other Tags
+ TagExifImageUniqueID = 0xA420, // Unique image ID
+ TagExifCameraOwnerName = 0xA430, // Camera Owner Name
+ TagExifBodySerialNumber = 0xA431, // Body Serial Number
+ TagExifLensSpecification = 0xA432, // Lens Specification
+ TagExifLensMake = 0xA433, // Lens Make
+ TagExifLensModel = 0xA434, // Lens Model
+ TagExifLensSerialNumber = 0xA435, // Lens Serial Number
+};
+
+// GPS Attribute Information
+enum {
+ TagGPSVersionID = 0x00, // GPS tag version
+ TagGPSLatitudeRef = 0x01, // North or South Latitude
+ TagGPSLatitude = 0x02, // Latitude
+ TagGPSLongitudeRef = 0x03, // East or West Longitude
+ TagGPSLongitude = 0x04, // Longitude
+ TagGPSAltitudeRef = 0x05, // Altitude reference
+ TagGPSAltitude = 0x06, // Altitude
+ TagGPSTimeStamp = 0x07, // GPS time (atomic clock)
+ TagGPSSatellites = 0x08, // GPS satellites used for measurement
+ TagGPSStatus = 0x09, // GPS receiver status
+ TagGPSMeasureMode = 0x0A, // GPS measurement mode
+ TagGPSDOP = 0x0B, // Measurement precision
+ TagGPSSpeedRef = 0x0C, // Speed unit
+ TagGPSSpeed = 0x0D, // Speed of GPS receiver
+ TagGPSTrackRef = 0x0E, // Reference for direction of movement
+ TagGPSTrack = 0x0F, // Direction of movement
+ TagGPSImgDirectionRef = 0x10, // Reference for direction of image
+ TagGPSImgDirection = 0x11, // Direction of image
+ TagGPSMapDatum = 0x12, // Geodetic survey data used
+ TagGPSDestLatitudeRef = 0x13, // Reference for latitude of destination
+ TagGPSDestLatitude = 0x14, // Latitude of destination
+ TagGPSDestLongitudeRef = 0x15, // Reference for longitude of destination
+ TagGPSDestLongitude = 0x16, // Longitude of destination
+ TagGPSDestBearingRef = 0x17, // Reference for bearing of destination
+ TagGPSDestBearing = 0x18, // Bearing of destination
+ TagGPSDestDistanceRef = 0x19, // Reference for distance to destination
+ TagGPSDestDistance = 0x1A, // Distance to destination
+ TagGPSProcessingMethod = 0x1B, // Name of GPS processing method
+ TagGPSAreaInformation = 0x1C, // Name of GPS area
+ TagGPSDateStamp = 0x1D, // GPS date
+ TagGPSDifferential = 0x1E, // GPS differential correction
+ TagGPSHPositioningError = 0x1F, // Horizontal positioning error
+};
+
+// TIFF type fields values
+enum {
+ TiffTypeByte = 1,
+ TiffTypeAscii = 2,
+ TiffTypeShort = 3,
+ TiffTypeLong = 4,
+ TiffTypeRational = 5,
+ TiffTypeUndef = 7,
+ TiffTypeSLong = 9,
+ TiffTypeSRational = 10,
+};
+
+#endif // __EXIF_DEFINES_H__
diff --git a/ESP32_PrusaConnectCam/mcu_cfg.h b/ESP32_PrusaConnectCam/mcu_cfg.h
index d3ce39f..b19edf4 100644
--- a/ESP32_PrusaConnectCam/mcu_cfg.h
+++ b/ESP32_PrusaConnectCam/mcu_cfg.h
@@ -14,7 +14,7 @@
#define _MCU_CFG_H_
/* ---------------- BASIC MCU CFG --------------*/
-#define SW_VERSION "1.0.2-rc2" ///< SW version
+#define SW_VERSION "1.0.2-rc3" ///< SW version
#define SW_BUILD __DATE__ " " __TIME__ ///< build number
#define CONSOLE_VERBOSE_DEBUG false ///< enable/disable verbose debug log level for console
#define DEVICE_HOSTNAME "Prusa-ESP32cam" ///< device hostname
@@ -99,6 +99,11 @@
#define NTP_GTM_OFFSET_SEC 0 ///< GMT offset in seconds. 0 = UTC. 3600 = GMT+1
#define NTP_DAYLIGHT_OFFSET_SEC 0 ///< daylight offset in seconds. 0 = no daylight saving time. 3600 = +1 hour
+/* ------------------ EXIF CFG ------------------*/
+#define CAMERA_MAKE "OmniVision" ///< Camera make string
+#define CAMERA_MODEL "OV2640" ///< Camera model string
+#define CAMERA_SOFTWARE "Prusa ESP32-cam" ///< Camera software string
+
/* ---------------- FACTORY CFG ----------------*/
#define FACTORY_CFG_PHOTO_REFRESH_INTERVAL 30 ///< in the second
#define FACTORY_CFG_PHOTO_QUALITY 10 ///< 10-63, lower is better
@@ -134,6 +139,7 @@
#define FACTORY_CFG_NETWORK_STATIC_MASK "255.255.255.255" ///< Static Mask
#define FACTORY_CFG_NETWORK_STATIC_GATEWAY "255.255.255.255" ///< Static Gateway
#define FACTORY_CFG_NETWORK_STATIC_DNS "255.255.255.255" ///< Static DNS
+#define FACTORY_CFG_IMAGE_EXIF_ROTATION 1 ///< Image rotation 1 - 0°, 6 - 90°, 3 - 180°, 8 - 270°
/* ---------------- CFG FLAGS ------------------*/
#define CFG_WIFI_SETTINGS_SAVED 0x0A ///< flag saved config
@@ -266,6 +272,8 @@
#define EEPROM_ADDR_NETWORK_STATIC_DNS_START (EEPROM_ADDR_NETWORK_STATIC_GATEWAY_START + EEPROM_ADDR_NETWORK_STATIC_GATEWAY_LENGTH)
#define EEPROM_ADDR_NETWORK_STATIC_DNS_LENGTH 4
+#define EEPROM_ADDR_IMAGE_ROTATION_START (EEPROM_ADDR_NETWORK_STATIC_DNS_START + EEPROM_ADDR_NETWORK_STATIC_DNS_LENGTH)
+#define EEPROM_ADDR_IMAGE_ROTATION_LENGTH 1
#define EEPROM_SIZE (EEPROM_ADDR_REFRESH_INTERVAL_LENGTH + EEPROM_ADDR_FINGERPRINT_LENGTH + EEPROM_ADDR_TOKEN_LENGTH + \
EEPROM_ADDR_FRAMESIZE_LENGTH + EEPROM_ADDR_BRIGHTNESS_LENGTH + EEPROM_ADDR_CONTRAST_LENGTH + \
@@ -281,7 +289,7 @@
EEPROM_ADDR_AEC_VALUE_LENGTH + EEPROM_ADDR_GAIN_CTRL_LENGTH + EEPROM_ADDR_AGC_GAIN_LENGTH + EEPROM_ADDR_LOG_LEVEL_LENGTH + \
EEPROM_ADDR_HOSTNAME_LENGTH + EEPROM_ADDR_SERVICE_AP_ENABLE_LENGTH + EEPROM_ADDR_NETWORK_IP_METHOD_LENGTH +\
EEPROM_ADDR_NETWORK_STATIC_IP_LENGTH + EEPROM_ADDR_NETWORK_STATIC_MASK_LENGTH + EEPROM_ADDR_NETWORK_STATIC_GATEWAY_LENGTH + \
- EEPROM_ADDR_NETWORK_STATIC_DNS_LENGTH) ///< how many bits do we need for eeprom memory
+ EEPROM_ADDR_NETWORK_STATIC_DNS_LENGTH + EEPROM_ADDR_IMAGE_ROTATION_LENGTH) ///< how many bits do we need for eeprom memory
#endif
diff --git a/ESP32_PrusaConnectCam/server.cpp b/ESP32_PrusaConnectCam/server.cpp
index 5ee80a9..b4ecf94 100644
--- a/ESP32_PrusaConnectCam/server.cpp
+++ b/ESP32_PrusaConnectCam/server.cpp
@@ -40,8 +40,28 @@ void Server_InitWebServer() {
if (Server_CheckBasicAuth(request) == false)
return;
+ if (SystemCamera.GetCameraCaptureSuccess() == false) {
+ request->send_P(404, "text/plain", "Photo not found!");
+ return;
+ }
+
SystemLog.AddEvent(LogLevel_Verbose, "Photo size: " + String(SystemCamera.GetPhotoFb()->len) + " bytes");
- request->send_P(200, "image/jpg", SystemCamera.GetPhotoFb()->buf, SystemCamera.GetPhotoFb()->len);
+
+ if (SystemCamera.GetPhotoExifData()->header != NULL) {
+ /* send photo with exif data */
+ SystemLog.AddEvent(LogLevel_Verbose, F("Send photo with EXIF data"));
+ size_t total_len = SystemCamera.GetPhotoExifData()->len + SystemCamera.GetPhotoFb()->len - SystemCamera.GetPhotoExifData()->offset;
+ auto response = request->beginResponseStream("image/jpg");
+ response->addHeader("Content-Length", String(total_len));
+ response->write(SystemCamera.GetPhotoExifData()->header, SystemCamera.GetPhotoExifData()->len);
+ response->write(&SystemCamera.GetPhotoFb()->buf[SystemCamera.GetPhotoExifData()->offset], SystemCamera.GetPhotoFb()->len - SystemCamera.GetPhotoExifData()->offset);
+ request->send(response);
+
+ } else {
+ /* send photo without exif data */
+ SystemLog.AddEvent(LogLevel_Verbose, F("Send photo without EXIF data"));
+ request->send_P(200, "image/jpg", SystemCamera.GetPhotoFb()->buf, SystemCamera.GetPhotoFb()->len);
+ }
});
/* route to jquery */
@@ -381,7 +401,7 @@ void Server_InitWebServer_Actions() {
return;
SystemCamera.CapturePhoto();
request->send_P(200, "text/plain", "Take Photo");
- SystemCamera.CaptureReturnFrameBuffer();
+ SystemCamera.CaptureReturnFrameBuffer();
});
/* route for send photo to prusa backend */
@@ -524,6 +544,14 @@ void Server_InitWebServer_Sets() {
response = true;
}
+ /* set image exif rotation */
+ if (request->hasParam("image_rotation")) {
+ SystemLog.AddEvent(LogLevel_Verbose, F("Set image EXIF rotation"));
+ SystemCamera.SetCameraImageRotation(request->getParam("image_rotation")->value().toInt());
+ response_msg = MSG_SAVE_OK;
+ response = true;
+ }
+
/* set log level /set_int?log_level=2 */
if (request->hasParam("log_level")) {
SystemLog.AddEvent(LogLevel_Verbose, F("Set log_level"));
@@ -1061,6 +1089,7 @@ String Server_GetJsonData() {
doc_json["net_mask"] = SystemWifiMngt.GetNetStaticMask();
doc_json["net_gw"] = SystemWifiMngt.GetNetStaticGateway();
doc_json["net_dns"] = SystemWifiMngt.GetNetStaticDns();
+ doc_json["image_rotation"] = SystemCamera.GetCameraImageRotation();
doc_json["sw_build"] = SW_BUILD;
doc_json["sw_ver"] = SW_VERSION;
doc_json["sw_new_ver"] = FirmwareUpdate.NewVersionFw;
diff --git a/ESP32_PrusaConnectCam/server.h b/ESP32_PrusaConnectCam/server.h
index 55d5cae..9e6fb98 100644
--- a/ESP32_PrusaConnectCam/server.h
+++ b/ESP32_PrusaConnectCam/server.h
@@ -38,6 +38,8 @@
#include "wifi_mngt.h"
#include "stream.h"
+#include "exif.h"
+
extern AsyncWebServer server; ///< global variable for web server
void Server_LoadCfg();
diff --git a/webpage/page_config.html b/webpage/page_config.html
index 096da17..8d4ccc5 100644
--- a/webpage/page_config.html
+++ b/webpage/page_config.html
@@ -18,7 +18,7 @@
-
+ pixels
| |
@@ -26,6 +26,16 @@
| Contrast | Low High |
| Saturation | Low High |
| |
+
+ | Image rotation |
+
+ |
+
| Horizontal mirror | |
| Vertical flip | |
| |
diff --git a/webpage/scripts.js b/webpage/scripts.js
index e6a7092..26e4e0d 100644
--- a/webpage/scripts.js
+++ b/webpage/scripts.js
@@ -25,6 +25,7 @@ function get_data(val) {
document.getElementById('brightnessid').value = obj.brightness;
document.getElementById('contrastid').value = obj.contrast;
document.getElementById('saturationid').value = obj.saturation;
+ document.getElementById('image_rotationid').value = obj.image_rotation;
document.getElementById('hmirrorid').checked = obj.hmirror;
document.getElementById('vflipid').checked = obj.vflip;
document.getElementById('lencid').checked = obj.lensc;