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( ContrastLow High SaturationLow 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 @@ ContrastLow High SaturationLow 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;