fix bugs, harden server, strip-based dithering, update docs

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
This commit is contained in:
2026-06-18 11:23:05 +02:00
parent 2f827c168d
commit 4ec723ef40
12 changed files with 263 additions and 219 deletions

View File

@@ -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.

169
README.md
View File

@@ -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, 5V9V |
| 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://<server-machine-ip>: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://<server-ip>: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) |

View File

@@ -2,16 +2,32 @@
#include <Arduino.h>
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;
}

View File

@@ -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);
};

View File

@@ -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);

View File

@@ -79,19 +79,32 @@
}
function addCard(filename) {
const div = document.createElement("div");
div.className = "card";
div.innerHTML = `
<a href="/gallery/img/${filename}" target="_blank">
<img src="/gallery/img/${filename}" loading="lazy" alt="Party Photo">
</a>
<div class="meta">${filename
.replace("capture_", "")
.replace(".png", "")
.replace("_", " ")}</div>
`;
// 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

View File

@@ -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<Arc<AppState>>) -> 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::<String>::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<Arc<AppState>>) -> 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<String>,
) -> 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);

View File

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

View File

@@ -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<int16_t>(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();
}
}

View File

@@ -1,10 +1,10 @@
#include <Arduino.h>
#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)
{

View File

@@ -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);
}

View File

@@ -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");