diff --git a/README.md b/README.md index c76a44ac..fa0f8818 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ ![logo](https://github.com/paxo-phone/PaxOS-9/assets/45568523/ddb3b517-605c-41b4-8c1e-c8e5d156431b) -[![PlatformIO](https://github.com/paxo-phone/PaxOS-9/actions/workflows/platformio.yml/badge.svg)](https://github.com/paxo-phone/PaxOS-9/actions/workflows/platformio.yml) [![PlatformIO CI](https://github.com/paxo-phone/PaxOS-9/actions/workflows/platformio-ci.yml/badge.svg)](https://github.com/paxo-phone/PaxOS-9/actions/workflows/platformio-ci.yml) **PaxOS 9** est la dernière version du **PaxOS**, un système d'exploitation léger destiné aux PaxoPhones. diff --git a/lib/applications/src/launcher.cpp b/lib/applications/src/launcher.cpp index 6ba24100..3b7015d4 100644 --- a/lib/applications/src/launcher.cpp +++ b/lib/applications/src/launcher.cpp @@ -9,6 +9,7 @@ #include #include #include +#include /** @@ -139,6 +140,9 @@ void applications::launcher::update() { launcherWindow->updateAll(); } + // Update all events + eventHandlerApp.update(); + // Check touch events if (brightnessSliderBox->isFocused(true)) { diff --git a/lib/graphics/src/Image.cpp b/lib/graphics/src/Image.cpp index cd896be8..1104bc12 100644 --- a/lib/graphics/src/Image.cpp +++ b/lib/graphics/src/Image.cpp @@ -24,7 +24,7 @@ uint64_t getFileSize(storage::Path& path) // TODO : Use "Path" std::shared_ptr getFileData(storage::Path& path) { - auto data = std::shared_ptr(new uint8_t[30]); // just for headers + auto data = std::shared_ptr(new uint8_t[2000]); // just for headers storage::FileStream stream(path.str(), storage::Mode::READ); @@ -34,7 +34,7 @@ std::shared_ptr getFileData(storage::Path& path) } size_t i = 0; - while (i < 30) + while (i < 2000) { data.get()[i++] = stream.readchar(); } @@ -50,10 +50,11 @@ namespace graphics { this->m_path = path; if(!path.exists() || !path.isfile()) { - throw libsystem::exceptions::InvalidArgument("Path does not exist : " + path.str() + "."); - // m_width = 0; - // m_height = 0; - // return; + std::cerr << "Path does not exist : " << path.str() << std::endl; + //throw libsystem::exceptions::InvalidArgument("Path does not exist : " + path.str() + "."); // trop radical + m_width = 0; + m_height = 0; + return; } m_data = getFileData(path); diff --git a/lib/graphics/src/Surface.cpp b/lib/graphics/src/Surface.cpp index 70b2e7c8..4c15b761 100644 --- a/lib/graphics/src/Surface.cpp +++ b/lib/graphics/src/Surface.cpp @@ -7,6 +7,7 @@ #include #include +//#include #include #ifdef ESP_PLATFORM @@ -14,6 +15,14 @@ #include #endif +#ifndef JPEG_PIXEL_RGB565 +#define JPEG_PIXEL_RGB565 4 +#endif + +#ifndef JPEG_SUBSAMPLE_444 +#define JPEG_SUBSAMPLE_444 0 +#endif + #include "color.hpp" #include "fonts/Arial-12.h" #include "fonts/Arial-16.h" @@ -65,13 +74,14 @@ namespace graphics { - Surface::Surface(const uint16_t width, const uint16_t height) : m_color(0xFFFF), + Surface::Surface(const uint16_t width, const uint16_t height, const uint8_t color_depth) : m_color(0xFFFF), m_transparent(false), m_transparent_color(0xFFFF), m_font(ARIAL), - m_fontSize(PT_12) + m_fontSize(PT_12), + m_color_depth(color_depth) { - m_sprite.setColorDepth(16); + m_sprite.setColorDepth(m_color_depth); m_sprite.setPsram(true); m_sprite.createSprite(width, height); @@ -286,6 +296,98 @@ namespace graphics #endif } + bool Surface::saveAsJpg(const storage::Path filename) + { + /*JPEG jpg; + int quality = 90; + + LGFX_Sprite sprite = &m_sprite; + + if (sprite.getBuffer() == nullptr || !filename.str().size()) { + Serial.println("Invalid sprite or filename"); + return false; + } + + int width = sprite.width(); + int height = sprite.height(); + + // Open the file for writing + std::ofstream outFile(filename.str(), std::ios::binary); + if (!outFile.is_open()) { + Serial.println("Failed to open file for writing"); + return false; + } + + // JPEG encoder object + JPEGENCODE jpe; + + // Start the JPEG encoding process + int rc = jpg.encodeBegin(&jpe, width, height, JPEG_PIXEL_RGB565, JPEG_SUBSAMPLE_444, quality); + if (rc != 0) { + Serial.println("Failed to start JPEG encoding"); + outFile.close(); + return false; + } + + // Calculate MCU dimensions + int mcu_w = (width + jpe.cx - 1) / jpe.cx; + int mcu_h = (height + jpe.cy - 1) / jpe.cy; + + // Buffer for one MCU + uint8_t ucMCU[64 * 3]; // 8x8 pixels, 3 channels (RGB) + + // Process each MCU + for (int y = 0; y < mcu_h; y++) { + for (int x = 0; x < mcu_w; x++) { + // Extract MCU data from sprite + for (int j = 0; j < 8; j++) { + for (int i = 0; i < 8; i++) { + int px = x * 8 + i; + int py = y * 8 + j; + if (px < width && py < height) { + uint16_t pixel = sprite.readPixel(px, py); + // Convert RGB565 to RGB888 + uint8_t r = ((pixel >> 11) & 0x1F) << 3; + uint8_t g = ((pixel >> 5) & 0x3F) << 2; + uint8_t b = (pixel & 0x1F) << 3; + ucMCU[(j * 8 + i) * 3] = r; + ucMCU[(j * 8 + i) * 3 + 1] = g; + ucMCU[(j * 8 + i) * 3 + 2] = b; + } else { + // Fill with black if outside sprite bounds + ucMCU[(j * 8 + i) * 3] = 0; + ucMCU[(j * 8 + i) * 3 + 1] = 0; + ucMCU[(j * 8 + i) * 3 + 2] = 0; + } + } + } + + // Add MCU to JPEG + rc = jpg.addMCU(&jpe, ucMCU, 8); + if (rc != 0) { + Serial.println("Failed to add MCU"); + outFile.close(); + return false; + } + } + } + + // Finish encoding and get the compressed data + int jpegSize = jpg.close(); + + // Write JPEG data to file + outFile.write(reinterpret_cast(jpe.pOutput), jpegSize); + outFile.close(); + + Serial.print("JPEG file created: "); + Serial.println(filename); + Serial.print("File size: "); + Serial.print(jpegSize); + Serial.println(" bytes");*/ + + return true; + } + void Surface::setFont(const EFont font) { m_font = font; @@ -501,7 +603,8 @@ namespace graphics { // Copy auto copy = lgfx::LGFX_Sprite(); - copy.createSprite(m_sprite.width(), m_sprite.height()); + copy.setColorDepth(m_color_depth); + copy.createSprite(getWidth(), getHeight()); // Apply blur effect for (int32_t x = radius; x < m_sprite.width(); x++) diff --git a/lib/graphics/src/Surface.hpp b/lib/graphics/src/Surface.hpp index fb7d0d82..878e641e 100644 --- a/lib/graphics/src/Surface.hpp +++ b/lib/graphics/src/Surface.hpp @@ -33,9 +33,10 @@ namespace graphics EFont m_font; float m_fontSize; color_t m_text_color; + uint8_t m_color_depth; public: - Surface(uint16_t width, uint16_t height); + Surface(uint16_t width, uint16_t height, const uint8_t color_depth = 16); ~Surface(); [[nodiscard]] uint16_t getWidth() const; @@ -67,6 +68,7 @@ namespace graphics void drawLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2, color_t color); + bool saveAsJpg(const storage::Path filename); void drawImage(const SImage &image, int16_t x, int16_t y, uint16_t w = 0, uint16_t h = 0); void setFont(EFont font); diff --git a/lib/graphics/src/graphics.cpp b/lib/graphics/src/graphics.cpp index 7c259830..b30515f8 100644 --- a/lib/graphics/src/graphics.cpp +++ b/lib/graphics/src/graphics.cpp @@ -4,6 +4,7 @@ // #include "graphics.hpp" +#include "standby.hpp" #include #include @@ -71,6 +72,11 @@ void graphics::setBrightness(uint16_t value) #endif } +LGFX* graphics::getLcd() +{ + return lcd.get(); +} + graphics::GraphicsInitCode graphics::init() { #ifdef ESP_PLATFORM @@ -269,6 +275,8 @@ int getTouch(uint16_t *pPoints) void graphics::touchUpdate() { + if(StandbyMode::state() == true) + return; bool touchState = true; int16_t liveTouchX = 0, liveTouchY = 0; @@ -326,6 +334,12 @@ void graphics::touchUpdate() newTouchY = -1; isTouchRead = false; } + + if(liveTouchX != touchX || liveTouchY != touchY) + { + if(StandbyMode::state() == false) + StandbyMode::trigger(); + } } bool graphics::isTouched() diff --git a/lib/graphics/src/graphics.hpp b/lib/graphics/src/graphics.hpp index b19d9b1d..ee557d23 100644 --- a/lib/graphics/src/graphics.hpp +++ b/lib/graphics/src/graphics.hpp @@ -6,8 +6,9 @@ #ifndef GRAPHICS_HPP #define GRAPHICS_HPP -// #define LGFX_USE_V1 -// #include +#define LGFX_USE_V1 + +#include #ifdef ESP_PLATFORM @@ -18,13 +19,8 @@ #include "lgfx/v1/platforms/sdl/Panel_sdl.hpp" #include "LGFX_AUTODETECT.hpp" -#include "LovyanGFX.hpp" - #endif -#include - - class FT6236G; namespace graphics @@ -37,7 +33,10 @@ namespace graphics class Surface; - enum GraphicsInitCode { + LGFX* getLcd(); + void reInit(); + + enum GraphicsInitCode { SUCCESS, ERROR_NO_TOUCHSCREEN, ERROR_FAULTY_TOUCHSCREEN diff --git a/lib/gsm/src/conversation.hpp b/lib/gsm/src/conversation.hpp index 01b34b47..756660bb 100644 --- a/lib/gsm/src/conversation.hpp +++ b/lib/gsm/src/conversation.hpp @@ -8,6 +8,7 @@ #include #define MESSAGES_LOCATION "apps/messages/data" +#define MESSAGES_IMAGES_LOCATION "apps/messages/images" #define MESSAGES_NOTIF_LOCATION "apps/messages/unread.txt" #define MAX_MESSAGES 40 diff --git a/lib/gsm/src/gsm.cpp b/lib/gsm/src/gsm.cpp index 225ac6e0..926dce91 100644 --- a/lib/gsm/src/gsm.cpp +++ b/lib/gsm/src/gsm.cpp @@ -1,36 +1,37 @@ #include "gsm.hpp" -#include "contacts.hpp" -#include "conversation.hpp" -#include -#include +#include #include #include #include + +#include +#include +#include #include -#include "../../tasks/src/threads.hpp" -#include +#include +#include +#include + +#include "Image.hpp" +#include "Surface.hpp" + +#include "contacts.hpp" +#include "conversation.hpp" +#include "pdu.hpp" const char *daysOfWeek[7] = {"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"}; const char *daysOfMonth[12] = {"Janvier", "Fevrier", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Decembre"}; #ifdef ESP_PLATFORM #include -#include -#include "driver/uart.h" -#include "soc/uart_struct.h" -#include "esp_system.h" #define RX 26 #define TX 27 -#define GSM_UART UART_NUM_2 #define gsm Serial2 #endif -#include - - namespace GSM { std::string data; @@ -41,6 +42,7 @@ namespace GSM uint16_t seconds, minutes, hours, days, months, years = 0; float voltage = -1; int networkQuality = 0; + bool flightMode = false; namespace ExternalEvents { @@ -52,47 +54,66 @@ namespace GSM void init() { #ifdef ESP_PLATFORM - pinMode(32, OUTPUT); // define pin mode - - digitalWrite(32, 1); // power on the module - delay(1000); - digitalWrite(32, 0); - - gsm.begin(115200, SERIAL_8N1, RX, TX); - - // Get the UART number (in this case, UART2 for Serial2) - uart_port_t uart_num = UART_NUM_2; - - // Configure UART to use REF_TICK as clock source - UART2.conf0.tick_ref_always_on = 1; - // Set the UART clock divider - uint32_t sclk_freq = 1000000; // REF_TICK frequency is 1MHz - uint32_t baud_rate = 115200; // Or whatever baud rate you're using - uint32_t clk_div = ((sclk_freq + baud_rate / 2) / baud_rate); + Serial.println("GSM initializing"); + pinMode(32, OUTPUT); // define pin mode - UART2.clk_div.div_int = clk_div; - UART2.clk_div.div_frag = 0; + /*digitalWrite(32, 1); // power on the module + delay(60); // according to the datasheet t > 50ms + digitalWrite(32, 0);*/ - // Reconfigure the baud rate - Serial2.updateBaudRate(baud_rate); + gsm.setRxBufferSize(1024*30); - while (true) + bool rebooted = false; + + while(true) { - - while (gsm.available()) - gsm.read(); - + gsm.begin(115200, SERIAL_8N1, RX, TX); gsm.println("AT\r"); - delay(1000); - - if (gsm.available()) + delay(100); + String data = gsm.readString(); + if (data.indexOf("OK") != -1) { - std::cout << "[GSM] Connected!" << std::endl; + Serial.println("GSM responding at 115200"); + + gsm.println("AT+IPR=921600"); + gsm.flush(); + gsm.updateBaudRate(921600); + return; } else - std::cout << "[GSM] [Error] Disconnected" << std::endl; + { + Serial.println("GSM not responding at 115200, trying 921600"); + Serial.println(data); + + gsm.updateBaudRate(921600); + + gsm.println("AT\r"); + delay(100); + String data = gsm.readString(); + if (data.indexOf("OK") != -1) + { + Serial.println("GSM responding at 921600"); + return; + } + else + { + Serial.println("GSM not responding at 115200 and 921600"); + Serial.println(data); + + + if(!rebooted) + { + Serial.println("Powering on the module"); + digitalWrite(32, 1); // power on the module + delay(60); // according to the datasheet t > 50ms + digitalWrite(32, 0); + rebooted = true; + } + } + } + gsm.end(); } #endif } @@ -102,22 +123,7 @@ namespace GSM #ifdef ESP_PLATFORM download(); - // Get the UART number (in this case, UART2 for Serial2) - uart_port_t uart_num = UART_NUM_2; - - // Configure UART to use REF_TICK as clock source - UART2.conf0.tick_ref_always_on = 1; - - // Set the UART clock divider - uint32_t sclk_freq = 1000000; // REF_TICK frequency is 1MHz - uint32_t baud_rate = 115200; // Or whatever baud rate you're using - uint32_t clk_div = ((sclk_freq + baud_rate / 2) / baud_rate); - - UART2.clk_div.div_int = clk_div; - UART2.clk_div.div_frag = 0; - - // Reconfigure the baud rate - Serial2.updateBaudRate(baud_rate); + gsm.updateBaudRate(BAUDRATE); #endif } @@ -336,7 +342,7 @@ namespace GSM } } - void onMessage() + /*void onMessage() { std::cout << "onMessage data: " << data << std::endl; clearFrom("+CMTI:", "\n"); @@ -427,6 +433,421 @@ namespace GSM if (ExternalEvents::onNewMessage) ExternalEvents::onNewMessage(); + }*/ + + #ifdef ESP_PLATFORM + + namespace Stream { + struct Chunk + { + char* data; + int size; + }; + + std::vector chunks; + bool running = false; + + void stream(storage::FileStream& stream, int size) { + running = true; + + std::cout << "Streaming chunks: " << size << std::endl; + + while (running) + { + while ((chunks.empty() && running)) { + delay(5); + } + + if(!chunks.empty()) + { + std::cout << "Writing chunk of size: " << int(chunks[0].size/1000) << "Ko" << std::endl; + stream.write(chunks[0].data, chunks[0].size); + delete[] chunks[0].data; + chunks.erase(chunks.begin()); + std::cout << "Writing chunk ended: " << int(chunks[0].size/1000) << "Ko" << std::endl; + } + } + } + + void end() + { + running = false; + while(!chunks.empty()) delay(1); + } + + void addChunk(char* chunk, int size) { + chunks.push_back({chunk, size}); + } + } + + #endif + + std::string getHttpMMS(std::string number, std::string url) { + #ifdef ESP_PLATFORM + StandbyMode::triggerPower(); + + // sending the AT command to initialize the HTTP session + send("AT+HTTPINIT", "AT+HTTPINIT", 500); + send("AT+HTTPPARA=\"URL\",\"" + url + "\"\r", "AT+HTTPPARA", 500); + send("AT+HTTPACTION=0", "AT+HTTPACTION", 500); + + // recovering the size of the data + bool result = false; + uint64_t timeout = millis() + 10000; + while (millis() < timeout) { + StandbyMode::triggerPower(); + download(); + + uint i = data.find("+HTTPACTION: "); + if(i != std::string::npos) + { + if(data.find("\r\n", i) != std::string::npos) + { + result = true; + std::cout << "Request to mms succeeded" << std::endl; + break; + } + } + } + + if (!result || data.find("+HTTPACTION: 0,200,") == std::string::npos) { + std::cerr << "Request to mms failed" << std::endl; + std::cout << data << std::endl; + return ""; + } + + std::string sizedata = ""; + + sizedata = data.substr(data.find("+HTTPACTION: 0,200,") + sizeof("+HTTPACTION: 0,200,") - 1); + sizedata = sizedata.substr(0, sizedata.find("\r\n")); + + std::cout << "Size of the Data 0: " << data << std::endl; + + std::cout << "Size of the Data: " << sizedata << std::endl; + int size = atoi(sizedata.c_str()); + + + // initialisation des fichiers + auto list = storage::Path(MESSAGES_IMAGES_LOCATION).listdir(); // liste les images déjà présentes + std::sort(list.begin(), list.end()); // trier par ordre croissant + int lastIndex = (list.size()==0) ? 0 : atoi(list[list.size() - 1].substr(0, list[list.size() - 2].find("p.jpg")).c_str()); // récupere l'index de l'image la plus recente + std::string filename = std::to_string(++lastIndex) + ".jpg"; // ajoute a l'index 1, et voila le nom de l'image + std::string filename_preview = std::to_string(lastIndex) + "p.jpg"; // ajoute a l'index 1, et voila le nom de l'image + + while (filename.size() < 8) filename = "0" + filename; + while (filename_preview.size() < 9) filename_preview = "0" + filename_preview; + + std::cout << "List of images: "; + for (auto& file : list) { + std::cout << file << " "; + } + std::cout << std::endl; + + storage::FileStream filestream((storage::Path(MESSAGES_IMAGES_LOCATION) / filename).str(), storage::Mode::WRITE); // ouvre le fichier de l'image en écriture + eventHandlerApp.setTimeout(new Callback<>([&](){ Stream::stream(filestream, size / 1024 + 1); }), 0); // ouvre un autre thread pour le stream + StandbyMode::triggerPower(); + + gsm.readString(); // vide le buffer + + uint64_t timer = millis(); // pour le timeout + uint64_t timeout_block = 10000; // timeout de 1 secondes + + uint8_t jpg = 0; // 0 = no jpeg, 1 = jpeg found, 2 = jpeg done + char lastChar = 0; // dernier caractère lu + int bufferIndex = 0; // index d'écriture dans le buffer + int bufferSize = 1024 * 100; // taille du buffer + int blockIndex = 0; // index de lecture du bloc + int blockSize = 1024; // taille d'un bloc + int numberOfBlocks = 10; // nombre de blocs à lire en une fois (dépend de la taille du buffer serie pour éviter les overflows) + char* buffer = new char[bufferSize]; // allocation du buffer de lecture // ne contient que du jpeg + + gsm.println("AT+HTTPREAD=0," + String(1024*numberOfBlocks) + "\r"); // Requette au module pour envoyer les premiers blocs de données + + graphics::Surface loading(320, 5); // affiche le chargement parce que l'app en cours est figée + loading.fillRect(0, 0, loading.getWidth(), loading.getHeight(), 0xFFFF); + + for(uint i = 0; i < size;) { // pour tous les caractères de données brutes annoncés + for (uint r = 0; r < numberOfBlocks +1 && i < size; r++) // pour tous les blocs envoyés en une fois + { + while ((gsm.available() && timer + timeout_block > millis())?(gsm.read() != ':'):(true)); // wait for the garbage data to be ignored + while ((gsm.available() && timer + timeout_block > millis())?(gsm.read() != '\n'):(true)); + + if(r == numberOfBlocks) + break; + + int nextBlockSize = (size - i >= blockSize) ? 1024 : (size - i); // size of the current block, that is equal or less than 1024 + while (gsm.available() < nextBlockSize && timer + timeout_block > millis()); // wait for the next block to be downloaded + + timer = millis(); + + if(jpg == 0) // no jpeg for the moment + { + for (uint j = 0; j < nextBlockSize; j++) // parse the block + { + char c = gsm.read(); // read the next char + + if(lastChar == '\xFF' && c == '\xD8') // if a jpg header is found + { + std::cout << "Found JPEG" << std::endl; + + jpg = 1; // set the state to reading jpg + buffer[0] = lastChar; // 0xFF + buffer[1] = c; // 0xD8 + + gsm.readBytes(buffer+2, nextBlockSize - j - 1); // read the rest of the block into the buffer -> buffer = 0xFF 0xD8 [rest of the data...] + bufferIndex = 2 + nextBlockSize - j - 1; // set the writing index to the size that has been written + break; // no need to search for jpg in the rest of the block + } + + lastChar = c; // save the last char if the jpg header is split in two blocks + } + } + else if(jpg == 1) // if is reading the jpg + { + if(bufferIndex + blockSize*2 >= bufferSize) // if the buffer is full, create a new one, and send the other to the stream + { + Stream::addChunk(buffer, bufferIndex); // add the buffer to the stream + bufferIndex = 0; // reset the index + buffer = new char[bufferSize]; // create a new buffer + } + + gsm.readBytes(buffer + bufferIndex, nextBlockSize); // read the next block, and add it to the buffer (after the last block so ```+ bufferIndex```) + + for (int j = (bufferIndex==0)?(0):(-1); j < nextBlockSize - 1 && bufferIndex + j + 1 < bufferSize; j++) // search for the end header; initialise j to -1 if the buffer is not empty, so it can check the header in the last char of the last buffer + { + if(buffer[bufferIndex + j] == 0xFF && buffer[bufferIndex + j + 1] == 0xD9) // if the end header is found + { + std::cout << "End of JPEG" << std::endl; + + jpg = 2; // set the state to ignore the rest of the data + bufferIndex += j + 2; // set the writing index to the end of the end header, so ignore the rest of the data + + Stream::addChunk(buffer, bufferIndex); // add the buffer to the stream + bufferIndex = 0; + + break; + } + } + + if(jpg == 1) // if no end header is found, adding the size of the block to the writing index + bufferIndex += nextBlockSize; + } else // jpg == 2 + { + for (uint j = 0; j < nextBlockSize; j++) // ignore the rest of the data + gsm.read(); + } + + i += nextBlockSize; + } + + gsm.println("AT+HTTPREAD=0," + String(1024*numberOfBlocks) + "\r"); // Read the data + + // loading bar in the terminal + std::cout << "["; + for (uint j = 0; j < 20; j++) + { + if(i < j*size/20) + std::cout << " "; + else + std::cout << "="; + } + std::cout << "] " << int(i/1024) << "Ko" << std::endl; + + // update the graphical update bar + loading.fillRect(0, 0, 320 * i / size, loading.getHeight(), 0); + graphics::showSurface(&loading, 0, 480-5); + + StandbyMode::triggerPower(); + } + + Stream::end(); + filestream.close(); + send("AT+HTTPTERM", "AT+HTTPTERM", 100); // close http connection + + StandbyMode::triggerPower(); + + ///////////////////////////////////////// FULLSCREEN IMAGE PROCESSING ///////////////////////////////////////// + + storage::Path path = (storage::Path(MESSAGES_IMAGES_LOCATION) / filename); // path to the new image + + graphics::SImage i = graphics::SImage(path); // get the size of the image + uint16_t m_width = i.getWidth(); + uint16_t m_height = i.getHeight(); + + float scale_width = 320.0 / m_width; // calculate the scaling factor of the image to fit in the screen + float scale_height = 480.0 / m_height; + float scale = std::min(scale_width, scale_height); + + graphics::Surface sprite(m_width * scale, m_height * scale, 24); // create a surface to draw the image on, to compress it then + sprite.m_sprite.drawJpgFile(path.str().c_str(), 0, 0, 0, 0, 0, 0, scale, scale); // draw the image (can take several seconds) + + StandbyMode::triggerPower(); + + imgdec::encodeJpg(reinterpret_cast(sprite.m_sprite.getBuffer()), sprite.getWidth(), sprite.getHeight(), (storage::Path(MESSAGES_IMAGES_LOCATION) / filename)); // compress the image and save it (take less than 2s) + + sprite.m_sprite.deleteSprite(); + + ///////////////////////////////////////// PREVIEW IMAGE PROCESSING ///////////////////////////////////////// + + scale_width = 60.0 / 320; // calculate the scaling factor of the image to fit in the preview + scale_height = 60.0 / 480; + scale = std::min(scale_width, scale_height); + + StandbyMode::triggerPower(); + + graphics::Surface sprite_preview(60, 60, 24); // create a surface to draw the image on, to compress it then + sprite_preview.fillRect(0, 0, 60, 60, 0xFFFF); + sprite_preview.m_sprite.drawJpgFile(path.str().c_str(), 0, 0, 0, 0, 0, 0, scale, scale); // draw the image (can take several seconds) + //graphics::showSurface(&sprite_preview, 0, 0); + + StandbyMode::triggerPower(); + + path = (storage::Path(MESSAGES_IMAGES_LOCATION) / filename_preview); // path to the new image preview + imgdec::encodeJpg(reinterpret_cast(sprite_preview.m_sprite.getBuffer()), sprite_preview.getWidth(), sprite_preview.getHeight(), path); // compress the image and save it (take less than 2s) + + sprite_preview.m_sprite.deleteSprite(); + + ////////////////////////////////////////// END ///////////////////////////////////////// + + // same procedure as saving message + auto contact = Contacts::getByNumber(number); + + Conversations::Conversation conv; + storage::Path convPath(std::string(MESSAGES_LOCATION) + "/" + number + ".json"); + if (convPath.exists()) + { + Conversations::loadConversation(convPath, conv); + } + else + { + conv.number = number; + } + + conv.messages.push_back({"/" + filename, true, getCurrentTimestamp()}); // true = message de l'autre + Conversations::saveConversation(convPath, conv); + + storage::FileStream file(std::string(MESSAGES_NOTIF_LOCATION), storage::Mode::READ); + std::string content = file.read(); + file.close(); + + std::cerr << content << std::endl; + + if(content.find(number) == std::string::npos) + { + storage::FileStream file2(storage::Path(std::string(MESSAGES_NOTIF_LOCATION)).str(), storage::Mode::APPEND); + file2.write(number + "\n"); + file2.close(); + } + + #endif + + return ""; + } + + void onMessage() + { + send("AT+CMGF=0", "AT+CMGF=0", 100); + + std::string input = send("AT+CMGL=0", "AT+CMGL", 500); + + std::vector pdus; + + std::istringstream iss(input); + std::string line; + + while (std::getline(iss, line)) { + // Remove \r if present at the end of the line + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + + // Check if the line starts with "+CMGL:" + if (line.find("+CMGL:") == 0) { + // The next line should contain the PDU + if (std::getline(iss, line)) { + // Remove \r if present at the end of the line + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + // Remove any whitespace + line.erase(std::remove_if(line.begin(), line.end(), ::isspace), line.end()); + // Add the PDU to the vector + pdus.push_back(line); + } + } + } + + // Print the extracted PDUs + for (const auto& pdu : pdus) { + std::cout << pdu << std::endl; + } + + for (auto pdu : pdus) + { + std::cout << "PDU: " << pdu << std::endl; + try + { + PDU decoder = decodePDU(pdu); + + if(decoder.type != PDU_type::UNKNOWN) + { + std::cout << "Phone number: " << decoder.sender << std::endl; + + if(decoder.type == PDU_type::MMS) + { + std::cout << "MMS: " << decoder.url << std::endl; + std::string number = decoder.sender; + std::string link = decoder.url; + getHttpMMS(number, link); + } + else // PDU_type::SMS + { + std::cout << "Message: " << decoder.message << std::endl; + + auto contact = Contacts::getByNumber(decoder.sender); + + // Ajout du message à la conversation + Conversations::Conversation conv; + storage::Path convPath(std::string(MESSAGES_LOCATION) + "/" + decoder.sender + ".json"); + if (convPath.exists()) + { + Conversations::loadConversation(convPath, conv); + } + else + { + conv.number = decoder.sender; + } + + conv.messages.push_back({decoder.message, true, getCurrentTimestamp()}); // true = message de l'autre + Conversations::saveConversation(convPath, conv); + + storage::FileStream file(std::string(MESSAGES_NOTIF_LOCATION), storage::Mode::READ); + std::string content = file.read(); + file.close(); + + std::cerr << content << std::endl; + + if(content.find(decoder.sender) == std::string::npos) + { + storage::FileStream file2(storage::Path(std::string(MESSAGES_NOTIF_LOCATION)).str(), storage::Mode::APPEND); + file2.write(decoder.sender + "\n"); + file2.close(); + } + } + } + } + catch (const std::out_of_range& e) { + std::cerr << "Erreur : " << e.what() << std::endl; + } + } + + if(pdus.size()) + { + if (ExternalEvents::onNewMessage) + ExternalEvents::onNewMessage(); + } + + send("AT+CMGD=1,1", "AT+CMGD", 1000); } void sendMessage(const std::string &number, const std::string &message) @@ -569,12 +990,14 @@ namespace GSM try { - return std::stof(voltage_str); + voltage = std::stof(voltage_str); } catch (std::exception) { - return 0; + voltage = -1; } + + return 0; } double getBatteryLevel() { @@ -709,6 +1132,32 @@ namespace GSM requests.push_back({&GSM::updateNetworkQuality, priority::normal}); } + void updateFlightMode() + { + if(flightMode) + { + std::cout << "Flight Mode ON" << std::endl; + std::cout << send("AT+CFUN=4", "AT+CFUN", 1000) << std::endl; + } + else + { + std::cout << "Flight Mode OFF" << std::endl; + //std::cout << send("AT+CFUN=6", "AT+CFUN", 1000) << std::endl; + std::cout << send("AT+CFUN=1", "AT+CFUN", 1000) << std::endl; + } + } + + bool isFlightMode() + { + return flightMode; + } + + void setFlightMode(bool mode) + { + eventHandlerBack.setTimeout(new Callback<>([mode](){ appendRequest({updateFlightMode}); }), 0); + flightMode = mode; + } + void run() { init(); @@ -720,11 +1169,12 @@ namespace GSM updateHour(); getNetworkQuality(); onMessage(); + getVoltage(); eventHandlerBack.setInterval(&GSM::getHour, 5000); eventHandlerBack.setInterval(&GSM::getNetworkQuality, 10000); eventHandlerBack.setInterval([](){ requests.push_back({&GSM::getVoltage, GSM::priority::normal}); }, 5000); - // eventHandlerBack.setInterval(new Callback<>([](){if(send("AT", "AT").find("OK") == std::string::npos) init(); }), 15000); + eventHandlerBack.setInterval([](){ requests.push_back({&GSM::onMessage, GSM::priority::normal}); }, 5000); keys.push_back({"RING", &GSM::onRinging}); keys.push_back({"+CMTI:", &GSM::onMessage}); diff --git a/lib/gsm/src/gsm.hpp b/lib/gsm/src/gsm.hpp index 8b2eb916..991eb1c8 100644 --- a/lib/gsm/src/gsm.hpp +++ b/lib/gsm/src/gsm.hpp @@ -6,6 +6,8 @@ #include #include +#define BAUDRATE 921600 + extern const char *daysOfWeek[7]; extern const char *daysOfMonth[12]; @@ -102,8 +104,13 @@ namespace GSM double getBatteryLevel(); void getHour(); + std::string getHttpMMS(std::string url); + int getNetworkStatus(); + bool isFlightMode(); + void setFlightMode(bool mode); + std::string getCurrentTimestamp(); std::string getCurrentTimestampNoSpaces(); void clearFrom(const std::string &from, const std::string &to); diff --git a/lib/gsm/src/pdu.cpp b/lib/gsm/src/pdu.cpp new file mode 100644 index 00000000..42df57a8 --- /dev/null +++ b/lib/gsm/src/pdu.cpp @@ -0,0 +1,219 @@ +#include "pdu.hpp" + +#include +#include +#include +#include +#include +#include +#include + +std::string convert_semi_octet(const std::string& s) { + std::string o; + for (uint32_t i = 0; i < s.size(); i+=2) + { + o += s[i+1]; + o += s[i]; + } + + return o; +} + +std::string decodeGSM7bit(const std::string& encoded) { + std::string decoded; + int length = encoded.length() / 2; + int bitOffset = 0; + + for (int i = 0; i < length; ++i) { + int byteIndex = (i * 7) / 8; + int shift = bitOffset % 8; + + uint8_t currentByte = std::stoi(encoded.substr(byteIndex * 2, 2), nullptr, 16); + uint8_t nextByte = (byteIndex + 1 < encoded.length() / 2) ? std::stoi(encoded.substr((byteIndex + 1) * 2, 2), nullptr, 16) : 0; + + uint8_t septet = ((currentByte >> shift) | (nextByte << (8 - shift))) & 0x7F; + + decoded += static_cast(septet); + bitOffset += 7; + } + + return decoded; +} + +int hex_to_int(const std::string& s) { + int result = 0; + for (char c : s) { + result *= 16; + if (c >= '0' && c <= '9') { + result += c - '0'; + } else if (c >= 'A' && c <= 'F') { + result += c - 'A' + 10; + } else if (c >= 'a' && c <= 'f') { + result += c - 'a' + 10; + } + } + return result; +} + +int hexCharToInt(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + throw std::invalid_argument("Invalid hex character"); +} + +// Function to convert a Latin-1 encoded hexadecimal string to a UTF-8 string +std::string latin1HexToUtf8(const std::string& hexString) { + std::ostringstream utf8Stream; + + for (size_t i = 0; i < hexString.length(); i += 2) { + // Convert the two hex characters to a byte + char byte = static_cast((hexCharToInt(hexString[i]) << 4) | hexCharToInt(hexString[i+1])); + + // Convert the byte from Latin-1 to UTF-8 + unsigned char ubyte = static_cast(byte); + if (ubyte < 0x80) { + // 1-byte UTF-8 (ASCII) + utf8Stream << byte; + } else { + // 2-byte UTF-8 + utf8Stream << static_cast(0xC0 | (ubyte >> 6)); + utf8Stream << static_cast(0x80 | (ubyte & 0x3F)); + } + } + + return utf8Stream.str(); +} + +std::string hex_to_text(const std::string& s) { + std::string result; + for (size_t i = 0; i < s.length(); i += 2) { + char c = static_cast(hex_to_int(s.substr(i, 2))); + if (c >= 32 && c < 127) { + result += c; + } else { + result += '~'; + } + } + return result; +} + +bool getBit(uint8_t byte, uint8_t bit) { + return (byte & (1 << bit)) != 0; +} + +PDU decodePDU(std::string pdu) +{ + std::string number; + std::string text; + std::string url; + + int i = 0; // index in pdu + + int SMSC_length = hex_to_int(pdu.substr(i, 2)) - 2; i += 2; + int SMSC_number_type = hex_to_int(pdu.substr(i, 2)); i += 2; + std::string SMSC = convert_semi_octet(pdu.substr(i, SMSC_length*2)); i += SMSC_length*2; + i += 2; // skip F0 + + // std::cout << "SMSC_length: " << SMSC_length << std::endl; + // std::cout << "SMSC_number_type: " << SMSC_number_type << std::endl; + // std::cout << "SMSC: " << SMSC << std::endl; + + // std::cout << "PDU_mode: " << pdu.substr(i, 2) << std::endl; + char PDU_mode = hex_to_int(pdu.substr(i, 2)) - 2; i += 2; + + // std::cout << "PDU_mode: " << int(PDU_mode) << " " << std::bitset<8>(PDU_mode).to_string() << std::endl; + + /*if(getBit(PDU_mode, 7)) + std::cout << "TP-RP enabled" << std::endl; + + if(getBit(PDU_mode, 6)) + std::cout << "TP-UDHI enabled" << std::endl; + + if(getBit(PDU_mode, 5)) + std::cout << "TP-SRI enabled" << std::endl; + + if(getBit(PDU_mode, 2)) + std::cout << "TP-MMS enabled" << std::endl;*/ + + + // std::cout << "TP-MTI: " << (int) getBit(PDU_mode, 0) << (int) getBit(PDU_mode, 1) << std::endl; + + + int Adress_length = hex_to_int(pdu.substr(i, 2)) + 1; i += 2; + int Adress_number_type = hex_to_int(pdu.substr(i, 2)); i += 2; + std::string Adress = convert_semi_octet(pdu.substr(i, Adress_length)); i += Adress_length; + Adress = Adress.substr(0, Adress.find('F')); + + // std::cout << "Adress_length: " << Adress_length << std::endl; + // std::cout << "Adress_number_type: " << Adress_number_type << std::endl; + // std::cout << "Adress: " << Adress << std::endl; + + char PID = hex_to_int(pdu.substr(i, 2)); i += 2; + char DSC = hex_to_int(pdu.substr(i, 2)); i += 2; + + PDU_type mode = UNKNOWN; + bool is_unicode = false; + + if(getBit(DSC, 3) == 0 && getBit(DSC, 2) == 0) + { + // std::cout << "SMS mode" << std::endl; + mode = SMS; + } + else if(getBit(DSC, 3) == 0 && getBit(DSC, 2) == 1) + { + // std::cout << "MMS mode" << std::endl; + mode = MMS; + } + else if (getBit(DSC, 3) == 1 && getBit(DSC, 2) == 0) + { + is_unicode = true; + mode = SMS; + // std::cout << "SMS mode with unicode" << std::endl; + } + else + { + // std::cout << "Unknown mode" << std::endl; + } + // std::cout << "DSC: " << (int) getBit(DSC, 3) << (int) getBit(DSC, 2) << " " << std::bitset<8>(DSC).to_string() << std::endl; + + i+= 7*2; // timestamp + + int Message_length = hex_to_int(pdu.substr(i, 2)) + 1; i += 2; + std::string Message = pdu.substr(i, Message_length*2); i += Message_length*2; + + if(mode == SMS) + { + number = "+" + Adress; + + if(is_unicode) + { + for(int j = 0; j < Message.length(); j+=2) + { + if(Message.substr(j, 2) == "00") + { + Message = Message.substr(0, j) + Message.substr(j+2); + } + } + + //std::cout << "Message unicode: " << Message << std::endl; + + text = latin1HexToUtf8(Message); + } + else + text = decodeGSM7bit(Message); + } + else + { + Message = hex_to_text(Message); + + number = Message.substr(Message.find("+"), Message.find("/") - Message.find("+")); + url = Message.substr(Message.find("http"), Message.find("~", Message.find("http")) - Message.find("http")); + } + + std::cout << "Number: " << number << std::endl; + std::cout << "Text: " << text << std::endl; + std::cout << "URL: " << url << std::endl; + + return {number, text, url, mode}; +} \ No newline at end of file diff --git a/lib/gsm/src/pdu.hpp b/lib/gsm/src/pdu.hpp new file mode 100644 index 00000000..31ddb7d8 --- /dev/null +++ b/lib/gsm/src/pdu.hpp @@ -0,0 +1,23 @@ +#ifndef PDU_DECODER_HPP +#define PDU_DECODER_HPP + +#include + +enum PDU_type +{ + SMS, + MMS, + UNKNOWN +}; + +struct PDU +{ + std::string sender; + std::string message; + std::string url; + PDU_type type; +}; + +PDU decodePDU(std::string pdu); + +#endif \ No newline at end of file diff --git a/lib/gui/src/ElementBase.cpp b/lib/gui/src/ElementBase.cpp index 37fb1652..bdf95a9a 100644 --- a/lib/gui/src/ElementBase.cpp +++ b/lib/gui/src/ElementBase.cpp @@ -110,6 +110,9 @@ void gui::ElementBase::renderAll(bool onScreen) bool gui::ElementBase::updateAll() { + if(!m_isEnabled) + return false; + if(!isInside()) { if(m_surface != nullptr) @@ -189,6 +192,9 @@ bool gui::ElementBase::update() { // algorithme de mise a jour des interactions tactiles + if(!this->m_isEnabled) + return false; + widgetUpdate(); if (!m_hasEvents && widgetPressed != this) diff --git a/lib/gui/src/elements/List.cpp b/lib/gui/src/elements/List.cpp index 767c6e3b..e22bb2de 100644 --- a/lib/gui/src/elements/List.cpp +++ b/lib/gui/src/elements/List.cpp @@ -13,6 +13,7 @@ namespace gui::elements m_lineSpace = 25; m_verticalScrollEnabled = true; m_hasEvents = true; + m_selectionFocus == SelectionFocus::UP; } VerticalList::~VerticalList() = default; @@ -22,6 +23,14 @@ namespace gui::elements m_surface->fillRect(0, 0, m_width, m_height, COLOR_WHITE); } + void VerticalList::postRender() + { + if(m_selectionFocus == SelectionFocus::CENTER && m_children.size()) + { + m_surface->fillRect(0, getHeight()/2 - m_children[m_focusedIndex]->getHeight()/2, 1, m_children[m_focusedIndex]->getHeight(), COLOR_BLACK); + } + } + void VerticalList::add(ElementBase* widget) { m_verticalScrollEnabled = true; @@ -46,6 +55,7 @@ namespace gui::elements void VerticalList::updateFocusedIndex() { eventHandlerApp.setTimeout(new Callback<>([&](){ + std::cout << "updateFocusedIndex" << std::endl; if(m_children.size() == 0) { m_focusedIndex = 0; @@ -53,7 +63,11 @@ namespace gui::elements } m_verticalScroll = m_children[m_focusedIndex]->m_y; + if(m_selectionFocus == SelectionFocus::CENTER) + m_verticalScroll = m_verticalScroll - getHeight() / 2 + m_children[m_focusedIndex]->getHeight() / 2; + localGraphicalUpdate(); + std::cout << "updateFocusedIndex end: " << m_selectionFocus << std::endl; // for (int i = 0; i < m_children.size(); i++) // { @@ -82,6 +96,17 @@ namespace gui::elements updateFocusedIndex(); } } + + void VerticalList::setSelectionFocus(SelectionFocus focus) + { + m_selectionFocus = focus; + updateFocusedIndex(); + } + + int VerticalList::getFocusedElement() + { + return m_focusedIndex; + } HorizontalList::HorizontalList(uint16_t x, uint16_t y, uint16_t width, uint16_t height) diff --git a/lib/gui/src/elements/List.hpp b/lib/gui/src/elements/List.hpp index 69021214..664e3ccc 100644 --- a/lib/gui/src/elements/List.hpp +++ b/lib/gui/src/elements/List.hpp @@ -12,6 +12,7 @@ namespace gui::elements ~VerticalList() override; void render() override; + void postRender(); void add(ElementBase* widget); void setIndex(int index); @@ -22,9 +23,19 @@ namespace gui::elements void onScrollUp(); void onScrollDown(); + enum SelectionFocus + { + UP, + CENTER + }; + + void setSelectionFocus(SelectionFocus focus); + int getFocusedElement(); + private: int16_t m_focusedIndex = 0; uint16_t m_lineSpace = 0; + SelectionFocus m_selectionFocus = SelectionFocus::UP; }; class HorizontalList final : public ElementBase diff --git a/lib/imgdec/src/imgdec.cpp b/lib/imgdec/src/imgdec.cpp index 95d64dd1..c12bd9b4 100644 --- a/lib/imgdec/src/imgdec.cpp +++ b/lib/imgdec/src/imgdec.cpp @@ -4,6 +4,12 @@ #include "imgdec.hpp" +#include +#include + +#define TJE_IMPLEMENTATION +#include "toojpeg.h" + #include // Create a uint16 with 2x uint8 (Little endian) @@ -48,33 +54,66 @@ imgdec::IMGData imgdec::decodeHeader(const uint8_t* rawData) imgData.width = make32B(rawData[0x10], rawData[0x11], rawData[0x12], rawData[0x13]); imgData.heigth = make32B(rawData[0x14], rawData[0x15], rawData[0x16], rawData[0x17]); } - else if (rawData[0x00] == 0xFF && rawData[0x01] == 0xD8 && rawData[0x02] == 0xFF) + else { imgData.type = JPG; - // JPG is such a weird image format, we need to find the address of the width and height + // Look for the SOF0 marker (FF C0) which indicates the start of frame int i = 0; - while (rawData[i] != 0xC0) // Only SOF0 marker, may cause issues + while (i < 2000) // A reasonable limit for searching within the file { + if (rawData[i] == 0xFF && rawData[i + 1] == 0xC0) + { + // SOF0 marker found + imgData.heigth = make16B(rawData[i + 5], rawData[i + 6]); + imgData.width = make16B(rawData[i + 7], rawData[i + 8]); + break; + } i++; } - // Skip SOF0 marker - i++; - - // Skip length - i += 2; - - // Skip bitsPerSample - i++; - - imgData.width = make16B(rawData[i], rawData[i + 1]); - imgData.heigth = make16B(rawData[i + 2], rawData[i + 3]); + // If we reach the end of the loop without finding the SOF0 marker, it's an error + if (i >= 2000) + { + std::cerr << "Invalid JPEG file: SOF0 marker not found." << std::endl; + } } - else + /*else { imgData.type = ERROR; // Unknown image - } + }*/ return imgData; } + +std::ofstream myFile; + +// write a single byte compressed by tooJpeg +void myOutput(unsigned char byte) +{ + myFile << byte; +} + +void imgdec::encodeJpg(const uint8_t *rawData, uint32_t width, uint32_t height, storage::Path filename) +{ + std::string path = filename.str(); + + myFile.open(path, std::ios::binary); + + // RGB: one byte each for red, green, blue + const auto bytesPerPixel = 3; + + // start JPEG compression + // note: myOutput is the function defined in line 18, it saves the output in example.jpg + // optional parameters: + const bool isRGB = true; // true = RGB image, else false = grayscale + const auto quality = 90; // compression quality: 0 = worst, 100 = best, 80 to 90 are most often used + const bool downsample = false; // false = save as YCbCr444 JPEG (better quality), true = YCbCr420 (smaller file) + const char *comment = "mms"; // arbitrary JPEG comment + + std::cout << "ready to write: " << path << std::endl; + + auto ok = TooJpeg::writeJpeg(myOutput, rawData, width, height, isRGB, quality, downsample, comment); + + myFile.close(); +} \ No newline at end of file diff --git a/lib/imgdec/src/imgdec.hpp b/lib/imgdec/src/imgdec.hpp index 187642fd..1707af1b 100644 --- a/lib/imgdec/src/imgdec.hpp +++ b/lib/imgdec/src/imgdec.hpp @@ -6,6 +6,7 @@ #define IMGDEC_HPP #include +#include namespace imgdec { @@ -27,6 +28,8 @@ namespace imgdec }; IMGData decodeHeader(const uint8_t *rawData); + + void encodeJpg(const uint8_t *rawData, uint32_t width, uint32_t heigth, storage::Path filename); } #endif //IMGDEC_HPP diff --git a/lib/imgdec/src/toojpeg.cpp b/lib/imgdec/src/toojpeg.cpp new file mode 100644 index 00000000..b17b583b --- /dev/null +++ b/lib/imgdec/src/toojpeg.cpp @@ -0,0 +1,665 @@ +// ////////////////////////////////////////////////////////// +// toojpeg.cpp +// written by Stephan Brumme, 2018-2019 +// see https://create.stephan-brumme.com/toojpeg/ +// + +#include "toojpeg.h" + +// - the "official" specifications: https://www.w3.org/Graphics/JPEG/itu-t81.pdf and https://www.w3.org/Graphics/JPEG/jfif3.pdf +// - Wikipedia has a short description of the JFIF/JPEG file format: https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format +// - the popular STB Image library includes Jon's JPEG encoder as well: https://github.com/nothings/stb/blob/master/stb_image_write.h +// - the most readable JPEG book (from a developer's perspective) is Miano's "Compressed Image File Formats" (1999, ISBN 0-201-60443-4), +// used copies are really cheap nowadays and include a CD with C++ sources as well (plus great format descriptions of GIF & PNG) +// - much more detailled is Mitchell/Pennebaker's "JPEG: Still Image Data Compression Standard" (1993, ISBN 0-442-01272-1) +// which contains the official JPEG standard, too - fun fact: I bought a signed copy in a second-hand store without noticing + +namespace // anonymous namespace to hide local functions / constants / etc. +{ +// //////////////////////////////////////// +// data types +using uint8_t = unsigned char; +using uint16_t = unsigned short; +using int16_t = short; +using int32_t = int; // at least four bytes + +// //////////////////////////////////////// +// constants + +// quantization tables from JPEG Standard, Annex K +const uint8_t DefaultQuantLuminance[8*8] = + { 16, 11, 10, 16, 24, 40, 51, 61, // there are a few experts proposing slightly more efficient values, + 12, 12, 14, 19, 26, 58, 60, 55, // e.g. https://www.imagemagick.org/discourse-server/viewtopic.php?t=20333 + 14, 13, 16, 24, 40, 57, 69, 56, // btw: Google's Guetzli project optimizes the quantization tables per image + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 }; +const uint8_t DefaultQuantChrominance[8*8] = + { 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 }; + +// 8x8 blocks are processed in zig-zag order +// most encoders use a zig-zag "forward" table, I switched to its inverse for performance reasons +// note: ZigZagInv[ZigZag[i]] = i +const uint8_t ZigZagInv[8*8] = + { 0, 1, 8,16, 9, 2, 3,10, // ZigZag[] = 0, 1, 5, 6,14,15,27,28, + 17,24,32,25,18,11, 4, 5, // 2, 4, 7,13,16,26,29,42, + 12,19,26,33,40,48,41,34, // 3, 8,12,17,25,30,41,43, + 27,20,13, 6, 7,14,21,28, // 9,11,18,24,31,40,44,53, + 35,42,49,56,57,50,43,36, // 10,19,23,32,39,45,52,54, + 29,22,15,23,30,37,44,51, // 20,22,33,38,46,51,55,60, + 58,59,52,45,38,31,39,46, // 21,34,37,47,50,56,59,61, + 53,60,61,54,47,55,62,63 }; // 35,36,48,49,57,58,62,63 + +// static Huffman code tables from JPEG standard Annex K +// - CodesPerBitsize tables define how many Huffman codes will have a certain bitsize (plus 1 because there nothing with zero bits), +// e.g. DcLuminanceCodesPerBitsize[2] = 5 because there are 5 Huffman codes being 2+1=3 bits long +// - Values tables are a list of values ordered by their Huffman code bitsize, +// e.g. AcLuminanceValues => Huffman(0x01,0x02 and 0x03) will have 2 bits, Huffman(0x00) will have 3 bits, Huffman(0x04,0x11 and 0x05) will have 4 bits, ... + +// Huffman definitions for first DC/AC tables (luminance / Y channel) +const uint8_t DcLuminanceCodesPerBitsize[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; // sum = 12 +const uint8_t DcLuminanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes +const uint8_t AcLuminanceCodesPerBitsize[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 }; // sum = 162 +const uint8_t AcLuminanceValues [162] = // => 162 codes + { 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, // 16*10+2 symbols because + 0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, // upper 4 bits can be 0..F + 0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, // while lower 4 bits can be 1..A + 0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, // plus two special codes 0x00 and 0xF0 + 0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, // order of these symbols was determined empirically by JPEG committee + 0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2, + 0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA }; +// Huffman definitions for second DC/AC tables (chrominance / Cb and Cr channels) +const uint8_t DcChrominanceCodesPerBitsize[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; // sum = 12 +const uint8_t DcChrominanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes (identical to DcLuminanceValues) +const uint8_t AcChrominanceCodesPerBitsize[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 }; // sum = 162 +const uint8_t AcChrominanceValues [162] = // => 162 codes + { 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, // same number of symbol, just different order + 0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, // (which is more efficient for AC coding) + 0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4, + 0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA, + 0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA }; +const int16_t CodeWordLimit = 2048; // +/-2^11, maximum value after DCT + +// //////////////////////////////////////// +// structs + +// represent a single Huffman code +struct BitCode +{ + BitCode() = default; // undefined state, must be initialized at a later time + BitCode(uint16_t code_, uint8_t numBits_) + : code(code_), numBits(numBits_) {} + uint16_t code; // JPEG's Huffman codes are limited to 16 bits + uint8_t numBits; // number of valid bits +}; + +// wrapper for bit output operations +struct BitWriter +{ + // user-supplied callback that writes/stores one byte + TooJpeg::WRITE_ONE_BYTE output; + // initialize writer + explicit BitWriter(TooJpeg::WRITE_ONE_BYTE output_) : output(output_) {} + + // store the most recently encoded bits that are not written yet + struct BitBuffer + { + int32_t data = 0; // actually only at most 24 bits are used + uint8_t numBits = 0; // number of valid bits (the right-most bits) + } buffer; + + // write Huffman bits stored in BitCode, keep excess bits in BitBuffer + BitWriter& operator<<(const BitCode& data) + { + // append the new bits to those bits leftover from previous call(s) + buffer.numBits += data.numBits; + buffer.data <<= data.numBits; + buffer.data |= data.code; + + // write all "full" bytes + while (buffer.numBits >= 8) + { + // extract highest 8 bits + buffer.numBits -= 8; + auto oneByte = uint8_t(buffer.data >> buffer.numBits); + output(oneByte); + + if (oneByte == 0xFF) // 0xFF has a special meaning for JPEGs (it's a block marker) + output(0); // therefore pad a zero to indicate "nope, this one ain't a marker, it's just a coincidence" + + // note: I don't clear those written bits, therefore buffer.bits may contain garbage in the high bits + // if you really want to "clean up" (e.g. for debugging purposes) then uncomment the following line + //buffer.bits &= (1 << buffer.numBits) - 1; + } + return *this; + } + + // write all non-yet-written bits, fill gaps with 1s (that's a strange JPEG thing) + void flush() + { + // at most seven set bits needed to "fill" the last byte: 0x7F = binary 0111 1111 + *this << BitCode(0x7F, 7); // I should set buffer.numBits = 0 but since there are no single bits written after flush() I can safely ignore it + } + + // NOTE: all the following BitWriter functions IGNORE the BitBuffer and write straight to output ! + // write a single byte + BitWriter& operator<<(uint8_t oneByte) + { + output(oneByte); + return *this; + } + + // write an array of bytes + template + BitWriter& operator<<(T (&manyBytes)[Size]) + { + for (auto c : manyBytes) + output(c); + return *this; + } + + // start a new JFIF block + void addMarker(uint8_t id, uint16_t length) + { + output(0xFF); output(id); // ID, always preceded by 0xFF + output(uint8_t(length >> 8)); // length of the block (big-endian, includes the 2 length bytes as well) + output(uint8_t(length & 0xFF)); + } +}; + +// //////////////////////////////////////// +// functions / templates + +// same as std::min() +template +Number minimum(Number value, Number maximum) +{ + return value <= maximum ? value : maximum; +} + +// restrict a value to the interval [minimum, maximum] +template +Number clamp(Number value, Limit minValue, Limit maxValue) +{ + if (value <= minValue) return minValue; // never smaller than the minimum + if (value >= maxValue) return maxValue; // never bigger than the maximum + return value; // value was inside interval, keep it +} + +// convert from RGB to YCbCr, constants are similar to ITU-R, see https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion +float rgb2y (float r, float g, float b) { return +0.299f * r +0.587f * g +0.114f * b; } +float rgb2cb(float r, float g, float b) { return -0.16874f * r -0.33126f * g +0.5f * b; } +float rgb2cr(float r, float g, float b) { return +0.5f * r -0.41869f * g -0.08131f * b; } + +// forward DCT computation "in one dimension" (fast AAN algorithm by Arai, Agui and Nakajima: "A fast DCT-SQ scheme for images") +void DCT(float block[8*8], uint8_t stride) // stride must be 1 (=horizontal) or 8 (=vertical) +{ + const auto SqrtHalfSqrt = 1.306562965f; // sqrt((2 + sqrt(2)) / 2) = cos(pi * 1 / 8) * sqrt(2) + const auto InvSqrt = 0.707106781f; // 1 / sqrt(2) = cos(pi * 2 / 8) + const auto HalfSqrtSqrt = 0.382683432f; // sqrt(2 - sqrt(2)) / 2 = cos(pi * 3 / 8) + const auto InvSqrtSqrt = 0.541196100f; // 1 / sqrt(2 - sqrt(2)) = cos(pi * 3 / 8) * sqrt(2) + + // modify in-place + auto& block0 = block[0 ]; + auto& block1 = block[1 * stride]; + auto& block2 = block[2 * stride]; + auto& block3 = block[3 * stride]; + auto& block4 = block[4 * stride]; + auto& block5 = block[5 * stride]; + auto& block6 = block[6 * stride]; + auto& block7 = block[7 * stride]; + + // based on https://dev.w3.org/Amaya/libjpeg/jfdctflt.c , the original variable names can be found in my comments + auto add07 = block0 + block7; auto sub07 = block0 - block7; // tmp0, tmp7 + auto add16 = block1 + block6; auto sub16 = block1 - block6; // tmp1, tmp6 + auto add25 = block2 + block5; auto sub25 = block2 - block5; // tmp2, tmp5 + auto add34 = block3 + block4; auto sub34 = block3 - block4; // tmp3, tmp4 + + auto add0347 = add07 + add34; auto sub07_34 = add07 - add34; // tmp10, tmp13 ("even part" / "phase 2") + auto add1256 = add16 + add25; auto sub16_25 = add16 - add25; // tmp11, tmp12 + + block0 = add0347 + add1256; block4 = add0347 - add1256; // "phase 3" + + auto z1 = (sub16_25 + sub07_34) * InvSqrt; // all temporary z-variables kept their original names + block2 = sub07_34 + z1; block6 = sub07_34 - z1; // "phase 5" + + auto sub23_45 = sub25 + sub34; // tmp10 ("odd part" / "phase 2") + auto sub12_56 = sub16 + sub25; // tmp11 + auto sub01_67 = sub16 + sub07; // tmp12 + + auto z5 = (sub23_45 - sub01_67) * HalfSqrtSqrt; + auto z2 = sub23_45 * InvSqrtSqrt + z5; + auto z3 = sub12_56 * InvSqrt; + auto z4 = sub01_67 * SqrtHalfSqrt + z5; + auto z6 = sub07 + z3; // z11 ("phase 5") + auto z7 = sub07 - z3; // z13 + block1 = z6 + z4; block7 = z6 - z4; // "phase 6" + block5 = z7 + z2; block3 = z7 - z2; +} + +// run DCT, quantize and write Huffman bit codes +int16_t encodeBlock(BitWriter& writer, float block[8][8], const float scaled[8*8], int16_t lastDC, + const BitCode huffmanDC[256], const BitCode huffmanAC[256], const BitCode* codewords) +{ + // "linearize" the 8x8 block, treat it as a flat array of 64 floats + auto block64 = (float*) block; + + // DCT: rows + for (auto offset = 0; offset < 8; offset++) + DCT(block64 + offset*8, 1); + // DCT: columns + for (auto offset = 0; offset < 8; offset++) + DCT(block64 + offset*1, 8); + + // scale + for (auto i = 0; i < 8*8; i++) + block64[i] *= scaled[i]; + + // encode DC (the first coefficient is the "average color" of the 8x8 block) + auto DC = int(block64[0] + (block64[0] >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect + + // quantize and zigzag the other 63 coefficients + auto posNonZero = 0; // find last coefficient which is not zero (because trailing zeros are encoded differently) + int16_t quantized[8*8]; + for (auto i = 1; i < 8*8; i++) // start at 1 because block64[0]=DC was already processed + { + auto value = block64[ZigZagInv[i]]; + // round to nearest integer + quantized[i] = int(value + (value >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect + // remember offset of last non-zero coefficient + if (quantized[i] != 0) + posNonZero = i; + } + + // same "average color" as previous block ? + auto diff = DC - lastDC; + if (diff == 0) + writer << huffmanDC[0x00]; // yes, write a special short symbol + else + { + auto bits = codewords[diff]; // nope, encode the difference to previous block's average color + writer << huffmanDC[bits.numBits] << bits; + } + + // encode ACs (quantized[1..63]) + auto offset = 0; // upper 4 bits count the number of consecutive zeros + for (auto i = 1; i <= posNonZero; i++) // quantized[0] was already written, skip all trailing zeros, too + { + // zeros are encoded in a special way + while (quantized[i] == 0) // found another zero ? + { + offset += 0x10; // add 1 to the upper 4 bits + // split into blocks of at most 16 consecutive zeros + if (offset > 0xF0) // remember, the counter is in the upper 4 bits, 0xF = 15 + { + writer << huffmanAC[0xF0]; // 0xF0 is a special code for "16 zeros" + offset = 0; + } + i++; + } + + auto encoded = codewords[quantized[i]]; + // combine number of zeros with the number of bits of the next non-zero value + writer << huffmanAC[offset + encoded.numBits] << encoded; // and the value itself + offset = 0; + } + + // send end-of-block code (0x00), only needed if there are trailing zeros + if (posNonZero < 8*8 - 1) // = 63 + writer << huffmanAC[0x00]; + + return DC; +} + +// Jon's code includes the pre-generated Huffman codes +// I don't like these "magic constants" and compute them on my own :-) +void generateHuffmanTable(const uint8_t numCodes[16], const uint8_t* values, BitCode result[256]) +{ + // process all bitsizes 1 thru 16, no JPEG Huffman code is allowed to exceed 16 bits + auto huffmanCode = 0; + for (auto numBits = 1; numBits <= 16; numBits++) + { + // ... and each code of these bitsizes + for (auto i = 0; i < numCodes[numBits - 1]; i++) // note: numCodes array starts at zero, but smallest bitsize is 1 + result[*values++] = BitCode(huffmanCode++, numBits); + + // next Huffman code needs to be one bit wider + huffmanCode <<= 1; + } +} + +} // end of anonymous namespace + +// -------------------- externally visible code -------------------- + +namespace TooJpeg +{ +// the only exported function ... +bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels_, unsigned short width, unsigned short height, + bool isRGB, unsigned char quality_, bool downsample, const char* comment) +{ + // reject invalid pointers + if (output == nullptr || pixels_ == nullptr) + return false; + // check image format + if (width == 0 || height == 0) + return false; + + // number of components + const auto numComponents = isRGB ? 3 : 1; + // note: if there is just one component (=grayscale), then only luminance needs to be stored in the file + // thus everything related to chrominance need not to be written to the JPEG + // I still compute a few things, like quantization tables to avoid a complete code mess + + // grayscale images can't be downsampled (because there are no Cb + Cr channels) + if (!isRGB) + downsample = false; + + // wrapper for all output operations + BitWriter bitWriter(output); + + // //////////////////////////////////////// + // JFIF headers + const uint8_t HeaderJfif[2+2+16] = + { 0xFF,0xD8, // SOI marker (start of image) + 0xFF,0xE0, // JFIF APP0 tag + 0,16, // length: 16 bytes (14 bytes payload + 2 bytes for this length field) + 'J','F','I','F',0, // JFIF identifier, zero-terminated + 1,1, // JFIF version 1.1 + 0, // no density units specified + 0,1,0,1, // density: 1 pixel "per pixel" horizontally and vertically + 0,0 }; // no thumbnail (size 0 x 0) + bitWriter << HeaderJfif; + + // //////////////////////////////////////// + // comment (optional) + if (comment != nullptr) + { + // look for zero terminator + auto length = 0; // = strlen(comment); + while (comment[length] != 0) + length++; + + // write COM marker + bitWriter.addMarker(0xFE, 2+length); // block size is number of bytes (without zero terminator) + 2 bytes for this length field + // ... and write the comment itself + for (auto i = 0; i < length; i++) + bitWriter << comment[i]; + } + + // //////////////////////////////////////// + // adjust quantization tables to desired quality + + // quality level must be in 1 ... 100 + auto quality = clamp(quality_, 1, 100); + // convert to an internal JPEG quality factor, formula taken from libjpeg + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + + uint8_t quantLuminance [8*8]; + uint8_t quantChrominance[8*8]; + for (auto i = 0; i < 8*8; i++) + { + int luminance = (DefaultQuantLuminance [ZigZagInv[i]] * quality + 50) / 100; + int chrominance = (DefaultQuantChrominance[ZigZagInv[i]] * quality + 50) / 100; + + // clamp to 1..255 + quantLuminance [i] = clamp(luminance, 1, 255); + quantChrominance[i] = clamp(chrominance, 1, 255); + } + + // write quantization tables + bitWriter.addMarker(0xDB, 2 + (isRGB ? 2 : 1) * (1 + 8*8)); // length: 65 bytes per table + 2 bytes for this length field + // each table has 64 entries and is preceded by an ID byte + + bitWriter << 0x00 << quantLuminance; // first quantization table + if (isRGB) + bitWriter << 0x01 << quantChrominance; // second quantization table, only relevant for color images + + // //////////////////////////////////////// + // write image infos (SOF0 - start of frame) + bitWriter.addMarker(0xC0, 2+6+3*numComponents); // length: 6 bytes general info + 3 per channel + 2 bytes for this length field + + // 8 bits per channel + bitWriter << 0x08 + // image dimensions (big-endian) + << (height >> 8) << (height & 0xFF) + << (width >> 8) << (width & 0xFF); + + // sampling and quantization tables for each component + bitWriter << numComponents; // 1 component (grayscale, Y only) or 3 components (Y,Cb,Cr) + for (auto id = 1; id <= numComponents; id++) + bitWriter << id // component ID (Y=1, Cb=2, Cr=3) + // bitmasks for sampling: highest 4 bits: horizontal, lowest 4 bits: vertical + << (id == 1 && downsample ? 0x22 : 0x11) // 0x11 is default YCbCr 4:4:4 and 0x22 stands for YCbCr 4:2:0 + << (id == 1 ? 0 : 1); // use quantization table 0 for Y, table 1 for Cb and Cr + + // //////////////////////////////////////// + // Huffman tables + // DHT marker - define Huffman tables + bitWriter.addMarker(0xC4, isRGB ? (2+208+208) : (2+208)); + // 2 bytes for the length field, store chrominance only if needed + // 1+16+12 for the DC luminance + // 1+16+162 for the AC luminance (208 = 1+16+12 + 1+16+162) + // 1+16+12 for the DC chrominance + // 1+16+162 for the AC chrominance (208 = 1+16+12 + 1+16+162, same as above) + + // store luminance's DC+AC Huffman table definitions + bitWriter << 0x00 // highest 4 bits: 0 => DC, lowest 4 bits: 0 => Y (baseline) + << DcLuminanceCodesPerBitsize + << DcLuminanceValues; + bitWriter << 0x10 // highest 4 bits: 1 => AC, lowest 4 bits: 0 => Y (baseline) + << AcLuminanceCodesPerBitsize + << AcLuminanceValues; + + // compute actual Huffman code tables (see Jon's code for precalculated tables) + BitCode huffmanLuminanceDC[256]; + BitCode huffmanLuminanceAC[256]; + generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC); + generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC); + + // chrominance is only relevant for color images + BitCode huffmanChrominanceDC[256]; + BitCode huffmanChrominanceAC[256]; + if (isRGB) + { + // store luminance's DC+AC Huffman table definitions + bitWriter << 0x01 // highest 4 bits: 0 => DC, lowest 4 bits: 1 => Cr,Cb (baseline) + << DcChrominanceCodesPerBitsize + << DcChrominanceValues; + bitWriter << 0x11 // highest 4 bits: 1 => AC, lowest 4 bits: 1 => Cr,Cb (baseline) + << AcChrominanceCodesPerBitsize + << AcChrominanceValues; + + // compute actual Huffman code tables (see Jon's code for precalculated tables) + generateHuffmanTable(DcChrominanceCodesPerBitsize, DcChrominanceValues, huffmanChrominanceDC); + generateHuffmanTable(AcChrominanceCodesPerBitsize, AcChrominanceValues, huffmanChrominanceAC); + } + + // //////////////////////////////////////// + // start of scan (there is only a single scan for baseline JPEGs) + bitWriter.addMarker(0xDA, 2+1+2*numComponents+3); // 2 bytes for the length field, 1 byte for number of components, + // then 2 bytes for each component and 3 bytes for spectral selection + + // assign Huffman tables to each component + bitWriter << numComponents; + for (auto id = 1; id <= numComponents; id++) + // highest 4 bits: DC Huffman table, lowest 4 bits: AC Huffman table + bitWriter << id << (id == 1 ? 0x00 : 0x11); // Y: tables 0 for DC and AC; Cb + Cr: tables 1 for DC and AC + + // constant values for our baseline JPEGs (which have a single sequential scan) + static const uint8_t Spectral[3] = { 0, 63, 0 }; // spectral selection: must be from 0 to 63; successive approximation must be 0 + bitWriter << Spectral; + + // //////////////////////////////////////// + // adjust quantization tables with AAN scaling factors to simplify DCT + float scaledLuminance [8*8]; + float scaledChrominance[8*8]; + for (auto i = 0; i < 8*8; i++) + { + auto row = ZigZagInv[i] / 8; // same as ZigZagInv[i] >> 3 + auto column = ZigZagInv[i] % 8; // same as ZigZagInv[i] & 7 + + // scaling constants for AAN DCT algorithm: AanScaleFactors[0] = 1, AanScaleFactors[k=1..7] = cos(k*PI/16) * sqrt(2) + static const float AanScaleFactors[8] = { 1, 1.387039845f, 1.306562965f, 1.175875602f, 1, 0.785694958f, 0.541196100f, 0.275899379f }; + auto factor = 1 / (AanScaleFactors[row] * AanScaleFactors[column] * 8); + scaledLuminance [ZigZagInv[i]] = factor / quantLuminance [i]; + scaledChrominance[ZigZagInv[i]] = factor / quantChrominance[i]; + // if you really want JPEGs that are bitwise identical to Jon Olick's code then you need slightly different formulas (note: sqrt(8) = 2.828427125f) + //static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; // line 240 of jo_jpeg.cpp + //scaledLuminance [ZigZagInv[i]] = 1 / (quantLuminance [i] * aasf[row] * aasf[column]); // lines 266-267 of jo_jpeg.cpp + //scaledChrominance[ZigZagInv[i]] = 1 / (quantChrominance[i] * aasf[row] * aasf[column]); + } + + // //////////////////////////////////////// + // precompute JPEG codewords for quantized DCT + BitCode codewordsArray[2 * CodeWordLimit]; // note: quantized[i] is found at codewordsArray[quantized[i] + CodeWordLimit] + BitCode* codewords = &codewordsArray[CodeWordLimit]; // allow negative indices, so quantized[i] is at codewords[quantized[i]] + uint8_t numBits = 1; // each codeword has at least one bit (value == 0 is undefined) + int32_t mask = 1; // mask is always 2^numBits - 1, initial value 2^1-1 = 2-1 = 1 + for (int16_t value = 1; value < CodeWordLimit; value++) + { + // numBits = position of highest set bit (ignoring the sign) + // mask = (2^numBits) - 1 + if (value > mask) // one more bit ? + { + numBits++; + mask = (mask << 1) | 1; // append a set bit + } + codewords[-value] = BitCode(mask - value, numBits); // note that I use a negative index => codewords[-value] = codewordsArray[CodeWordLimit value] + codewords[+value] = BitCode( value, numBits); + } + + // just convert image data from void* + auto pixels = (const uint8_t*)pixels_; + + // the next two variables are frequently used when checking for image borders + const auto maxWidth = width - 1; // "last row" + const auto maxHeight = height - 1; // "bottom line" + + // process MCUs (minimum codes units) => image is subdivided into a grid of 8x8 or 16x16 tiles + const auto sampling = downsample ? 2 : 1; // 1x1 or 2x2 sampling + const auto mcuSize = 8 * sampling; + + // average color of the previous MCU + int16_t lastYDC = 0, lastCbDC = 0, lastCrDC = 0; + // convert from RGB to YCbCr + float Y[8][8], Cb[8][8], Cr[8][8]; + + for (auto mcuY = 0; mcuY < height; mcuY += mcuSize) // each step is either 8 or 16 (=mcuSize) + for (auto mcuX = 0; mcuX < width; mcuX += mcuSize) + { + // YCbCr 4:4:4 format: each MCU is a 8x8 block - the same applies to grayscale images, too + // YCbCr 4:2:0 format: each MCU represents a 16x16 block, stored as 4x 8x8 Y-blocks plus 1x 8x8 Cb and 1x 8x8 Cr block) + for (auto blockY = 0; blockY < mcuSize; blockY += 8) // iterate once (YCbCr444 and grayscale) or twice (YCbCr420) + for (auto blockX = 0; blockX < mcuSize; blockX += 8) + { + // now we finally have an 8x8 block ... + for (auto deltaY = 0; deltaY < 8; deltaY++) + { + auto column = minimum(mcuX + blockX , maxWidth); // must not exceed image borders, replicate last row/column if needed + auto row = minimum(mcuY + blockY + deltaY, maxHeight); + for (auto deltaX = 0; deltaX < 8; deltaX++) + { + // find actual pixel position within the current image + auto pixelPos = row * int(width) + column; // the cast ensures that we don't run into multiplication overflows + if (column < maxWidth) + column++; + + // grayscale images have solely a Y channel which can be easily derived from the input pixel by shifting it by 128 + if (!isRGB) + { + Y[deltaY][deltaX] = pixels[pixelPos] - 128.f; + continue; + } + + // RGB: 3 bytes per pixel (whereas grayscale images have only 1 byte per pixel) + auto r = pixels[3 * pixelPos ]; + auto g = pixels[3 * pixelPos + 1]; + auto b = pixels[3 * pixelPos + 2]; + + Y [deltaY][deltaX] = rgb2y (r, g, b) - 128; // again, the JPEG standard requires Y to be shifted by 128 + // YCbCr444 is easy - the more complex YCbCr420 has to be computed about 20 lines below in a second pass + if (!downsample) + { + Cb[deltaY][deltaX] = rgb2cb(r, g, b); // standard RGB-to-YCbCr conversion + Cr[deltaY][deltaX] = rgb2cr(r, g, b); + } + } + } + + // encode Y channel + lastYDC = encodeBlock(bitWriter, Y, scaledLuminance, lastYDC, huffmanLuminanceDC, huffmanLuminanceAC, codewords); + // Cb and Cr are encoded about 50 lines below + } + + // grayscale images don't need any Cb and Cr information + if (!isRGB) + continue; + + // //////////////////////////////////////// + // the following lines are only relevant for YCbCr420: + // average/downsample chrominance of four pixels while respecting the image borders + if (downsample) + for (short deltaY = 7; downsample && deltaY >= 0; deltaY--) // iterating loop in reverse increases cache read efficiency + { + auto row = minimum(mcuY + 2*deltaY, maxHeight); // each deltaX/Y step covers a 2x2 area + auto column = mcuX; // column is updated inside next loop + auto pixelPos = (row * int(width) + column) * 3; // numComponents = 3 + + // deltas (in bytes) to next row / column, must not exceed image borders + auto rowStep = (row < maxHeight) ? 3 * int(width) : 0; // always numComponents*width except for bottom line + auto columnStep = (column < maxWidth ) ? 3 : 0; // always numComponents except for rightmost pixel + + for (short deltaX = 0; deltaX < 8; deltaX++) + { + // let's add all four samples (2x2 area) + auto right = pixelPos + columnStep; + auto down = pixelPos + rowStep; + auto downRight = pixelPos + columnStep + rowStep; + + // note: cast from 8 bits to >8 bits to avoid overflows when adding + auto r = short(pixels[pixelPos ]) + pixels[right ] + pixels[down ] + pixels[downRight ]; + auto g = short(pixels[pixelPos + 1]) + pixels[right + 1] + pixels[down + 1] + pixels[downRight + 1]; + auto b = short(pixels[pixelPos + 2]) + pixels[right + 2] + pixels[down + 2] + pixels[downRight + 2]; + + // convert to Cb and Cr + Cb[deltaY][deltaX] = rgb2cb(r, g, b) / 4; // I still have to divide r,g,b by 4 to get their average values + Cr[deltaY][deltaX] = rgb2cr(r, g, b) / 4; // it's a bit faster if done AFTER CbCr conversion + + // step forward to next 2x2 area + pixelPos += 2*3; // 2 pixels => 6 bytes (2*numComponents) + column += 2; + + // reached right border ? + if (column >= maxWidth) + { + columnStep = 0; + pixelPos = ((row + 1) * int(width) - 1) * 3; // same as (row * width + maxWidth) * numComponents => current's row last pixel + } + } + } // end of YCbCr420 code for Cb and Cr + + // encode Cb and Cr + lastCbDC = encodeBlock(bitWriter, Cb, scaledChrominance, lastCbDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords); + lastCrDC = encodeBlock(bitWriter, Cr, scaledChrominance, lastCrDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords); + } + + bitWriter.flush(); // now image is completely encoded, write any bits still left in the buffer + + // /////////////////////////// + // EOI marker + bitWriter << 0xFF << 0xD9; // this marker has no length, therefore I can't use addMarker() + return true; +} // writeJpeg() +} // namespace TooJpeg \ No newline at end of file diff --git a/lib/imgdec/src/toojpeg.h b/lib/imgdec/src/toojpeg.h new file mode 100644 index 00000000..e709db0b --- /dev/null +++ b/lib/imgdec/src/toojpeg.h @@ -0,0 +1,62 @@ +// ////////////////////////////////////////////////////////// +// toojpeg.h +// written by Stephan Brumme, 2018-2019 +// see https://create.stephan-brumme.com/toojpeg/ +// + +// This is a compact baseline JPEG/JFIF writer, written in C++ (but looks like C for the most part). +// Its interface has only one function: writeJpeg() - and that's it ! +// +// basic example: +// => create an image with any content you like, e.g. 1024x768, RGB = 3 bytes per pixel +// auto pixels = new unsigned char[1024*768*3]; +// => you need to define a callback that receives the compressed data byte-by-byte from my JPEG writer +// void myOutput(unsigned char oneByte) { fputc(oneByte, myFileHandle); } // save byte to file +// => let's go ! +// TooJpeg::writeJpeg(myOutput, mypixels, 1024, 768); + +#pragma once + +namespace TooJpeg +{ + // write one byte (to disk, memory, ...) + typedef void (*WRITE_ONE_BYTE)(unsigned char); + // this callback is called for every byte generated by the encoder and behaves similar to fputc + // if you prefer stylish C++11 syntax then it can be a lambda, too: + // auto myOutput = [](unsigned char oneByte) { fputc(oneByte, output); }; + + // output - callback that stores a single byte (writes to disk, memory, ...) + // pixels - stored in RGB format or grayscale, stored from upper-left to lower-right + // width,height - image size + // isRGB - true if RGB format (3 bytes per pixel); false if grayscale (1 byte per pixel) + // quality - between 1 (worst) and 100 (best) + // downsample - if true then YCbCr 4:2:0 format is used (smaller size, minor quality loss) instead of 4:4:4, not relevant for grayscale + // comment - optional JPEG comment (0/NULL if no comment), must not contain ASCII code 0xFF + bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels, unsigned short width, unsigned short height, + bool isRGB = true, unsigned char quality = 90, bool downsample = false, const char* comment = nullptr); +} // namespace TooJpeg + +// My main inspiration was Jon Olick's Minimalistic JPEG writer +// ( https://www.jonolick.com/code.html => direct link is https://www.jonolick.com/uploads/7/9/2/1/7921194/jo_jpeg.cpp ). +// However, his code documentation is quite sparse - probably because it wasn't written from scratch and is (quote:) "based on a javascript jpeg writer", +// most likely Andreas Ritter's code: https://github.com/eugeneware/jpeg-js/blob/master/lib/encoder.js +// +// Therefore I wrote the whole lib from scratch and tried hard to add tons of comments to my code, especially describing where all those magic numbers come from. +// And I managed to remove the need for any external includes ... +// yes, that's right: my library has no (!) includes at all, not even #include +// Depending on your callback WRITE_ONE_BYTE, the library writes either to disk, or in-memory, or wherever you wish. +// Moreover, no dynamic memory allocations are performed, just a few bytes on the stack. +// +// In contrast to Jon's code, compression can be significantly improved in many use cases: +// a) grayscale JPEG images need just a single Y channel, no need to save the superfluous Cb + Cr channels +// b) YCbCr 4:2:0 downsampling is often about 20% more efficient (=smaller) than the default YCbCr 4:4:4 with only little visual loss +// +// TooJpeg 1.2+ compresses about twice as fast as jo_jpeg (and about half as fast as libjpeg-turbo). +// A few benchmark numbers can be found on my website https://create.stephan-brumme.com/toojpeg/#benchmark +// +// Last but not least you can optionally add a JPEG comment. +// +// Your C++ compiler needs to support a reasonable subset of C++11 (g++ 4.7 or Visual C++ 2013 are sufficient). +// I haven't tested the code on big-endian systems or anything that smells like an apple. +// +// USE AT YOUR OWN RISK. Because you are a brave soul :-) \ No newline at end of file diff --git a/lib/lua/src/lua_canvas.cpp b/lib/lua/src/lua_canvas.cpp index 2bf1e7b9..c446da93 100644 --- a/lib/lua/src/lua_canvas.cpp +++ b/lib/lua/src/lua_canvas.cpp @@ -14,8 +14,8 @@ sol::table LuaCanvas::getTouch() int16_t x = gui::ElementBase::touchX, y = gui::ElementBase::touchY; sol::table result = lua->lua.create_table(); - result.set(1, x); - result.set(2, y); + result.set(1, x - widget->getAbsoluteX()); + result.set(2, y - widget->getAbsoluteY()); return result; } diff --git a/lib/lua/src/lua_file.cpp b/lib/lua/src/lua_file.cpp index ce9a0a0d..60fb065d 100755 --- a/lib/lua/src/lua_file.cpp +++ b/lib/lua/src/lua_file.cpp @@ -11,6 +11,9 @@ #include #include +#include +#include + /* LuaHttpClient::LuaHttpClient(LuaFile* lua) @@ -112,6 +115,150 @@ int custom_panic_handler(lua_State* L) { return 0; } +template +void writeBinaryValue(std::ofstream& file, const T& value) { + file.write(reinterpret_cast(&value), sizeof(T)); +} + +void saveTableToBinaryFile(std::ofstream& file, const sol::table& table) { + // Write the number of key-value pairs + size_t numPairs = table.size(); + writeBinaryValue(file, numPairs); + + for (const auto& pair : table) { + sol::object key = pair.first; + sol::object value = pair.second; + + // Write the key type + if (key.is()) { + writeBinaryValue(file, static_cast(1)); // 1 for string key + std::string keyStr = key.as(); + size_t keySize = keyStr.size(); + writeBinaryValue(file, keySize); + file.write(keyStr.c_str(), keySize); + } else if (key.is() || key.is()) { + writeBinaryValue(file, static_cast(2)); // 2 for numeric key + double numericKey = key.as(); + writeBinaryValue(file, numericKey); + } else { + throw std::runtime_error("Unsupported table key type for binary serialization"); + } + + // Write the value type and value + if (value.is()) { + writeBinaryValue(file, static_cast(1)); // 1 for string + std::string valueStr = value.as(); + size_t valueSize = valueStr.size(); + writeBinaryValue(file, valueSize); + file.write(valueStr.c_str(), valueSize); + } else if (value.is() || value.is()) { + writeBinaryValue(file, static_cast(2)); // 2 for number + double numericValue = value.as(); + writeBinaryValue(file, numericValue); + } else if (value.is()) { + writeBinaryValue(file, static_cast(3)); // 3 for boolean + bool boolValue = value.as(); + writeBinaryValue(file, boolValue); + } else if (value.is()) { + writeBinaryValue(file, static_cast(4)); // 4 for nil + } else if (value.is()) { + writeBinaryValue(file, static_cast(5)); // 5 for table + saveTableToBinaryFile(file, value.as()); // Recursively save + } else { + throw std::runtime_error("Unsupported table value type for binary serialization"); + } + } +} + +void saveTableToBinaryFile(const std::string& filename, const sol::table& table) { + std::ofstream file(filename, std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Error opening file for writing: " + filename); + } + + try { + saveTableToBinaryFile(file, table); + } catch (const std::exception& e) { + file.close(); + throw std::runtime_error(std::string("Error while writing to file: ") + e.what()); + } + + file.close(); +} + +template +T readBinaryValue(std::ifstream& file) { + T value; + file.read(reinterpret_cast(&value), sizeof(T)); + return value; +} + +sol::table loadTableFromBinaryFile(sol::state& lua, std::ifstream& file) { + sol::table table = lua.create_table(); + + size_t numPairs = readBinaryValue(file); + + for (size_t i = 0; i < numPairs; ++i) { + // Read key + uint8_t keyType = readBinaryValue(file); + sol::object key; + + if (keyType == 1) { // String key + size_t keySize = readBinaryValue(file); + std::vector keyBuffer(keySize); + file.read(keyBuffer.data(), keySize); + key = sol::make_object(lua, std::string(keyBuffer.data(), keySize)); + } else if (keyType == 2) { // Numeric key + key = sol::make_object(lua, readBinaryValue(file)); + } else { + throw std::runtime_error("Unsupported key type in binary file"); + } + + // Read value + uint8_t valueType = readBinaryValue(file); + + switch (valueType) { + case 1: { // String + size_t valueSize = readBinaryValue(file); + std::vector valueBuffer(valueSize); + file.read(valueBuffer.data(), valueSize); + table[key] = std::string(valueBuffer.data(), valueSize); + break; + } + case 2: // Number + table[key] = readBinaryValue(file); + break; + case 3: // Boolean + table[key] = readBinaryValue(file); + break; + case 4: // Nil + table[key] = sol::nil; + break; + case 5: // Nested table + table[key] = loadTableFromBinaryFile(lua, file); + break; + default: + throw std::runtime_error("Unsupported value type in binary file"); + } + } + + return table; +} + +sol::table loadTableFromBinaryFile(sol::state& lua, const std::string& filename) { + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Error opening file for reading: " + filename); + } + + try { + return loadTableFromBinaryFile(lua, file); + } catch (const std::exception& e) { + file.close(); + throw std::runtime_error(std::string("Error while reading from file: ") + e.what()); + } +} + void LuaFile::load() { StandbyMode::triggerPower(); @@ -131,7 +278,9 @@ void LuaFile::load() if(!nlohmann::json::accept(conf)) { - throw libsystem::exceptions::RuntimeError("Invalid app permissions."); + std::cerr << "Les permissions de l'app ne sont pas définies ou sont invalides" << std::endl; + std::cerr << "Conf: " << conf << " in " << manifest.str() << std::endl; + return; } nlohmann::json confJson = nlohmann::json::parse(conf); @@ -153,6 +302,30 @@ void LuaFile::load() lua.set_function("nonothing", []() { }); + lua["require"] = [&](const std::string& filename) -> sol::object { + storage::Path lib(filename); + + // Load the file + sol::load_result chunk = lua.load_file(this->lua_storage.convertPath(lib).str()); + if (!chunk.valid()) { + sol::error err = chunk; + throw std::runtime_error("Error loading module '" + filename + "': " + err.what()); + } + + // 4. Execute the loaded chunk and return its results + return chunk(); + }; + + lua["saveTable"] = [&](const std::string& filename, const sol::table& table) + { + saveTableToBinaryFile(lua_storage.convertPath(filename).str(), table); + }; + + lua["loadTable"] = [&](const std::string& filename) + { + return loadTableFromBinaryFile(lua, lua_storage.convertPath(filename).str()); + }; + if (perms.acces_hardware) // si hardware est autorisé { lua.new_usertype("hardware", @@ -349,6 +522,8 @@ void LuaFile::load() lua.new_usertype("LuaVList", "setIndex", &LuaVerticalList::setIndex, "setSpaceLine", &LuaVerticalList::setSpaceLine, + "setSelectionFocus", &LuaVerticalList::setFocus, + "getSelected", &LuaVerticalList::getSelected, sol::base_classes, sol::bases()); lua.new_usertype("LuaHList", diff --git a/lib/lua/src/lua_list.hpp b/lib/lua/src/lua_list.hpp index 115a6a1e..a0f888df 100644 --- a/lib/lua/src/lua_list.hpp +++ b/lib/lua/src/lua_list.hpp @@ -11,6 +11,8 @@ class LuaVerticalList : public LuaWidget void addChild(LuaWidget* widget){ this->children.push_back(widget); widget->parent = this; this->widget->add(widget->widget); } void setSpaceLine(int line){ this->widget->setSpaceLine(line); } void setIndex(int i = 0) { this->widget->setIndex(i); }; + void setFocus(VerticalList::SelectionFocus focus) { std::cout << "setFocus: " << focus << std::endl; this->widget->setSelectionFocus(focus); }; + int getSelected() { return this->widget->getFocusedElement(); }; VerticalList* widget = nullptr; }; diff --git a/lib/storage/filestream.cpp b/lib/storage/filestream.cpp index 3a58e65b..5001d7db 100644 --- a/lib/storage/filestream.cpp +++ b/lib/storage/filestream.cpp @@ -69,6 +69,11 @@ namespace storage m_stream << str; } + void FileStream::write(const char* str, std::size_t len) + { + m_stream.write(str, len); + } + void FileStream::write(const char c) { m_stream << c; diff --git a/lib/storage/filestream.hpp b/lib/storage/filestream.hpp index b491f3e0..4d397352 100644 --- a/lib/storage/filestream.hpp +++ b/lib/storage/filestream.hpp @@ -36,6 +36,7 @@ namespace storage char readchar(void); void write(const std::string &str); + void write(const char* str, std::size_t len); void write(const char c); bool isopen(void) const; diff --git a/lib/storage/path.cpp b/lib/storage/path.cpp index dc73c165..203c940a 100644 --- a/lib/storage/path.cpp +++ b/lib/storage/path.cpp @@ -34,7 +34,7 @@ bool storage::init() { constexpr uint8_t sdBeginTryCount = 4; for (int i = 0; i < sdBeginTryCount; i++) { - if (SD.begin(4/*, SPI, 8000000*/)) { + if (SD.begin(4, SPI, 8000000)) { libsystem::log("SD card initialized."); return true; } diff --git a/lib/tasks/src/standby.cpp b/lib/tasks/src/standby.cpp index 987c94c0..26d36675 100644 --- a/lib/tasks/src/standby.cpp +++ b/lib/tasks/src/standby.cpp @@ -1,4 +1,8 @@ #include "standby.hpp" +#include "app.hpp" +#include "graphics.hpp" +#include "hardware.hpp" +#include "gsm.hpp" #include #include @@ -24,6 +28,8 @@ namespace StandbyMode void trigger() { lastTrigger = millis(); + + graphics::setBrightness(graphics::brightness); } void triggerPower() @@ -38,9 +44,14 @@ namespace StandbyMode void update() { - if (millis() - lastTrigger > 1000) + if (!enabled && millis() - lastTrigger > sleepTime - 10000) + { + graphics::setBrightness(graphics::brightness/3 + 3); + } + + if (!enabled && millis() - lastTrigger > sleepTime) { - enabled = true; + enable(); } if (millis() - lastPowerTrigger > 5000) @@ -65,6 +76,30 @@ namespace StandbyMode void enable() { enabled = true; + lastTrigger = millis() - sleepTime; + + graphics::setBrightness(0); + StandbyMode::savePower(); + + while(AppManager::isAnyVisibleApp()) + { + AppManager::appStack[AppManager::appStack.size() - 1]->kill(); + AppManager::appStack.pop_back(); + } + + while (hardware::getHomeButton()); + while (!hardware::getHomeButton() && !AppManager::isAnyVisibleApp() && millis() - sleepTime > lastTrigger) + { + eventHandlerApp.update(); + AppManager::loop(); + } + + while (hardware::getHomeButton()); + + StandbyMode::restorePower(); + graphics::setBrightness(graphics::brightness); + + disable(); } void disable() diff --git a/lib/tasks/src/threads.cpp b/lib/tasks/src/threads.cpp index 5d08c1bd..8c0f9281 100644 --- a/lib/tasks/src/threads.cpp +++ b/lib/tasks/src/threads.cpp @@ -1,5 +1,6 @@ #include "threads.hpp" #include "delay.hpp" +#include #include #ifndef THREAD_HANDLER @@ -23,7 +24,7 @@ void ThreadManager::init() { - new_thread(CORE_BACK, &ThreadManager::simcom_thread, 16*1024); + new_thread(CORE_BACK, &ThreadManager::simcom_thread, 32*1024); new_thread(CORE_BACK, &ThreadManager::background_thread, 16*1024); } diff --git a/platformio.ini b/platformio.ini index c997ec74..3e99163a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,6 +20,7 @@ board = esp-wrover-kit framework = arduino board_build.partitions = huge_app.csv monitor_speed = 115200 +lib_ldf_mode = chain+ lib_deps = lovyan03/LovyanGFX@^1.1.9 bitbank2/FT6236G@^1.0.0 @@ -47,12 +48,14 @@ monitor_filters = esp32_exception_decoder [env:windows] platform = native +lib_ldf_mode = chain+ lib_deps = lovyan03/LovyanGFX@^1.1.9 plerup/EspSoftwareSerial@^8.2.0 maxgerhardt/ghostl@^1.0.1 mbed-babylonica/AsyncSerial@0.0.0+sha.278f7f125495 bitbank2/FT6236G@^1.0.0 + bitbank2/JPEGENC@^1.1.0 test_framework = googletest test_testing_command = ${platformio.build_dir}/${this.__env__}/program build_flags = @@ -73,12 +76,14 @@ extra_scripts = [env:windows-build-only] platform = native +lib_ldf_mode = chain+ lib_deps = lovyan03/LovyanGFX@^1.1.9 plerup/EspSoftwareSerial@^8.2.0 maxgerhardt/ghostl@^1.0.1 mbed-babylonica/AsyncSerial@0.0.0+sha.278f7f125495 bitbank2/FT6236G@^1.0.0 + bitbank2/JPEGENC@^1.1.0 build_flags = -std=c++17 -lm @@ -96,12 +101,14 @@ extra_scripts = [env:linux] platform = native +lib_ldf_mode = chain+ lib_deps = lovyan03/LovyanGFX@^1.1.9 plerup/EspSoftwareSerial@^8.2.0 maxgerhardt/ghostl@^1.0.1 mbed-babylonica/AsyncSerial@0.0.0+sha.278f7f125495 bitbank2/FT6236G@^1.0.0 + bitbank2/JPEGENC@^1.1.0 test_framework = googletest build_flags = -std=c++17 @@ -113,12 +120,14 @@ build_flags = [env:macos] platform = native +lib_ldf_mode = chain+ lib_deps = lovyan03/LovyanGFX@^1.1.9 plerup/EspSoftwareSerial@^8.2.0 maxgerhardt/ghostl@^1.0.1 mbed-babylonica/AsyncSerial@0.0.0+sha.278f7f125495 bitbank2/FT6236G@^1.0.0 + bitbank2/JPEGENC@^1.1.0 test_framework = googletest build_flags = -std=c++17 @@ -136,10 +145,11 @@ test_framework = googletest build_flags = -std=c++23 -lm -lib_ignore = - graphics +lib_ldf_mode = chain+ +lib_ignore = graphics lib_deps = plerup/EspSoftwareSerial@^8.2.0 maxgerhardt/ghostl@^1.0.1 mbed-babylonica/AsyncSerial@0.0.0+sha.278f7f125495 bitbank2/FT6236G@^1.0.0 + bitbank2/JPEGENC@^1.1.0 diff --git a/storage/apps/messages/app.lua b/storage/apps/messages/app.lua index 3e552a82..09f00e9c 100644 --- a/storage/apps/messages/app.lua +++ b/storage/apps/messages/app.lua @@ -19,6 +19,13 @@ function eraseNotif(number) listUpdated = true end +function openImage(path) + win3=gui:window() + + local image = gui:image(win3, "images/" .. path, 0, 0, 320, 480) + gui:setWindow(win3) +end + function newMessage(number) local msg = gui:keyboard("Message au " .. number, "") @@ -84,27 +91,38 @@ function converation(number) --print("getMessages returned " .. tostring(messages)) for i, message in pairs(messages) do - --print("message " .. tostring(i) .. " : " .. tostring(message)) - local bull = gui:box(list, 0, 0, 184, 30) - - local label = gui:label(bull, 0, 0, 184, 0) - label:setHorizontalAlignment(CENTER_ALIGNMENT) - label:setText(message.message) - label:setFontSize(18) + local numberPart = message.message:match("/(%d+)%.jpg") -- Extract the number part + + if(numberPart) then -- image + print("image " .. numberPart) + local image = gui:image(list, "images/" .. numberPart .. "p.jpg" , 0, 0, 60, 60) - local labelHeight = label:getTextHeight() + 8 + image:onClick(function () + openImage(string.sub(message.message, 2)) + end) + else + print("message " .. message.message) + local bull = gui:box(list, 0, 0, 184, 30) + + local label = gui:label(bull, 0, 0, 184, 0) + label:setHorizontalAlignment(CENTER_ALIGNMENT) + label:setText(message.message) + label:setFontSize(18) - label:setHeight(labelHeight) + local labelHeight = label:getTextHeight() + 8 - local canva = gui:canvas(bull, 0, labelHeight, 68, 1) - canva:fillRect(0, 0, 68, 1, COLOR_DARK) - canva:setX(57) + label:setHeight(labelHeight) - if(message.who == false) then - bull:setX(96) - end + local canva = gui:canvas(bull, 0, labelHeight, 68, 1) + canva:fillRect(0, 0, 68, 1, COLOR_DARK) + canva:setX(57) - bull:setHeight(labelHeight + 9) + if(message.who == false) then + bull:setX(96) + end + + bull:setHeight(labelHeight + 9) + end end list:setIndex(#messages -1) diff --git a/storage/system/icons/dark/battery_0_bar_64px.png b/storage/system/icons/dark/battery_0_bar_64px.png new file mode 100644 index 00000000..6a3f6372 Binary files /dev/null and b/storage/system/icons/dark/battery_0_bar_64px.png differ diff --git a/storage/system/icons/dark/battery_1_bar_64px.png b/storage/system/icons/dark/battery_1_bar_64px.png new file mode 100644 index 00000000..4ae80050 Binary files /dev/null and b/storage/system/icons/dark/battery_1_bar_64px.png differ diff --git a/storage/system/icons/dark/battery_2_bar_64px.png b/storage/system/icons/dark/battery_2_bar_64px.png new file mode 100644 index 00000000..6aff637d Binary files /dev/null and b/storage/system/icons/dark/battery_2_bar_64px.png differ diff --git a/storage/system/icons/dark/battery_3_bar_64px.png b/storage/system/icons/dark/battery_3_bar_64px.png new file mode 100644 index 00000000..eee05847 Binary files /dev/null and b/storage/system/icons/dark/battery_3_bar_64px.png differ diff --git a/storage/system/icons/dark/battery_4_bar_64px.png b/storage/system/icons/dark/battery_4_bar_64px.png new file mode 100644 index 00000000..476397b3 Binary files /dev/null and b/storage/system/icons/dark/battery_4_bar_64px.png differ diff --git a/storage/system/icons/dark/battery_5_bar_64px.png b/storage/system/icons/dark/battery_5_bar_64px.png new file mode 100644 index 00000000..63981794 Binary files /dev/null and b/storage/system/icons/dark/battery_5_bar_64px.png differ diff --git a/storage/system/icons/dark/battery_6_bar_64px.png b/storage/system/icons/dark/battery_6_bar_64px.png new file mode 100644 index 00000000..145b6d4f Binary files /dev/null and b/storage/system/icons/dark/battery_6_bar_64px.png differ diff --git a/storage/system/icons/dark/battery_charging_20_64px.png b/storage/system/icons/dark/battery_charging_20_64px.png new file mode 100644 index 00000000..7d8be6f3 Binary files /dev/null and b/storage/system/icons/dark/battery_charging_20_64px.png differ diff --git a/storage/system/icons/dark/battery_charging_30_64px.png b/storage/system/icons/dark/battery_charging_30_64px.png new file mode 100644 index 00000000..8a53a4a9 Binary files /dev/null and b/storage/system/icons/dark/battery_charging_30_64px.png differ diff --git a/storage/system/icons/dark/battery_charging_50_64px.png b/storage/system/icons/dark/battery_charging_50_64px.png new file mode 100644 index 00000000..e218895f Binary files /dev/null and b/storage/system/icons/dark/battery_charging_50_64px.png differ diff --git a/storage/system/icons/dark/battery_charging_60_64px.png b/storage/system/icons/dark/battery_charging_60_64px.png new file mode 100644 index 00000000..6ee4ea92 Binary files /dev/null and b/storage/system/icons/dark/battery_charging_60_64px.png differ diff --git a/storage/system/icons/dark/battery_charging_80_64px.png b/storage/system/icons/dark/battery_charging_80_64px.png new file mode 100644 index 00000000..8aa2c735 Binary files /dev/null and b/storage/system/icons/dark/battery_charging_80_64px.png differ diff --git a/storage/system/icons/dark/battery_charging_90_64px.png b/storage/system/icons/dark/battery_charging_90_64px.png new file mode 100644 index 00000000..ba8334ee Binary files /dev/null and b/storage/system/icons/dark/battery_charging_90_64px.png differ diff --git a/storage/system/icons/dark/battery_charging_full_64px.png b/storage/system/icons/dark/battery_charging_full_64px.png new file mode 100644 index 00000000..2787e26f Binary files /dev/null and b/storage/system/icons/dark/battery_charging_full_64px.png differ diff --git a/storage/system/icons/dark/battery_full_64px.png b/storage/system/icons/dark/battery_full_64px.png new file mode 100644 index 00000000..543cecb8 Binary files /dev/null and b/storage/system/icons/dark/battery_full_64px.png differ