From 4ec723ef40a9f12c8fb340f77726a8be36561943 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 18 Jun 2026 11:23:05 +0200 Subject: [PATCH] fix bugs, harden server, strip-based dithering, update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firmware: - fix 90° rotation (was transpose) - fix Adafruit_Thermal constructor (spurious DTR pin arg) - wire up uploadImage; heatInterval now applied - PSRAM-free strip dithering: 2-row buffers (~1KB) replace 153KB PSRAM alloc - consolidate all pins into config.h; BUTTON_PIN → Config::PIN_BUTTON - constrain contrast/brightness/heat in settings save handler - uploadImage size param int → size_t Server: - canonicalize upload_dir at startup (fixes path traversal guard) - path traversal guard in serve_image - replace unwrap in gallery_data with error handling - IMAGE_WIDTH/IMAGE_HEIGHT named constants Gallery: - innerHTML → createElement (XSS-safe) - encodeURIComponent on image URLs - replace("_"," ") → regex /_/g Docs: rewrite README, clarify GUEST_MANUAL placeholders --- GUEST_MANUAL.md | 19 +++-- README.md | 169 ++++++++++++++++++++++++-------------- include/config.h | 32 ++++++-- include/image_processor.h | 2 +- include/printer_service.h | 2 +- server/src/gallery.html | 39 ++++++--- server/src/main.rs | 28 +++++-- src/camera_service.cpp | 50 ++++------- src/image_processor.cpp | 121 ++++++++++----------------- src/main.cpp | 6 +- src/printer_service.cpp | 8 +- src/settings_service.cpp | 6 +- 12 files changed, 263 insertions(+), 219 deletions(-) diff --git a/GUEST_MANUAL.md b/GUEST_MANUAL.md index 256b445..b072e69 100644 --- a/GUEST_MANUAL.md +++ b/GUEST_MANUAL.md @@ -6,11 +6,11 @@ Snap a photo, get a receipt, and view the digital archive instantly. ### How to use -1. **Frame your shot.** (The lens is slightly wide-angle!) -2. **Press the Button.** -3. **Wait ~5 seconds.** - - **Print:** Collect your physical photo. - - **Upload:** The photo is magically sent to the gallery. +1. **Frame your shot.** (Wide-angle lens — back up a little!) +2. **Press the button.** +3. **Wait ~5 seconds.** + - A thermal print comes out of the printer. + - The photo is uploaded to the live gallery. --- @@ -18,11 +18,12 @@ Snap a photo, get a receipt, and view the digital archive instantly. Want to save the photo to your phone? -1. Connect to the WiFi: **[Your_WiFi_Name]** -2. Scan this code or go to: - **http://[YOUR_LAPTOP_IP]:3000/gallery** -3. Password: **partytime** +1. Connect to the WiFi: **[FILL IN: venue WiFi name]** +2. Go to: **http://[FILL IN: server IP]:3000/gallery** +3. Password: **[FILL IN: gallery password]** _(Ask the host if you can't connect!)_ --- + +> **Host checklist before printing this:** fill in the three `[FILL IN]` fields above. diff --git a/README.md b/README.md index 7b3d945..b8bbf6f 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,131 @@ # Party Cam: Instant Thermal Camera & Digital Gallery -A DIY point-and-shoot camera that prints dithered lo-fi photos on thermal receipts and instantly uploads high-res copies to a local Rust server. +A DIY point-and-shoot that prints dithered lo-fi photos on thermal receipt paper and simultaneously uploads captures to a local Rust web server. + +--- ## Hardware BOM -- **ESP32-CAM** -- **Thermal Printer** (TTL Serial, 5V-9V) -- **Batteries** (Recommended: 2x 18650 Li-Ion in series = 7.4V, powering printer directly + buck converter to 5V for ESP32) -- **Button** (Tactile switch) -- **Programmer**: FTDI Adapter or Arduino Uno (for flashing) +| Part | Notes | +| :--- | :--- | +| ESP32-CAM (AI-Thinker) | Camera + WiFi module | +| Thermal Printer | TTL serial, 5V–9V | +| 2× 18650 Li-Ion | In series = 7.4V; powers printer directly + 5V buck converter for ESP32 | +| Tactile button | Shutter trigger | +| FTDI adapter or Arduino Uno | For flashing only | + +--- ## Wiring -### 1. Camera & Printer +| ESP32-CAM Pin | Component | Function | +| :--- | :--- | :--- | +| **5V** | PSU 5V | Power in | +| **GND** | PSU GND | Common ground | +| **GPIO 12** | Button | Shutter (other leg → GND) | +| **GPIO 14** (U1T) | Printer RX | Data to printer | +| **GPIO 15** (U1R) | Printer TX | Data from printer | +| **GPIO 4** | Flash LED | Optional built-in flash | -| ESP32-CAM Pin | Component | Function | -| :---------------- | :--------- | :--------------------------------- | -| **5V** | PSU 5V | Power In | -| **GND** | PSU GND | Common Ground | -| **GPIO 12** | Button | Trigger (Connect other leg to GND) | -| **GPIO 14** (U1T) | Printer RX | Data TO Printer | -| **GPIO 15** (U1R) | Printer TX | Data FROM Printer | -| **GPIO 4** | Flash LED | (Optional) Built-in Flash | - -### 2. Flashing (Using Arduino Uno as Bridge) - -_See "How to Flash" section below._ +All pin assignments are in `include/config.h`. Change them there if your wiring differs. --- -## Setup Guide +## Server Setup -### A. The Server (Rust) +Run the server on any machine on the same local network as the camera. It receives uploads and hosts the gallery. -The server receives images and hosts the gallery for guests. +### Option A: Docker Compose (recommended) -1. **Install Rust:** `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` -2. **Configure:** - Create a `.env` file in the `server` folder: - ```ini - SERVER_HOST=0.0.0.0 - SERVER_PORT=3000 - GALLERY_PASSWORD=partytime - UPLOAD_DIR=./uploads - ``` -3. **Run:** - ```bash - cd server - cargo run --release - ``` - _Server will listen on port 3000._ +```bash +cd server +docker compose up -d +``` -### B. The Camera (Firmware) +Images are saved to `server/party_images/` on the host. To change the gallery password, edit `GALLERY_PASSWORD` in `compose.yml`. -1. **Open Project:** Open this folder in VS Code with the PlatformIO extension. -2. **Flash Mode Wiring (Arduino Uno Method):** - - **Arduino RESET** -> **Arduino GND** (Bypasses Arduino chip). - - **Arduino Pin 0 (RX)** -> **ESP32 U0R** (Direct passthrough). - - **Arduino Pin 1 (TX)** -> **ESP32 U0T** (Direct passthrough). - - **ESP32 IO0** -> **ESP32 GND** (Enables Download Mode). -3. **Upload:** - - Connect via USB. - - Click "Upload". - - Press the **RST** button on ESP32 when "Connecting..." appears. -4. **Run Mode:** - - **Disconnect IO0 from GND.** - - Press **RST** to boot. +### Option B: Cargo (dev) + +```bash +cd server +cargo run --release +``` + +Requires a `.env` file in the `server/` directory: + +```ini +SERVER_HOST=0.0.0.0 +SERVER_PORT=3000 +GALLERY_PASSWORD=partytime +UPLOAD_DIR=./uploads +``` + +`GALLERY_PASSWORD` is required — the server panics at startup without it. --- -## Configuration (First Boot) +## Firmware Setup -1. **Power on** the camera. -2. Connect your phone to the WiFi network: `PartyCam-Setup`. -3. A captive portal should open (or go to `192.168.4.1`). -4. **WiFi:** Select the venue's WiFi (or your phone hotspot). -5. **Server URL:** Enter your laptop's IP (e.g., `http://192.168.1.15:3000/upload`). -6. **Settings:** Adjust Contrast/Heat if the print is too faint or dark. -7. **Save & Reboot.** +### 1. Configure -The camera is now live! +Check `platformio.ini` and make sure `board` matches your actual hardware. The default is `4d_systems_esp32s3_gen4_r8n16` — change it to `ai_thinker_esp32-cam` if using a standard ESP32-CAM. Camera pin definitions in `include/config.h` are pre-set for AI-Thinker. + +### 2. Flash (Arduino Uno as bridge) + +Wire the Uno as a passthrough: +- Arduino RESET → Arduino GND (bypasses the Uno chip) +- Arduino Pin 0 (RX) → ESP32 U0R +- Arduino Pin 1 (TX) → ESP32 U0T +- **ESP32 IO0 → ESP32 GND** (enables download mode) + +Click **Upload** in PlatformIO, then press RST on the ESP32 when "Connecting…" appears. + +### 3. Boot + +Disconnect IO0 from GND, press RST. The camera boots. + +--- + +## First Boot Configuration + +1. Power on. Camera broadcasts WiFi AP: **`PartyCam-Setup`**. +2. Connect your phone — a captive portal opens automatically (or go to `192.168.4.1`). +3. Select the venue WiFi and enter the password. +4. Set the **Upload URL** to `http://:3000/upload`. +5. Adjust contrast/heat to taste. **Save & Reboot.** + +The portal times out after 3 minutes. If it closes before you save, the camera boots in offline mode (printing still works, upload disabled). + +--- + +## Settings Page + +After first boot the settings page is always reachable at: + +``` +http://partycam.local +``` + +(or the camera's IP address on port 80). Use it to adjust contrast, brightness, flip/mirror, heat density, and the upload URL. + +--- + +## Gallery + +``` +http://:3000/gallery +``` + +Open from any device on the same network. The browser will prompt for the gallery password. New photos appear automatically — the page polls every 5 seconds. + +--- + +## Customisation + +| What | Where | +| :--- | :--- | +| Pin assignments | `include/config.h` | +| Printer baud rate | `include/config.h` → `BAUD_RATE` | +| Default camera settings | `include/settings_service.h` → `AppSettings` struct defaults | +| Gallery password | `compose.yml` or `.env` | +| Capture resolution | `src/camera_service.cpp` → `config.frame_size` (update `IMAGE_WIDTH`/`IMAGE_HEIGHT` in `server/src/main.rs` to match) | diff --git a/include/config.h b/include/config.h index 3d893ba..17468f8 100644 --- a/include/config.h +++ b/include/config.h @@ -2,16 +2,32 @@ #include namespace Config { - // Hardware Pins (ESP32-CAM AI-Thinker) - constexpr int PIN_CAM_PWDN = 32; - constexpr int PIN_CAM_RESET = -1; - // ... (Add standard camera pins here to keep main clean) ... - + // Camera Pins (ESP32-CAM AI-Thinker) + constexpr int PIN_CAM_PWDN = 32; + constexpr int PIN_CAM_RESET = -1; + constexpr int PIN_CAM_XCLK = 0; + constexpr int PIN_CAM_SIOD = 26; + constexpr int PIN_CAM_SIOC = 27; + constexpr int PIN_CAM_Y9 = 35; + constexpr int PIN_CAM_Y8 = 34; + constexpr int PIN_CAM_Y7 = 39; + constexpr int PIN_CAM_Y6 = 36; + constexpr int PIN_CAM_Y5 = 21; + constexpr int PIN_CAM_Y4 = 19; + constexpr int PIN_CAM_Y3 = 18; + constexpr int PIN_CAM_Y2 = 5; + constexpr int PIN_CAM_VSYNC = 25; + constexpr int PIN_CAM_HREF = 23; + constexpr int PIN_CAM_PCLK = 22; + // Printer Pins - constexpr int PIN_PRINTER_RX = 14; + constexpr int PIN_PRINTER_RX = 14; constexpr int PIN_PRINTER_TX = 15; - + + // Button + constexpr int PIN_BUTTON = 12; + // Settings - constexpr int PRINTER_WIDTH = 384; constexpr int BAUD_RATE = 9600; // Check your printer specs (some are 19200) + constexpr int PRINTER_SERIAL = 1; } \ No newline at end of file diff --git a/include/image_processor.h b/include/image_processor.h index 91fe0be..908ae6d 100644 --- a/include/image_processor.h +++ b/include/image_processor.h @@ -7,5 +7,5 @@ class ImageProcessor { public: static void processAndPrint(const CameraService::Frame &frame, PrinterService &printer, const AppSettings &settings); - static void uploadImage(const uint8_t *bitmap, int size, const String &url); + static void uploadImage(const uint8_t *bitmap, size_t size, const String &url); }; \ No newline at end of file diff --git a/include/printer_service.h b/include/printer_service.h index 8f37d9c..fe23865 100644 --- a/include/printer_service.h +++ b/include/printer_service.h @@ -24,7 +24,7 @@ public: void sleep(); void feed(int lines); - void setHeat(uint8_t heatTime); + void setHeat(uint8_t heatTime, uint8_t heatInterval); void printBitmap(int w, int h, const uint8_t *bitmap); void printText(const String &text); diff --git a/server/src/gallery.html b/server/src/gallery.html index b75db7d..d4be1b6 100644 --- a/server/src/gallery.html +++ b/server/src/gallery.html @@ -79,19 +79,32 @@ } function addCard(filename) { - const div = document.createElement("div"); - div.className = "card"; - div.innerHTML = ` - - Party Photo - -
${filename - .replace("capture_", "") - .replace(".png", "") - .replace("_", " ")}
- `; - // Prepend to show new ones at top - gallery.prepend(div); + const url = `/gallery/img/${encodeURIComponent(filename)}`; + const label = filename + .replace("capture_", "") + .replace(".png", "") + .replace(/_/g, " "); + + const a = document.createElement("a"); + a.href = url; + a.target = "_blank"; + + const img = document.createElement("img"); + img.src = url; + img.loading = "lazy"; + img.alt = "Party Photo"; + a.appendChild(img); + + const meta = document.createElement("div"); + meta.className = "meta"; + meta.textContent = label; + + const card = document.createElement("div"); + card.className = "card"; + card.appendChild(a); + card.appendChild(meta); + + gallery.prepend(card); } // Initial load and poll every 5 seconds diff --git a/server/src/main.rs b/server/src/main.rs index 8cc0379..0197b12 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -34,6 +34,7 @@ async fn main() -> anyhow::Result<()> { PathBuf::from(env::var("UPLOAD_DIR").unwrap_or_else(|_| "./uploads".to_string())); tokio::fs::create_dir_all(&upload_dir).await?; + let upload_dir = upload_dir.canonicalize()?; let state = Arc::new(AppState { upload_dir, password, @@ -89,7 +90,13 @@ async fn gallery_view() -> Html<&'static str> { } async fn gallery_data(State(state): State>) -> impl IntoResponse { - let mut entries = tokio::fs::read_dir(&state.upload_dir).await.unwrap(); + let mut entries = match tokio::fs::read_dir(&state.upload_dir).await { + Ok(e) => e, + Err(e) => { + tracing::error!("Failed to read upload dir: {}", e); + return axum::Json(Vec::::new()).into_response(); + } + }; let mut images = Vec::new(); while let Ok(Some(entry)) = entries.next_entry().await { @@ -104,7 +111,7 @@ async fn gallery_data(State(state): State>) -> impl IntoResponse { } images.sort_by(|a, b| b.cmp(a)); - axum::Json(images) + axum::Json(images).into_response() } async fn serve_image( @@ -112,7 +119,13 @@ async fn serve_image( Path(filename): Path, ) -> impl IntoResponse { let path = state.upload_dir.join(&filename); - match tokio::fs::read(&path).await { + let Ok(canonical) = path.canonicalize() else { + return StatusCode::NOT_FOUND.into_response(); + }; + if !canonical.starts_with(&state.upload_dir) { + return StatusCode::BAD_REQUEST.into_response(); + } + match tokio::fs::read(&canonical).await { Ok(bytes) => ([(header::CONTENT_TYPE, "image/png")], bytes).into_response(), Err(_) => StatusCode::NOT_FOUND.into_response(), } @@ -150,10 +163,13 @@ async fn basic_auth( .into_response() } +// Must match FRAMESIZE_QVGA rotated 90°: camera captures 320×240, firmware rotates to 240×320. +const IMAGE_WIDTH: u32 = 240; +const IMAGE_HEIGHT: u32 = 320; + fn convert_and_save(data: &[u8], path: &PathBuf) -> anyhow::Result<()> { - // Dimensions from ESP32 code (240x320) - let width = 240; - let height = 320; + let width = IMAGE_WIDTH; + let height = IMAGE_HEIGHT; let mut img = GrayImage::new(width, height); diff --git a/src/camera_service.cpp b/src/camera_service.cpp index 79b81c8..b8f7258 100644 --- a/src/camera_service.cpp +++ b/src/camera_service.cpp @@ -2,46 +2,28 @@ #include "camera_service.h" #include "config.h" -#define PWDN_GPIO_NUM 32 -#define RESET_GPIO_NUM -1 -#define XCLK_GPIO_NUM 0 -#define SIOD_GPIO_NUM 26 -#define SIOC_GPIO_NUM 27 - -#define Y9_GPIO_NUM 35 -#define Y8_GPIO_NUM 34 -#define Y7_GPIO_NUM 39 -#define Y6_GPIO_NUM 36 -#define Y5_GPIO_NUM 21 -#define Y4_GPIO_NUM 19 -#define Y3_GPIO_NUM 18 -#define Y2_GPIO_NUM 5 -#define VSYNC_GPIO_NUM 25 -#define HREF_GPIO_NUM 23 -#define PCLK_GPIO_NUM 22 - void CameraService::init() { camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; config.ledc_timer = LEDC_TIMER_0; - config.pin_d0 = Y2_GPIO_NUM; - config.pin_d1 = Y3_GPIO_NUM; - config.pin_d2 = Y4_GPIO_NUM; - config.pin_d3 = Y5_GPIO_NUM; - config.pin_d4 = Y6_GPIO_NUM; - config.pin_d5 = Y7_GPIO_NUM; - config.pin_d6 = Y8_GPIO_NUM; - config.pin_d7 = Y9_GPIO_NUM; - config.pin_xclk = XCLK_GPIO_NUM; - config.pin_pclk = PCLK_GPIO_NUM; - config.pin_vsync = VSYNC_GPIO_NUM; - config.pin_href = HREF_GPIO_NUM; - config.pin_sscb_sda = SIOD_GPIO_NUM; - config.pin_sscb_scl = SIOC_GPIO_NUM; - config.pin_pwdn = PWDN_GPIO_NUM; - config.pin_reset = RESET_GPIO_NUM; + config.pin_d0 = Config::PIN_CAM_Y2; + config.pin_d1 = Config::PIN_CAM_Y3; + config.pin_d2 = Config::PIN_CAM_Y4; + config.pin_d3 = Config::PIN_CAM_Y5; + config.pin_d4 = Config::PIN_CAM_Y6; + config.pin_d5 = Config::PIN_CAM_Y7; + config.pin_d6 = Config::PIN_CAM_Y8; + config.pin_d7 = Config::PIN_CAM_Y9; + config.pin_xclk = Config::PIN_CAM_XCLK; + config.pin_pclk = Config::PIN_CAM_PCLK; + config.pin_vsync = Config::PIN_CAM_VSYNC; + config.pin_href = Config::PIN_CAM_HREF; + config.pin_sscb_sda = Config::PIN_CAM_SIOD; + config.pin_sscb_scl = Config::PIN_CAM_SIOC; + config.pin_pwdn = Config::PIN_CAM_PWDN; + config.pin_reset = Config::PIN_CAM_RESET; config.xclk_freq_hz = 20000000; diff --git a/src/image_processor.cpp b/src/image_processor.cpp index e2d1c47..cd185c0 100644 --- a/src/image_processor.cpp +++ b/src/image_processor.cpp @@ -7,113 +7,86 @@ void ImageProcessor::processAndPrint(const CameraService::Frame &frame, PrinterS if (!frame.isValid()) return; - // 1. Setup Dimensions (Rotate 90 degrees) - // Camera is 320x240 (Landscape) -> Printer is 384 wide (Portrait) - // We will map Camera Height (240) to Printer Width. - // Since 240 < 384, the image will be centered or small. - // If you want full width, we must scale, but rotation is cheaper. - const int width = frame.getHeight(); // 240 - const int height = frame.getWidth(); // 320 + // Rotate 90° CW: source is 320×240 (landscape), output is 240×320 (portrait) + // dest[row=y, col=x] = src[row = srcHeight-1-x, col = y] + const int width = frame.getHeight(); // 240 + const int height = frame.getWidth(); // 320 + const int rowBytes = (width + 7) / 8; // 30 - // 2. Allocate Buffer in PSRAM - // We need int16_t to handle the overflow error during calculation - size_t bufSize = width * height * sizeof(int16_t); - int16_t *pixels = (int16_t *)heap_caps_malloc(bufSize, MALLOC_CAP_SPIRAM); + const uint8_t *src = frame.getData(); + const int srcWidth = frame.getWidth(); // 320 + const int srcHeight = frame.getHeight(); // 240 - if (!pixels) + // Two row buffers for Floyd-Steinberg: current row + next-row error accumulator. + // ~960 bytes total — fits in internal RAM, no PSRAM required. + int16_t *cur_row = (int16_t *)malloc(width * sizeof(int16_t)); + int16_t *next_row = (int16_t *)calloc(width, sizeof(int16_t)); + size_t bitmapSize = (size_t)rowBytes * height; + uint8_t *bitmap = (uint8_t *)calloc(bitmapSize, 1); + + if (!cur_row || !next_row || !bitmap) { - Serial.println("[ERR] PSRAM Alloc Failed"); + free(cur_row); + free(next_row); + free(bitmap); + Serial.println("[ERR] Alloc Failed"); return; } - // 3. Rotate & Load - const uint8_t *src = frame.getData(); - // Pre-calculate to save cycles in loop - const int srcWidth = frame.getWidth(); - - for (int y = 0; y < srcWidth; y++) - { // 320 (Source Y) - for (int x = 0; x < width; x++) - { // 240 (Source X) - // 90 Deg Rotation Logic - // Dest(x, y) = Src(y, srcWidth - x - 1) - // We are mapping the camera buffer to our Dithering Buffer - uint8_t val = src[x * srcWidth + y]; - pixels[y * width + x] = static_cast(val); - } - } - - // 4. Floyd-Steinberg Dithering for (int y = 0; y < height; y++) { + // Load rotated pixels for this row, folding in accumulated error from previous row for (int x = 0; x < width; x++) { - int16_t oldPixel = pixels[y * width + x]; - int16_t newPixel = (oldPixel > 128) ? 255 : 0; + cur_row[x] = (int16_t)src[(srcHeight - 1 - x) * srcWidth + y] + next_row[x]; + next_row[x] = 0; + } - pixels[y * width + x] = newPixel; + // Floyd-Steinberg dithering + for (int x = 0; x < width; x++) + { + int16_t oldPixel = cur_row[x]; + int16_t newPixel = (oldPixel > 128) ? 255 : 0; + cur_row[x] = newPixel; int16_t error = oldPixel - newPixel; - // Distribute Error (Bit shifting is faster than division) - // Right: 7/16 if (x + 1 < width) - pixels[y * width + (x + 1)] += (error * 7) >> 4; + cur_row[x + 1] += (error * 7) >> 4; - // Bottom-Left: 3/16 if (y + 1 < height) { if (x - 1 >= 0) - pixels[(y + 1) * width + (x - 1)] += (error * 3) >> 4; - - // Bottom: 5/16 - pixels[(y + 1) * width + x] += (error * 5) >> 4; - - // Bottom-Right: 1/16 + next_row[x - 1] += (error * 3) >> 4; + next_row[x] += (error * 5) >> 4; if (x + 1 < width) - pixels[(y + 1) * width + (x + 1)] += (error * 1) >> 4; + next_row[x + 1] += (error * 1) >> 4; } } - } - // 5. Pack & Print - // We pack 8 pixels into 1 byte. - // The Adafruit library expects a full bitmap array. - // Optimization: We reuse the PSRAM allocation or alloc a smaller one. - - // Row stride must be multiple of 8 - int rowBytes = (width + 7) / 8; - size_t bitmapSize = rowBytes * height; - uint8_t *bitmap = (uint8_t *)heap_caps_calloc(bitmapSize, 1, MALLOC_CAP_SPIRAM); - - if (!bitmap) - { - free(pixels); - return; - } - - for (int y = 0; y < height; y++) - { + // Pack row into bitmap for (int x = 0; x < width; x++) { - if (pixels[y * width + x] == 0) - { // If black + if (cur_row[x] == 0) bitmap[y * rowBytes + (x / 8)] |= (1 << (7 - (x % 8))); - } } } - printer.setHeat(settings.heatTime); + free(cur_row); + free(next_row); + + printer.setHeat(settings.heatTime, settings.heatInterval); printer.wake(); printer.printBitmap(width, height, bitmap); printer.feed(3); printer.sleep(); - // 6. Cleanup (RAII handled elsewhere, but raw pointers need free) - free(pixels); + if (settings.enableUpload && settings.uploadUrl.length() > 0) + uploadImage(bitmap, bitmapSize, settings.uploadUrl); + free(bitmap); } -void ImageProcessor::uploadImage(const uint8_t *bitmap, int size, const String &url) +void ImageProcessor::uploadImage(const uint8_t *bitmap, size_t size, const String &url) { if (WiFi.status() != WL_CONNECTED) return; @@ -125,13 +98,9 @@ void ImageProcessor::uploadImage(const uint8_t *bitmap, int size, const String & int responseCode = http.POST((uint8_t *)bitmap, size); if (responseCode > 0) - { Serial.printf("[Upload] Success: %d\n", responseCode); - } else - { Serial.printf("[Upload] Failed: %s\n", http.errorToString(responseCode).c_str()); - } http.end(); -} \ No newline at end of file +} diff --git a/src/main.cpp b/src/main.cpp index 14aea74..a31c964 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,10 @@ #include #include "camera_service.h" +#include "config.h" #include "printer_service.h" #include "image_processor.h" #include "settings_service.h" -const int BUTTON_PIN = 12; // Check your wiring unsigned long lastDebounceTime = 0; const unsigned long debounceDelay = 200; @@ -28,7 +28,7 @@ void setup() Serial.begin(115200); Serial.println("Party Cam Starting..."); - pinMode(BUTTON_PIN, INPUT_PULLUP); + pinMode(Config::PIN_BUTTON, INPUT_PULLUP); CameraService::init(); PrinterService::init(); @@ -45,7 +45,7 @@ void loop() settingsService.handle(); - if (digitalRead(BUTTON_PIN) == LOW) + if (digitalRead(Config::PIN_BUTTON) == LOW) { if ((millis() - lastDebounceTime) > debounceDelay) { diff --git a/src/printer_service.cpp b/src/printer_service.cpp index b911e27..913f261 100644 --- a/src/printer_service.cpp +++ b/src/printer_service.cpp @@ -4,8 +4,8 @@ PrinterService *PrinterService::instance = nullptr; PrinterService::PrinterService() { - printerSerial = new HardwareSerial(1); // replace it with not a magic number - printer = new Adafruit_Thermal(printerSerial, Config::PIN_PRINTER_RX); + printerSerial = new HardwareSerial(Config::PRINTER_SERIAL); + printer = new Adafruit_Thermal(printerSerial); } void PrinterService::init() @@ -51,10 +51,8 @@ void PrinterService::printText(const String &text) printer->println(text); } -void PrinterService::setHeat(uint8_t heatTime) +void PrinterService::setHeat(uint8_t heatTime, uint8_t heatInterval) { constexpr int heatDots = 11; - constexpr int heatInterval = 50; - printer->setHeatConfig(heatDots, heatTime, heatInterval); } \ No newline at end of file diff --git a/src/settings_service.cpp b/src/settings_service.cpp index f6372ca..4797325 100644 --- a/src/settings_service.cpp +++ b/src/settings_service.cpp @@ -123,9 +123,9 @@ void SettingsService::setupRoutes() server.on("/save", HTTP_POST, [this]() { - if (server.hasArg("contrast")) currentSettings.contrast = server.arg("contrast").toInt(); - if (server.hasArg("brightness")) currentSettings.brightness = server.arg("brightness").toInt(); - if (server.hasArg("heat")) currentSettings.heatTime = server.arg("heat").toInt(); + if (server.hasArg("contrast")) currentSettings.contrast = constrain(server.arg("contrast").toInt(), -2, 2); + if (server.hasArg("brightness")) currentSettings.brightness = constrain(server.arg("brightness").toInt(), -2, 2); + if (server.hasArg("heat")) currentSettings.heatTime = constrain(server.arg("heat").toInt(), 0, 255); currentSettings.uploadUrl = server.arg("url"); currentSettings.vFlip = server.hasArg("vflip");