Arnov Sharma
Published © MIT

PolyShot Point and Shoot Camera

An open-source point-and-shoot camera I built from scratch.

BeginnerFull instructions provided5 hours226

Things used in this project

Hardware components

Unihiker K10
×1
NextPCB  Custom PCB Board
NextPCB Custom PCB Board
×1

Software apps and online services

Fusion
Autodesk Fusion
Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

BOTTOM ENCLOSURE

TOP ENCLOSURE

LENS HOLDER

SWITCH 1 & 2

SWITCH 3

LENS CAP

SWITCH HOLDER

Schematics

SCH

Code

code

C/C++
#include "unihiker_k10.h"

UNIHIKER_K10 k10;

uint8_t screen_dir = 2; // Keep DMA landscape logic
int imageCounter = 1;

// --- STATE MACHINE & FLAGS ---
bool inGalleryMode = false;
int galleryIndex = 1;

// High-speed debouncing state trackers
bool lastStateP0 = HIGH;
bool lastStateP1 = HIGH;
bool lastStateP2 = HIGH;
unsigned long lastDebounceTime = 0;
const int DEBOUNCE_DELAY = 200; // 200ms lock-out after a press

// --- 1. FULL 5x7 ASCII FONT ---
const uint8_t font5x7[59][5] = {
  {0x00, 0x00, 0x00, 0x00, 0x00}, // 32: Space
  {0x00, 0x00, 0x2f, 0x00, 0x00}, // 33: !
  {0x00, 0x07, 0x00, 0x07, 0x00}, // 34: "
  {0x14, 0x7f, 0x14, 0x7f, 0x14}, // 35: #
  {0x24, 0x2a, 0x7f, 0x2a, 0x12}, // 36: $
  {0x23, 0x13, 0x08, 0x64, 0x62}, // 37: %
  {0x36, 0x49, 0x55, 0x22, 0x50}, // 38: &
  {0x00, 0x05, 0x03, 0x00, 0x00}, // 39: '
  {0x00, 0x1c, 0x22, 0x41, 0x00}, // 40: (
  {0x00, 0x41, 0x22, 0x1c, 0x00}, // 41: )
  {0x14, 0x08, 0x3E, 0x08, 0x14}, // 42: *
  {0x08, 0x08, 0x3E, 0x08, 0x08}, // 43: +
  {0x00, 0x00, 0x50, 0x30, 0x00}, // 44: ,
  {0x08, 0x08, 0x08, 0x08, 0x08}, // 45: -
  {0x00, 0x60, 0x60, 0x00, 0x00}, // 46: .
  {0x20, 0x10, 0x08, 0x04, 0x02}, // 47: /
  {0x3E, 0x51, 0x49, 0x45, 0x3E}, // 48: 0
  {0x00, 0x42, 0x7F, 0x40, 0x00}, // 49: 1
  {0x42, 0x61, 0x51, 0x49, 0x46}, // 50: 2
  {0x21, 0x41, 0x45, 0x4B, 0x31}, // 51: 3
  {0x18, 0x14, 0x12, 0x7F, 0x10}, // 52: 4
  {0x27, 0x45, 0x45, 0x45, 0x39}, // 53: 5
  {0x3C, 0x4A, 0x49, 0x49, 0x30}, // 54: 6
  {0x01, 0x71, 0x09, 0x05, 0x03}, // 55: 7
  {0x36, 0x49, 0x49, 0x49, 0x36}, // 56: 8
  {0x06, 0x49, 0x49, 0x29, 0x1E}, // 57: 9
  {0x00, 0x36, 0x36, 0x00, 0x00}, // 58: :
  {0x00, 0x56, 0x36, 0x00, 0x00}, // 59: ;
  {0x08, 0x14, 0x22, 0x41, 0x00}, // 60: <
  {0x14, 0x14, 0x14, 0x14, 0x14}, // 61: =
  {0x00, 0x41, 0x22, 0x14, 0x08}, // 62: >
  {0x02, 0x01, 0x51, 0x09, 0x06}, // 63: ?
  {0x32, 0x49, 0x59, 0x51, 0x3E}, // 64: @
  {0x7E, 0x11, 0x11, 0x11, 0x7E}, // 65: A
  {0x7F, 0x49, 0x49, 0x49, 0x36}, // 66: B
  {0x3E, 0x41, 0x41, 0x41, 0x22}, // 67: C
  {0x7F, 0x41, 0x41, 0x22, 0x1C}, // 68: D
  {0x7F, 0x49, 0x49, 0x49, 0x41}, // 69: E
  {0x7F, 0x09, 0x09, 0x09, 0x01}, // 70: F
  {0x3E, 0x41, 0x49, 0x49, 0x7A}, // 71: G
  {0x7F, 0x08, 0x08, 0x08, 0x7F}, // 72: H
  {0x00, 0x41, 0x7F, 0x41, 0x00}, // 73: I
  {0x20, 0x40, 0x41, 0x3F, 0x01}, // 74: J
  {0x7F, 0x08, 0x14, 0x22, 0x41}, // 75: K
  {0x7F, 0x40, 0x40, 0x40, 0x40}, // 76: L
  {0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 77: M
  {0x7F, 0x04, 0x08, 0x10, 0x7F}, // 78: N
  {0x3E, 0x41, 0x41, 0x41, 0x3E}, // 79: O
  {0x7F, 0x09, 0x09, 0x09, 0x06}, // 80: P
  {0x3E, 0x41, 0x51, 0x21, 0x5E}, // 81: Q
  {0x7F, 0x09, 0x19, 0x29, 0x46}, // 82: R
  {0x46, 0x49, 0x49, 0x49, 0x31}, // 83: S
  {0x01, 0x01, 0x7F, 0x01, 0x01}, // 84: T
  {0x3F, 0x40, 0x40, 0x40, 0x3F}, // 85: U
  {0x1F, 0x20, 0x40, 0x20, 0x1F}, // 86: V
  {0x3F, 0x40, 0x38, 0x40, 0x3F}, // 87: W
  {0x63, 0x14, 0x08, 0x14, 0x63}, // 88: X
  {0x07, 0x08, 0x70, 0x08, 0x07}, // 89: Y
  {0x61, 0x51, 0x49, 0x45, 0x43}  // 90: Z
};

// --- 2. BULLETPROOF DRAWING ENGINE ---
void drawSolidBox(int startX, int startY, int w, int h, uint32_t color) {
    for(int i = 0; i < h; i++) {
        k10.canvas->canvasLine(startX, startY + i, startX + w, startY + i, color);
    }
}

void drawRotatedText(String text, int startX, int startY, uint32_t color, uint32_t bgColor, int scale) {
    int cursorX = startX;
    int cursorY = startY;

    for (int i = 0; i < text.length(); i++) {
        char c = toupper(text[i]); 
        int charIndex = c - 32; 
        if (charIndex < 0 || charIndex > 58) charIndex = 0; 

        if (bgColor != 0x000000) {
            drawSolidBox(cursorX - scale, cursorY - (5 * scale), 9 * scale, 7 * scale, bgColor);
        }

        for (int col = 0; col < 5; col++) {
            uint8_t line = font5x7[charIndex][col];
            for (int row = 0; row < 7; row++) {
                if (line & (1 << row)) { 
                    int drawX = cursorX + (row * scale);
                    int drawY = cursorY - (col * scale); 
                    drawSolidBox(drawX, drawY, scale, scale, color); 
                }
            }
        }
        cursorY -= (6 * scale); 
    }
}

// --- 3. LOPAKA-STYLE BOOT SPLASH SCREEN ---
void drawSplashScreen() {
    k10.canvas->canvasClear();
    drawSolidBox(0, 0, 240, 320, 0x010101);
    uint32_t accentColor = 0xB4815A; 
    
    k10.canvas->canvasCircle(63, 160, 26, accentColor, 0x010101, false);
    k10.canvas->canvasCircle(63, 160, 18, accentColor, 0x010101, false);
    
    drawRotatedText("POLYSHOT", 108, 256, 0xFF0000, 0x010101, 4);
    drawRotatedText("INITIALIZING", 158, 196, 0xFFFFFF, 0x010101, 1);
    
    k10.canvas->canvasLine(183, 60, 183, 260, 0xFFFFFF); 
    k10.canvas->canvasLine(202, 60, 202, 260, 0xFFFFFF); 
    k10.canvas->canvasLine(183, 260, 202, 260, 0xFFFFFF);
    k10.canvas->canvasLine(183, 60, 202, 60, 0xFFFFFF); 
    
    k10.canvas->updateCanvas();
    
    for(int fillY = 258; fillY > 60; fillY -= 4) {
        drawSolidBox(185, fillY, 16, 4, accentColor);
        k10.canvas->updateCanvas();
        delay(35); 
    }
    delay(400); 
}

// --- 4. PRO CAMERA UI HUD ---
void drawCameraHUD() {
    k10.canvas->canvasClear();
    
    // Cinematic Black Bars
    drawSolidBox(0, 0, 25, 320, 0x010101);   // Top Bar
    drawSolidBox(215, 0, 25, 320, 0x010101); // Bottom Bar

    // Top Bar Details
    drawRotatedText("F2.8", 8, 305, 0xFFFFFF, 0x010101, 1); 
    drawRotatedText("ISO AUTO", 8, 250, 0xFFFFFF, 0x010101, 1); 
    drawRotatedText("EV 0.0", 8, 160, 0xFFFFFF, 0x010101, 1); 
    drawRotatedText("AWB", 8, 80, 0x00FFFF, 0x010101, 1); 

    // Bottom Bar Details
    String counterText = String(imageCounter);
    if(imageCounter < 10) counterText = "00" + counterText;
    else if(imageCounter < 100) counterText = "0" + counterText;
    
    drawRotatedText("SD " + counterText, 222, 305, 0xFFFF00, 0x010101, 1);
    drawRotatedText("1080P", 222, 160, 0xFFFFFF, 0x010101, 1); 
    drawRotatedText("BATT 95%", 222, 80, 0x00FF00, 0x010101, 1);

    // Center Focus Reticle
    k10.canvas->canvasLine(90, 180, 105, 180, 0xFFFFFF); 
    k10.canvas->canvasLine(90, 180, 90, 165, 0xFFFFFF);  
    k10.canvas->canvasLine(150, 180, 135, 180, 0xFFFFFF); 
    k10.canvas->canvasLine(150, 180, 150, 165, 0xFFFFFF); 
    
    k10.canvas->canvasLine(90, 140, 105, 140, 0xFFFFFF); 
    k10.canvas->canvasLine(90, 140, 90, 155, 0xFFFFFF);  
    k10.canvas->canvasLine(150, 140, 135, 140, 0xFFFFFF); 
    k10.canvas->canvasLine(150, 140, 150, 155, 0xFFFFFF); 

    // Tiny center dot
    drawSolidBox(118, 158, 4, 4, 0xFFFFFF);

    k10.canvas->updateCanvas();
}

// --- 5. GALLERY UI ENGINE ---
void drawGalleryUI() {
    k10.canvas->canvasClear();
    // Removed the massive solid box fill here. The image covers the screen anyway!

    // Render SD Card Image
    String fileName = "S:/IMG_" + String(galleryIndex) + ".bmp";
    k10.canvas->canvasDrawImage(0, 0, fileName); 
    
    // UI Elements overlay on top of the image (faster than drawing full bars)
    drawRotatedText("GALLERY MODE", 10, 260, 0xFFFF00, 0x010101, 2);
    drawRotatedText("NEXT [>]", 220, 90, 0x00FFFF, 0x010101, 1);
    
    String fileInfo = "IMG " + String(galleryIndex) + "/" + String(imageCounter - 1);
    drawRotatedText(fileInfo, 220, 200, 0xFFFFFF, 0x010101, 1);

    k10.canvas->updateCanvas();
}

// --- 6. FAST ACTION FUNCTIONS ---
void takePictureAction() {
    if (inGalleryMode) return; 

    // 1. Instant Flash ON
    k10.rgb->setRangeColor(0, 2, 0xFFFFFF); 
    
    // 2. Save directly (Skipping all the slow screen wipes and text drawing!)
    String fileName = "S:/IMG_" + String(imageCounter) + ".bmp";
    k10.photoSaveToTFCard(fileName);
    
    // 3. Flash OFF
    k10.rgb->setRangeColor(0, 2, 0x000000); 
    
    imageCounter++;
    
    // 4. ONLY update the SD card number (Lightning fast, doesn't redraw the HUD)
    String counterText = String(imageCounter);
    if(imageCounter < 10) counterText = "00" + counterText;
    else if(imageCounter < 100) counterText = "0" + counterText;
    
    drawRotatedText("SD " + counterText, 222, 305, 0xFFFF00, 0x010101, 1);
    k10.canvas->updateCanvas();
}

void toggleModeAction() {
    inGalleryMode = !inGalleryMode;
    
    if (inGalleryMode) {
        k10.setBgCamerImage(false); 
        if (imageCounter > 1) {
            galleryIndex = imageCounter - 1; // Start at latest photo
        } else {
            galleryIndex = 0; 
        }
        drawGalleryUI();
    } else {
        k10.setBgCamerImage(true); 
        drawCameraHUD(); // Full redraw only when switching modes
    }
}

void scrollImageAction() {
    if (!inGalleryMode || imageCounter <= 1) return;
    
    galleryIndex++;
    if (galleryIndex >= imageCounter) {
        galleryIndex = 1; // Loop back to the first photo
    }
    drawGalleryUI();
}

// --- 7. MAIN ROUTINES ---
void setup() {
    Serial.begin(115200);
    k10.begin();
    k10.initScreen(screen_dir); 
    
    k10.initBgCamerImage();
    k10.setBgCamerImage(false);
    k10.creatCanvas();
    
    // Define inputs using the K10's specific edge pin macros
    pinMode(P0, INPUT_PULLUP);
    pinMode(P1, INPUT_PULLUP);
    pinMode(P2, INPUT_PULLUP);
    
    drawSplashScreen(); 
    drawCameraHUD(); 
    
    k10.initSDFile();
    k10.setBgCamerImage(true);
    
    k10.rgb->brightness(255); 
    k10.rgb->setRangeColor(0, 2, 0x000000); 
}

void loop() {
    unsigned long currentMillis = millis();
    
    bool currentStateP0 = digitalRead(P0);
    bool currentStateP1 = digitalRead(P1);
    bool currentStateP2 = digitalRead(P2);

    // P0: Take Picture Button (Instant trigger on falling edge)
    if (currentStateP0 == LOW && lastStateP0 == HIGH && (currentMillis - lastDebounceTime > DEBOUNCE_DELAY)) {
        takePictureAction();
        lastDebounceTime = currentMillis;
    }

    // P1: Toggle Mode Button
    if (currentStateP1 == LOW && lastStateP1 == HIGH && (currentMillis - lastDebounceTime > DEBOUNCE_DELAY)) {
        toggleModeAction();
        lastDebounceTime = currentMillis;
    }

    // P2: Next Image Button
    if (currentStateP2 == LOW && lastStateP2 == HIGH && (currentMillis - lastDebounceTime > DEBOUNCE_DELAY)) {
        scrollImageAction();
        lastDebounceTime = currentMillis;
    }

    // Save states for next loop iteration
    lastStateP0 = currentStateP0;
    lastStateP1 = currentStateP1;
    lastStateP2 = currentStateP2;
}

Credits

Arnov Sharma
403 projects • 446 followers
I'm Arnov. I build, design, and experiment with tech; 3D printing, PCB design, and retro consoles are my jam.

Comments